This commit is contained in:
David Arranz 2024-06-26 13:18:22 +02:00
parent 6ead69908c
commit 52de165a52
19 changed files with 343 additions and 135 deletions

View File

@ -6,34 +6,9 @@ import { IAuthRepository } from "../domain/repository";
export const findUserByEmail = async (
email: Email,
adapter: IAdapter,
repository: RepositoryBuilder<IAuthRepository>,
repository: RepositoryBuilder<IAuthRepository>
): Promise<AuthUser | null> => {
return await adapter
.startTransaction()
.complete(async (t) =>
repository({ transaction: t }).findUserByEmail(email),
);
.complete(async (t) => repository({ transaction: t }).findUserByEmail(email));
};
/*export class AuthService extends ApplicationService {
private _adapter: ISequelizeAdapter;
private _repository: IRepositoryManager;
private _authRepository: IAuthRepository;
constructor(props: {
adapter: ISequelizeAdapter,
repository: IRepositoryManager,
}) {
super();
this._adapter = props.adapter;
this._repository = props.repository;
this._repository.getRepository<IAuthRepository>("auth")
this._authRepository = ;
}
}
*/

View File

@ -1,11 +1,20 @@
import { Password } from "@/contexts/common/domain";
import { AggregateRoot, Email, IDomainError, Name, Result, UniqueID } from "@shared/contexts";
import {
AggregateRoot,
Email,
IDomainError,
Language,
Name,
Result,
UniqueID,
} from "@shared/contexts";
import { AuthUserRole } from "./AuthUserRole";
export interface IAuthUserProps {
name: Name;
email: Email;
password: Password;
language: Language;
roles: AuthUserRole[];
}
@ -14,6 +23,7 @@ export interface IAuthUser {
name: Name;
email: Email;
password: Password;
language: Language;
isUser: boolean;
isAdmin: boolean;
getRoles: () => AuthUserRole[];
@ -51,6 +61,10 @@ export class AuthUser extends AggregateRoot<IAuthUserProps> implements IAuthUser
return this.props.password;
}
get language(): Language {
return this.props.language;
}
get isUser(): boolean {
return this._hasRole(AuthUserRole.ROLE_USER);
}

View File

@ -4,37 +4,7 @@ import { IServerError } from "@/contexts/common/domain/errors";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import passport from "passport";
// Export a middleware function to authenticate incoming requests
/*export const authenticate = (req, res, next) => {
// Use Passport to authenticate the request using the "jwt" strategy
passport.authenticate("jwt", { session: false }, (err, user) => {
console.log(user);
if (err) next(err); // If there's an error, pass it on to the next middleware
if (!user) {
// If the user is not authenticated, send a 401 Unauthorized response
return res.status(401).json({
message: "Unauthorized access. No token provided.",
});
}
// If the user is authenticated, attach the user object to the request and move on to the next middleware
req.user = user;
next();
})(req, res, next);
};*/
export class AuthenticateController extends ExpressController {
//private context: AuthContext;
constructor() {
//context: IAuthContext,
super();
/*const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;*/
}
async executeImpl() {
try {
return passport.authenticate(
@ -44,22 +14,20 @@ export class AuthenticateController extends ExpressController {
err: any,
user?: AuthUser | false | null,
info?: object | string | Array<string | undefined>,
status?: number | Array<number | undefined>,
status?: number | Array<number | undefined>
) => {
if (err) {
return this.next(err);
}
if (!user) {
return this.unauthorizedError(
"Unauthorized access. No token provided.",
);
return this.unauthorizedError("Unauthorized access. No token provided.");
}
// If the user is authenticated, attach the user object to the request and move on to the next middleware
this.req.user = user;
return this.next();
},
}
);
} catch (e: unknown) {
return this.fail(e as IServerError);

View File

@ -2,9 +2,7 @@ import { IAuthUser } from "@/contexts/auth/domain";
import { IAuthContext } from "@/contexts/auth/infrastructure/Auth.context";
import { IIdentity_Response_DTO } from "@shared/contexts";
export interface IIdentityUser extends IAuthUser {
language: string;
}
export interface IIdentityUser extends IAuthUser {}
export interface IIdentityPresenter {
map: (user: IIdentityUser, context: IAuthContext) => IIdentity_Response_DTO;
@ -17,7 +15,7 @@ export const identityPresenter: IIdentityPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
language: "es",
language: user.language.toString(),
roles: user.getRoles()?.map((rol) => rol.toString()),
};
},

View File

@ -22,6 +22,7 @@ export const loginPresenter: ILoginPresenter = {
id: user.id.toString(),
name: user.name.toString(),
email: user.email.toString(),
lang_code: user.language.toString(),
roles,
token,
refresh_token: refreshToken,

View File

@ -1,7 +1,7 @@
import { Password } from "@/contexts/common/domain";
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { UserRole } from "@/contexts/users/domain";
import { Email, Name, UniqueID } from "@shared/contexts";
import { Email, Language, Name, UniqueID } from "@shared/contexts";
import { AuthUser, IAuthUserProps } from "../../domain/entities";
import { IAuthContext } from "../Auth.context";
import { AuthUserCreationAttributes, AuthUser_Model } from "../sequelize/authUser.model";
@ -22,6 +22,9 @@ class AuthUserMapper
name: this.mapsValue(source, "name", Name.create),
email: this.mapsValue(source, "email", Email.create),
password: this.mapsValue(source, "password", Password.createFromHashedTextPassword),
language: this.mapsValue(source, "lang_code", Language.createFromCode, {
defaultValue: Language.DEFAULT_LANGUAGE_CODE,
}),
roles: source.roles.map((rol) => UserRole.create(rol).object),
};
@ -41,6 +44,7 @@ class AuthUserMapper
name: source.name.toPrimitive(),
email: source.email.toPrimitive(),
password: source.password.toPrimitive(),
lang_code: source.language.toPrimitive(),
roles: source.getRoles().map((role) => role.toPrimitive()),
};
}

View File

@ -19,6 +19,7 @@ export class AuthUser_Model extends Model<
declare name: string;
declare email: string;
declare password: string;
declare lang_code: string;
declare roles: string[];
}
@ -44,6 +45,12 @@ export default (sequelize: Sequelize) => {
allowNull: false,
},
lang_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",
},
roles: {
type: DataTypes.STRING,
allowNull: false,

View File

@ -2,6 +2,8 @@ import {
AggregateRoot,
Description,
IDomainError,
Language,
Name,
Quantity,
Result,
Slug,
@ -11,9 +13,10 @@ import {
import { ArticleIdentifier } from "./ArticleIdentifier";
export interface IArticleProps {
catalog_name: Slug;
catalog_name: Name;
id_article: ArticleIdentifier;
reference: Slug;
language: Language;
description: Description;
points: Quantity;
retail_price: UnitPrice;
@ -21,9 +24,10 @@ export interface IArticleProps {
export interface IArticle {
id: UniqueID;
catalog_name: Slug;
catalog_name: Name;
id_article: ArticleIdentifier;
reference: Slug;
language: Language;
description: Description;
points: Quantity;
retail_price: UnitPrice;
@ -63,6 +67,10 @@ export class Article extends AggregateRoot<IArticleProps> implements IArticle {
return this.props.reference;
}
get language(): Language {
return this.props.language;
}
get description(): Description {
return this.props.description;
}

View File

@ -1,5 +1,6 @@
import Joi from "joi";
import { AuthUser } from "@/contexts/auth/domain";
import { ListArticlesResult, ListArticlesUseCase } from "@/contexts/catalog/application";
import { Article } from "@/contexts/catalog/domain";
import { QueryCriteriaService } from "@/contexts/common/application/services";
@ -10,6 +11,7 @@ import {
IListArticles_Response_DTO,
IListResponse_DTO,
IQueryCriteria,
Language,
Result,
RuleValidator,
} from "@shared/contexts";
@ -49,7 +51,13 @@ export class ListArticlesController extends ExpressController {
}
async executeImpl() {
const queryOrError = this.validateQuery(this.req.query);
const { language = Language.createDefaultCode() } = <AuthUser>this.req.user;
const queryOrError = this.validateQuery({
$filters: `lang_code[eq]${language.toString()}`,
...this.req.query,
});
if (queryOrError.isFailure) {
return this.clientError(queryOrError.error.message);
}
@ -67,10 +75,10 @@ export class ListArticlesController extends ExpressController {
return this.clientError(result.error.message);
}
const customers = <ICollection<Article>>result.object;
const articles = <ICollection<Article>>result.object;
return this.ok<IListResponse_DTO<IListArticles_Response_DTO>>(
this.presenter.mapArray(customers, this.context, {
this.presenter.mapArray(articles, this.context, {
page: queryCriteria.pagination.offset,
limit: queryCriteria.pagination.limit,
})

View File

@ -1,8 +1,8 @@
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Description, Quantity, Slug, UniqueID, UnitPrice } from "@shared/contexts";
import { Description, Language, Name, Quantity, Slug, UniqueID, UnitPrice } from "@shared/contexts";
import { ICatalogContext } from "..";
import { Article, ArticleIdentifier, IArticleProps } from "../../domain/entities";
import { ArticleCreationAttributes, Article_Model } from "../sequelize/article.model";
import { ArticleCreationAttributes, Article_Model } from "../sequelize";
export interface IArticleMapper
extends ISequelizeMapper<Article_Model, ArticleCreationAttributes, Article> {}
@ -16,15 +16,27 @@ class ArticleMapper
}
protected toDomainMappingImpl(source: Article_Model, params: any): Article {
const catalog_name = this.mapsValue(source, "catalog_name", Name.create);
const id_article = this.mapsValue(source, "id_article", ArticleIdentifier.create);
const reference = this.mapsValue(source, "reference", Slug.create);
const points = this.mapsValue(source, "points", (value: any) =>
Quantity.create({ amount: value, precision: 4 })
);
const retail_price = this.mapsValue(source, "retail_price", (value: any) =>
UnitPrice.create({ amount: value, precision: 4 })
);
const language = this.mapsValue(source, "lang_code", Language.createFromCode);
const description = this.mapsValue(source, "description", Description.create);
const props: IArticleProps = {
catalog_name: this.mapsValue(source, "catalog_name", Slug.create),
id_article: this.mapsValue(source, "id_article", ArticleIdentifier.create),
reference: this.mapsValue(source, "reference", Slug.create),
description: this.mapsValue(source, "description", Description.create),
points: this.mapsValue(source, "points", Quantity.create),
retail_price: this.mapsValue(source, "retail_price", (value: any) =>
UnitPrice.create({ amount: value, precision: 4 })
),
catalog_name,
id_article,
reference,
points,
retail_price,
language,
description,
};
const id = this.mapsValue(source, "id", UniqueID.create);

View File

@ -15,9 +15,9 @@ export class Article_Model extends Model<
InferCreationAttributes<Article_Model>
> {
// To avoid table creation
/*static async sync(): Promise<any> {
static async sync(): Promise<any> {
return Promise.resolve();
}*/
}
static associate(connection: Sequelize) {}
@ -26,6 +26,7 @@ export class Article_Model extends Model<
declare catalog_name: string;
declare id_article: string; // number ??
declare reference: CreationOptional<string>;
declare lang_code: CreationOptional<string>;
declare description: CreationOptional<string>;
declare points: CreationOptional<number>;
declare retail_price: CreationOptional<number>;
@ -43,37 +44,38 @@ export default (sequelize: Sequelize) => {
type: DataTypes.STRING(),
allowNull: false,
},
id_article: {
type: DataTypes.BIGINT().UNSIGNED,
allowNull: false,
},
reference: DataTypes.STRING(),
lang_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",
},
description: DataTypes.STRING(),
points: {
type: DataTypes.SMALLINT().UNSIGNED,
type: DataTypes.BIGINT().UNSIGNED,
defaultValue: 0,
},
retail_price: DataTypes.BIGINT(),
},
{
sequelize,
tableName: "catalog",
tableName: "v_catalog",
//paranoid: true, // softs deletes
timestamps: true,
//version: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{ name: "catalog_name_idx", fields: ["catalog_name"] },
{ name: "id_article_idx", fields: ["id_article"] },
{ name: "updated_at_idx", fields: ["updated_at"] },
],
paranoid: true, // softs deletes
timestamps: false,
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
scopes: {
quickSearch(value) {
return {

View File

@ -0,0 +1,140 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Op,
Sequelize,
} from "sequelize";
import { ArticleTranslation_Model } from "./articleTraslation.model.ts.bak";
export type ArticleCreationAttributes = InferCreationAttributes<
Article_Model,
{
omit: "translations";
}
>;
export class Article_Model extends Model<
InferAttributes<Article_Model, { omit: "translations" }>,
InferCreationAttributes<Article_Model, { omit: "translations" }>
> {
// To avoid table creation
static async sync(): Promise<any> {
return Promise.resolve();
}
static associate(connection: Sequelize) {
const { Article_Model, ArticleTranslation_Model } = connection.models;
Article_Model.hasMany(ArticleTranslation_Model, {
as: "translations",
foreignKey: "catalog_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare catalog_name: string;
declare id_article: string; // number ??
declare reference: CreationOptional<string>;
//declare description: CreationOptional<string>;
declare points: CreationOptional<number>;
declare retail_price: CreationOptional<number>;
declare translations?: NonAttribute<ArticleTranslation_Model[]>;
}
export default (sequelize: Sequelize) => {
Article_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
catalog_name: {
type: DataTypes.STRING(),
allowNull: false,
},
id_article: {
type: DataTypes.BIGINT().UNSIGNED,
allowNull: false,
},
reference: DataTypes.STRING(),
//description: DataTypes.STRING(),
points: {
type: DataTypes.BIGINT().UNSIGNED,
defaultValue: 0,
},
retail_price: DataTypes.BIGINT(),
},
{
sequelize,
tableName: "catalog",
paranoid: true, // softs deletes
timestamps: true,
//version: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{ name: "catalog_name_idx", fields: ["catalog_name"] },
{ name: "id_article_idx", fields: ["id_article"] },
{ name: "updated_at_idx", fields: ["updated_at"] },
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {
attributes: { exclude: ["id_article", "reference"] },
include: [
{
attributes: ["description"],
model: ArticleTranslation_Model,
as: "translations",
required: true,
where: {
lang_code: "es",
},
},
],
},
scopes: {
quickSearch(value) {
return {
include: [
{
model: ArticleTranslation_Model,
as: "translations",
required: true,
where: {
[Op.or]: {
reference: {
[Op.like]: `%${value}%`,
},
description: {
[Op.like]: `%${value}%`,
},
},
},
},
],
};
},
},
}
);
return Article_Model;
};

View File

@ -0,0 +1,83 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Op,
Sequelize,
} from "sequelize";
export type ArticleTranslationCreationAttributes =
InferCreationAttributes<ArticleTranslation_Model>;
export class ArticleTranslation_Model extends Model<
InferAttributes<ArticleTranslation_Model>,
InferCreationAttributes<ArticleTranslation_Model>
> {
// To avoid table creation
/*static async sync(): Promise<any> {
return Promise.resolve();
}*/
static associate(connection: Sequelize) {
const { Article_Model, ArticleTranslation_Model } = connection.models;
ArticleTranslation_Model.belongsTo(Article_Model, {
as: "translations",
foreignKey: "catalog_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare lang_code: CreationOptional<string>;
declare description: CreationOptional<string>;
}
export default (sequelize: Sequelize) => {
ArticleTranslation_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
lang_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",
},
description: DataTypes.STRING(),
},
{
sequelize,
tableName: "catalog_translations",
//paranoid: true, // softs deletes
timestamps: false,
//version: true,
indexes: [{ name: "lang_code_idx", fields: ["lang_code"] }],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
scopes: {
quickSearch(value) {
return {
where: {
[Op.or]: {
description: {
[Op.like]: `%${value}%`,
},
},
},
};
},
},
}
);
return ArticleTranslation_Model;
};

View File

@ -9,10 +9,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
protected _transaction: Transaction;
protected _adapter: ISequelizeAdapter;
public constructor(props: {
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
public constructor(props: { adapter: ISequelizeAdapter; transaction: Transaction }) {
this._adapter = props.adapter;
this._transaction = props.transaction;
}
@ -49,7 +46,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
modelName: string,
field: string,
value: any,
params: any = {},
params: any = {}
): Promise<T> {
const _model = this._adapter.getModel(modelName);
const where: { [key: string]: any } = {};
@ -63,11 +60,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
});
}
protected async _getById(
modelName: string,
id: UniqueID | string,
params: any = {},
): Promise<T> {
protected async _getById(modelName: string, id: UniqueID | string, params: any = {}): Promise<T> {
const _model = this._adapter.getModel(modelName);
return _model.findByPk(id.toString(), params);
}
@ -75,7 +68,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
protected async _findAll(
modelName: string,
queryCriteria?: IQueryCriteria,
params: any = {},
params: any = {}
): Promise<{ rows: any[]; count: number }> {
console.time("_findAll");
@ -84,6 +77,8 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
queryCriteria,
});
console.log(query);
const args = {
...query,
distinct: true,
@ -102,7 +97,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
modelName: string,
field: string,
value: any,
params: any = {},
params: any = {}
): Promise<boolean> {
const _model = this._adapter.getModel(modelName) as ModelDefined<any, any>;
const where = {};
@ -120,7 +115,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
modelName: string,
id: UniqueID,
data: any,
params: any = {},
params: any = {}
): Promise<void> {
const _model = this._adapter.getModel(modelName);
@ -134,7 +129,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
where: { id: id.toPrimitive() },
transaction: this._transaction,
...params,
},
}
);
} else {
await _model.create(
@ -146,7 +141,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
include: [{ all: true }],
transaction: this._transaction,
...params,
},
}
);
}
}
@ -155,7 +150,7 @@ export abstract class SequelizeRepository<T> implements IRepository<T> {
modelName: string,
id: UniqueID,
force: boolean = false,
params: any = {},
params: any = {}
): Promise<void> {
const model: ModelDefined<any, any> = this._adapter.getModel(modelName);

View File

@ -38,7 +38,7 @@ export class User_Model extends Model<
declare name: string;
declare email: string;
declare password: string;
declare language: string;
declare lang_code: string;
declare roles: string[];
}
@ -64,8 +64,8 @@ export default (sequelize: Sequelize) => {
allowNull: false,
},
language: {
type: DataTypes.STRING,
lang_code: {
type: DataTypes.STRING(2),
allowNull: false,
defaultValue: "es",
},

View File

@ -1,8 +1,7 @@
import { checkUser, createLoginController } from "@/contexts/auth";
import { createIdentityController } from "@/contexts/auth/infrastructure/express/controllers/identity";
import Express from "express";
import passport from "passport";
import { createLoginController } from "../../../../contexts/auth/infrastructure/express/controllers";
import { createIdentityController } from "../../../../contexts/auth/infrastructure/express/controllers/identity";
import { checkUser } from "../../../../contexts/auth/infrastructure/express/passport";
export const authRouter = (appRouter: Express.Router) => {
const authRoutes: Express.Router = Express.Router({ mergeParams: true });

View File

@ -2,6 +2,7 @@ export interface ILogin_Response_DTO {
id: string;
name: string;
email: string;
lang_code: string;
roles: string[];
token: string;
refresh_token: string;

View File

@ -3,8 +3,8 @@ import { IMoney_Response_DTO } from "../../../../common";
export interface IListArticles_Response_DTO {
id: string;
catalog_name: string;
id_article: string;
reference: string;
//id_article: string;
//reference: string;
//family: string;
//subfamily: string;
description: string;

View File

@ -3,15 +3,12 @@ import { UndefinedOr } from "../../../../utilities";
import { RuleValidator } from "../RuleValidator";
import { DomainError, handleDomainError } from "../errors";
import { Result } from "./Result";
import {
IStringValueObjectOptions,
StringValueObject,
} from "./StringValueObject";
import { IStringValueObjectOptions, StringValueObject } from "./StringValueObject";
export interface INameOptions extends IStringValueObjectOptions {}
export class Name extends StringValueObject {
private static readonly MIN_LENGTH = 2;
//private static readonly MIN_LENGTH = 1;
private static readonly MAX_LENGTH = 100;
protected static validate(value: UndefinedOr<string>, options: INameOptions) {
@ -20,7 +17,7 @@ export class Name extends StringValueObject {
.allow("")
.default("")
.trim()
.min(Name.MIN_LENGTH)
//.min(Name.MIN_LENGTH)
.max(Name.MAX_LENGTH)
.label(options.label ? options.label : "value");
@ -37,11 +34,7 @@ export class Name extends StringValueObject {
if (validationResult.isFailure) {
return Result.fail(
handleDomainError(
DomainError.INVALID_INPUT_DATA,
validationResult.error.message,
_options
)
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
);
}