This commit is contained in:
David Arranz 2024-05-21 18:48:40 +02:00
parent c2550e57f2
commit 50a6001252
71 changed files with 1769 additions and 173 deletions

View File

@ -1,11 +0,0 @@
{
"printWidth": 130,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "always"
}

13
.prettierrc Normal file
View File

@ -0,0 +1,13 @@
{
"bracketSpacing": true,
"useTabs": false,
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"jsxSingleQuote": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"rcVerbose": true
}

View File

@ -3,5 +3,10 @@
"source.organizeImports": "explicit",
"source.fixAll.eslint": "explicit"
},
"editor.formatOnSave": true
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": false,
"prettier.useEditorConfig": false,
"prettier.useTabs": false,
"prettier.configPath": ".prettierrc"
}

View File

@ -1,16 +1,12 @@
import { AuthUser } from "@/contexts/auth/domain";
import { generateExpressErrorResponse } from "@/contexts/common/infrastructure/express/ExpressErrorResponse";
import { generateExpressError } from "@/contexts/common/infrastructure/express/ExpressErrorResponse";
import Express from "express";
import httpStatus from "http-status";
import passport from "passport";
function compose(middlewareArray: any[]) {
if (!middlewareArray.length) {
return function (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction,
) {
return function (req: Express.Request, res: Express.Response, next: Express.NextFunction) {
next();
};
}
@ -18,11 +14,7 @@ function compose(middlewareArray: any[]) {
const head = middlewareArray[0];
const tail = middlewareArray.slice(1);
return function (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction,
) {
return function (req: Express.Request, res: Express.Response, next: Express.NextFunction) {
head(req, res, function (err: unknown) {
if (err) return next(err);
compose(tail)(req, res, next);
@ -37,7 +29,7 @@ export const isLoggedUser = compose([
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user;
if (!user.isUser) {
return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED);
return generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}
next();
},
@ -48,7 +40,7 @@ export const isAdminUser = compose([
(req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
const user = <AuthUser>req.user;
if (!user.isAdmin) {
return generateExpressErrorResponse(req, res, httpStatus.UNAUTHORIZED);
return generateExpressError(req, res, httpStatus.UNAUTHORIZED);
}
next();
},

View File

@ -7,19 +7,19 @@ import { Email, Name, UniqueID } from "@shared/contexts";
import { AuthUser, IAuthUserProps } from "../../domain/entities";
import { IAuthContext } from "../Auth.context";
import {
AuthUserCreationAttributes,
AuthUser_Model,
TCreationUser_Attributes,
} from "../sequelize/authUser.model";
export interface IUserMapper
extends ISequelizeMapper<
AuthUser_Model,
TCreationUser_Attributes,
AuthUserCreationAttributes,
AuthUser
> {}
class AuthUserMapper
extends SequelizeMapper<AuthUser_Model, TCreationUser_Attributes, AuthUser>
extends SequelizeMapper<AuthUser_Model, AuthUserCreationAttributes, AuthUser>
implements IUserMapper
{
public constructor(props: { context: IAuthContext }) {

View File

@ -6,7 +6,8 @@ import {
Sequelize,
} from "sequelize";
export type TCreationUser_Attributes = InferCreationAttributes<AuthUser_Model>;
export type AuthUserCreationAttributes =
InferCreationAttributes<AuthUser_Model>;
export class AuthUser_Model extends Model<
InferAttributes<AuthUser_Model>,

View File

@ -4,7 +4,7 @@ import { registerCatalogRepository } from "../../../Catalog.repository";
import { ListArticlesController } from "./ListArticlesController";
import { listArticlesPresenter } from "./presenter";
export const createListArticlesController = (context: ICatalogContext) => {
export const listArticlesController = (context: ICatalogContext) => {
registerCatalogRepository(context);
return new ListArticlesController(
@ -12,6 +12,6 @@ export const createListArticlesController = (context: ICatalogContext) => {
useCase: new ListArticlesUseCase(context),
presenter: listArticlesPresenter,
},
context,
context
);
};

View File

@ -1,6 +1,6 @@
import { applyMiddleware } from "@/contexts/common/infrastructure/express";
import Express from "express";
import { createListArticlesController } from "./controllers";
import { listArticlesController } from "./controllers";
/*catalogRoutes.get(
"/:articleId",
@ -15,11 +15,7 @@ export const CatalogRouter = (appRouter: Express.Router) => {
"/",
applyMiddleware("isLoggedUser"),
(req: Express.Request, res: Express.Response, next: Express.NextFunction) =>
createListArticlesController(res.locals["context"]).execute(
req,
res,
next,
),
listArticlesController(res.locals["context"]).execute(req, res, next)
);
appRouter.use("/catalog", catalogRoutes);

View File

@ -16,19 +16,15 @@ import {
IArticleProps,
} from "../../domain/entities";
import {
ArticleCreationAttributes,
Article_Model,
TCreationArticle_Attributes,
} from "../sequelize/article.model";
export interface IArticleMapper
extends ISequelizeMapper<
Article_Model,
TCreationArticle_Attributes,
Article
> {}
extends ISequelizeMapper<Article_Model, ArticleCreationAttributes, Article> {}
class ArticleMapper
extends SequelizeMapper<Article_Model, TCreationArticle_Attributes, Article>
extends SequelizeMapper<Article_Model, ArticleCreationAttributes, Article>
implements IArticleMapper
{
public constructor(props: { context: ICatalogContext }) {

View File

@ -8,8 +8,7 @@ import {
Sequelize,
} from "sequelize";
export type TCreationArticle_Attributes =
InferCreationAttributes<Article_Model>;
export type ArticleCreationAttributes = InferCreationAttributes<Article_Model>;
export class Article_Model extends Model<
InferAttributes<Article_Model>,

View File

@ -5,7 +5,7 @@ import { URL } from "url";
import { IServerError } from "../../domain/errors";
import { IController } from "../Controller.interface";
import { InfrastructureError } from "../InfrastructureError";
import { generateExpressErrorResponse } from "./ExpressErrorResponse";
import { generateExpressError } from "./ExpressErrorResponse";
export abstract class ExpressController implements IController {
protected req: express.Request;
@ -17,19 +17,13 @@ export abstract class ExpressController implements IController {
protected abstract executeImpl(): Promise<void | any>;
public execute(
req: express.Request,
res: express.Response,
next: express.NextFunction,
): void {
public execute(req: express.Request, res: express.Response, next: express.NextFunction): void {
this.req = req;
this.res = res;
this.next = next;
this.serverURL = `${
new URL(
`${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`,
).origin
new URL(`${this.req.protocol}://${this.req.get("host")}${this.req.originalUrl}`).origin
}/api/v1`;
this.file = this.req && this.req["file"]; // <-- ????
@ -100,11 +94,7 @@ export abstract class ExpressController implements IController {
}
public internalServerError(message?: string, error?: IServerError) {
return this._errorResponse(
httpStatus.INTERNAL_SERVER_ERROR,
message,
error,
);
return this._errorResponse(httpStatus.INTERNAL_SERVER_ERROR, message, error);
}
public todoError(message?: string) {
@ -115,24 +105,15 @@ export abstract class ExpressController implements IController {
return this._errorResponse(httpStatus.SERVICE_UNAVAILABLE, message);
}
private _jsonResponse(
statusCode: number,
jsonPayload: any,
): express.Response<any> {
private _jsonResponse(statusCode: number, jsonPayload: any): express.Response<any> {
return this.res.status(statusCode).json(jsonPayload).send();
}
private _errorResponse(
statusCode: number,
message?: string,
error?: Error | InfrastructureError,
error?: Error | InfrastructureError
): express.Response<IError_Response_DTO> {
return generateExpressErrorResponse(
this.req,
this.res,
statusCode,
message,
error,
);
return generateExpressError(this.req, this.res, statusCode, message, error);
}
}

View File

@ -1,18 +1,15 @@
import {
IErrorExtra_Response_DTO,
IError_Response_DTO,
} from "@shared/contexts";
import { IErrorExtra_Response_DTO, IError_Response_DTO } from "@shared/contexts";
import Express from "express";
import { UseCaseError } from "../../application";
import { InfrastructureError } from "../InfrastructureError";
import { ProblemDocument, ProblemDocumentExtension } from "./ProblemDocument";
export const generateExpressErrorResponse = (
export const generateExpressError = (
req: Express.Request,
res: Express.Response,
statusCode: number,
message?: string,
error?: Error | InfrastructureError,
error?: Error | InfrastructureError
): Express.Response<IError_Response_DTO> => {
const context = {
user: res.locals.user || undefined,
@ -23,7 +20,7 @@ export const generateExpressErrorResponse = (
const extension = new ProblemDocumentExtension({
context,
extra: error ? { ...generateExpressError(error) } : {},
extra: error ? { ..._extractExtraInfoFromError(error) } : {},
});
const jsonPayload = new ProblemDocument(
@ -32,15 +29,13 @@ export const generateExpressErrorResponse = (
detail: message,
instance: req.baseUrl,
},
extension,
extension
);
return res.status(statusCode).json(jsonPayload).send();
};
function generateExpressError(
error: Error | InfrastructureError,
): IErrorExtra_Response_DTO {
function _extractExtraInfoFromError(error: Error | InfrastructureError): IErrorExtra_Response_DTO {
const useCaseError = error as UseCaseError;
const payload = Array.isArray(useCaseError.payload)

View File

@ -1,37 +1,27 @@
import { Collection, Entity, ICollection, Result } from "@shared/contexts";
import { Model, ValidationError } from "sequelize";
import {
FieldValueError,
RequiredFieldMissingError,
} from "../../domain/errors";
import { FieldValueError, RequiredFieldMissingError } from "../../domain/errors";
import { InfrastructureError } from "../InfrastructureError";
export type MapperParamsType = Record<string, any>;
export interface ISequelizeMapper<
TModel extends Model,
TModelAttributes,
TEntity extends Entity<any>,
> {
mapToDomain(source: TModel, params?: Record<string, any>): TEntity;
mapArrayToDomain(
source: TModel[],
params?: Record<string, any>
): Collection<TEntity>;
mapToDomain(source: TModel, params?: MapperParamsType): TEntity;
mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection<TEntity>;
mapArrayAndCountToDomain(
source: TModel[],
totalCount: number,
params?: Record<string, any>
params?: MapperParamsType
): Collection<TEntity>;
mapToPersistence(
source: TEntity,
params?: Record<string, any>
): TModelAttributes;
mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes;
mapCollectionToPersistence(
source: ICollection<TEntity>,
params?: Record<string, any>
params?: MapperParamsType
): TModelAttributes[];
}
@ -43,64 +33,49 @@ export abstract class SequelizeMapper<
{
public constructor(protected props: any) {}
public mapToDomain(source: TModel, params?: Record<string, any>): TEntity {
public mapToDomain(source: TModel, params?: MapperParamsType): TEntity {
return this.toDomainMappingImpl(source, params);
}
public mapArrayToDomain(
source: TModel[],
params?: Record<string, any>
): Collection<TEntity> {
return this.mapArrayAndCountToDomain(
source,
source ? source.length : 0,
params
);
public mapArrayToDomain(source: TModel[], params?: MapperParamsType): Collection<TEntity> {
return this.mapArrayAndCountToDomain(source, source ? source.length : 0, params);
}
public mapArrayAndCountToDomain(
source: TModel[],
totalCount: number,
params?: Record<string, any>
params?: MapperParamsType
): Collection<TEntity> {
const items = source
? source.map((value, index: number) =>
this.toDomainMappingImpl!(value, { index, ...params })
)
? source.map((value, index: number) => this.toDomainMappingImpl!(value, { index, ...params }))
: [];
return new Collection(items, totalCount);
}
public mapToPersistence(
source: TEntity,
params?: Record<string, any>
): TModelAttributes {
public mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes {
return this.toPersistenceMappingImpl(source, params);
}
public mapCollectionToPersistence(
source: ICollection<TEntity>,
params?: Record<string, any>
params?: MapperParamsType
): TModelAttributes[] {
return source.items.map((value: TEntity, index: number) =>
this.toPersistenceMappingImpl!(value, { index, ...params })
);
}
protected toDomainMappingImpl(
source: TModel,
params?: Record<string, any>
): TEntity {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected toDomainMappingImpl(source: TModel, params?: MapperParamsType): TEntity {
throw InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
'Method "toDomainMappingImpl" not implemented!'
);
}
protected toPersistenceMappingImpl(
source: TEntity,
params?: Record<string, any>
): TModelAttributes {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected toPersistenceMappingImpl(source: TEntity, params?: MapperParamsType): TModelAttributes {
throw InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
'Method "toPersistenceMappingImpl" not implemented!'
@ -118,20 +93,24 @@ export abstract class SequelizeMapper<
protected mapsValue(
row: TModel,
key: string,
customMapFn: (
value: any,
params: Record<string, any>
) => Result<any, Error>,
params: Record<string, any> = {
customMapFn: (value: any, params: MapperParamsType) => Result<any, Error>,
params: MapperParamsType = {
defaultValue: null,
}
) {
let value = params.defaultValue;
const value = row?.dataValues[key] ?? params.defaultValue;
const valueOrError = customMapFn(value, params);
if (valueOrError.isFailure) {
this._handleFailure(valueOrError.error, key);
}
return valueOrError.object;
/*let value = params.defaultValue;
if (!row || typeof row !== "object") {
console.debug(
`Data row has not keys! Key ${key} not exists in data row!`
);
console.debug(`Data row has not keys! Key ${key} not exists in data row!`);
} else if (!Object.hasOwn(row.dataValues, key)) {
console.debug(`Key ${key} not exists in data row!`);
} else {
@ -143,16 +122,46 @@ export abstract class SequelizeMapper<
if (valueOrError.isFailure) {
this.handleFailure(valueOrError.error, key);
}
return valueOrError.object;
return valueOrError.object;*/
}
protected mapsAssociation(
row: TModel,
associationName: string,
customMapper: any,
params: Record<string, any> = {}
params: MapperParamsType = {}
) {
if (!customMapper) {
if (!customMapper)
throw InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
'Custom mapper undefined at "mapsAssociation"!'
);
const { filter, ...otherParams } = params;
let associationRows = row?.dataValues[associationName] ?? [];
if (filter)
associationRows = Array.isArray(associationRows)
? associationRows.filter(filter)
: filter(associationRows);
const customMapFn = Array.isArray(associationRows)
? customMapper.mapArrayToDomain
: customMapper.mapToDomain;
if (!customMapFn)
throw InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
'Custom mapper function undefined at "mapsAssociation"!'
);
const associatedDataOrError = customMapFn(associationRows, otherParams);
if (associatedDataOrError.isFailure)
this._handleFailure(associatedDataOrError.error, associationName);
return associatedDataOrError.object;
/*if (!customMapper) {
throw InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
'Custom mapper undefined at "mapsAssociation"!'
@ -195,13 +204,10 @@ export abstract class SequelizeMapper<
if (associatedDataOrError.isFailure) {
this.handleFailure(associatedDataOrError.error, associationName);
}
return associatedDataOrError.object;
//const associatedData = row[association.accessors.get]();
//return associatedData;
return associatedDataOrError.object;*/
}
private handleFailure(error: Error, key: string) {
private _handleFailure(error: Error, key: string) {
if (error instanceof ValidationError) {
this.handleInvalidFieldError(key, error);
} else {

View File

@ -0,0 +1,143 @@
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IRepositoryManager, Password } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
DomainError,
Email,
ICreateDealer_Request_DTO,
IDomainError,
Name,
Result,
UniqueID,
ensureIdIsValid,
ensureNameIsValid,
} from "@shared/contexts";
import { Dealer, IDealerRepository } from "../domain";
export type CreateDealerResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Dealer, never>; // Success!
export class CreateDealerUseCase
implements IUseCase<ICreateDealer_Request_DTO, Promise<CreateDealerResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
async execute(request: ICreateDealer_Request_DTO) {
const { id, name } = request;
// Validaciones de datos
const idOrError = ensureIdIsValid(id);
if (idOrError.isFailure) {
const message = idOrError.error.message; //`Dealer ID ${dealerDTO.id} is not valid`;
return Result.fail(
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "id" }])
);
}
const nameOrError = ensureNameIsValid(name);
if (nameOrError.isFailure) {
const message = nameOrError.error.message; //`Dealer ID ${dealerDTO.id} is not valid`;
return Result.fail(
UseCaseError.create(UseCaseError.INVALID_INPUT_DATA, message, [{ path: "name" }])
);
}
// Comprobar que no existe un usuario previo con esos datos
const dealerRepository = this._getDealerRepository();
const idExists = await dealerRepository().exists(idOrError.object);
if (idExists) {
const message = `Another dealer with same ID exists`;
return Result.fail(
UseCaseError.create(UseCaseError.RESOURCE_ALREADY_EXITS, message, {
path: "id",
})
);
}
// Crear dealer
const dealerOrError = this._tryCreateDealerInstance(request, idOrError.object);
if (dealerOrError.isFailure) {
const { error: domainError } = dealerOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
case DomainError.INVALID_INPUT_DATA:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message = "El usuario tiene algún dato erróneo.";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = domainError.message;
break;
}
return Result.fail(UseCaseError.create(errorCode, message, domainError));
}
return this._saveDealer(dealerOrError.object);
}
private async _saveDealer(dealer: Dealer) {
// Guardar el contacto
const transaction = this._adapter.startTransaction();
const dealerRepository = this._getDealerRepository();
let dealerRepo: IDealerRepository;
try {
await transaction.complete(async (t) => {
dealerRepo = dealerRepository({ transaction: t });
await dealerRepo.create(dealer);
});
return Result.ok<Dealer>(dealer);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
}
}
private _tryCreateDealerInstance(
dealerDTO: ICreateDealer_Request_DTO,
dealerId: UniqueID
): Result<Dealer, IDomainError> {
const nameOrError = Name.create(dealerDTO.name);
if (nameOrError.isFailure) {
return Result.fail(nameOrError.error);
}
const emailOrError = Email.create(dealerDTO.email);
if (emailOrError.isFailure) {
return Result.fail(emailOrError.error);
}
const passwordOrError = Password.createFromPlainTextPassword(dealerDTO.password);
if (passwordOrError.isFailure) {
return Result.fail(passwordOrError.error);
}
return Dealer.create(
{
name: nameOrError.object,
email: emailOrError.object,
password: passwordOrError.object,
},
dealerId
);
}
private _getDealerRepository() {
return this._repositoryManager.getRepository<IDealerRepository>("Dealer");
}
}

View File

@ -0,0 +1,55 @@
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application/useCases";
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { Result, UniqueID } from "@shared/contexts";
import { IDealerRepository } from "../domain";
export interface IDeleteDealerUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
}
export type DeleteDealerResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<void, never>; // Success!
export class DeleteDealerUseCase
implements IUseCase<IDeleteDealerUseCaseRequest, Promise<DeleteDealerResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
private getRepositoryByName<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(request: IDeleteDealerUseCaseRequest): Promise<DeleteDealerResponseOrError> {
const { id: dealerId } = request;
const transaction = this._adapter.startTransaction();
const dealerRepoBuilder = this.getRepositoryByName<IDealerRepository>("Dealer");
try {
await transaction.complete(async (t) => {
const invoiceRepo = dealerRepoBuilder({ transaction: t });
await invoiceRepo.removeById(dealerId);
});
return Result.ok<void>();
} catch (error: unknown) {
//const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al eliminar el usuario")
);
}
}
}

View File

@ -0,0 +1,71 @@
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application/useCases";
import { IRepositoryManager } from "@/contexts/common/domain";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { Result, UniqueID } from "@shared/contexts";
import { IDealerRepository } from "../domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { Dealer } from "../domain/entities/Dealer";
export interface IGetDealerUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
}
export type GetDealerResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Dealer, never>; // Success!
export class GetDealerUseCase
implements IUseCase<IGetDealerUseCaseRequest, Promise<GetDealerResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
private getRepositoryByName<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(request: IGetDealerUseCaseRequest): Promise<GetDealerResponseOrError> {
const { id } = request;
// Validación de datos
// No hay en este caso
return await this.findDealer(id);
}
private async findDealer(id: UniqueID) {
const transaction = this._adapter.startTransaction();
const dealerRepoBuilder = this.getRepositoryByName<IDealerRepository>("Dealer");
let dealer: Dealer | null = null;
try {
await transaction.complete(async (t) => {
const dealerRepo = dealerRepoBuilder({ transaction: t });
dealer = await dealerRepo.getById(id);
});
if (!dealer) {
return Result.fail(UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, "Dealer not found"));
}
return Result.ok<Dealer>(dealer!);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error)
);
}
}
}

View File

@ -0,0 +1,57 @@
import { IUseCase, IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { IRepositoryManager } from "@/contexts/common/domain";
import { Collection, ICollection, IQueryCriteria, Result } from "@shared/contexts";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { Dealer } from "../domain";
import { IDealerRepository } from "../domain/repository";
export interface IListDealersParams {
queryCriteria: IQueryCriteria;
}
export type ListDealersResult =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<ICollection<Dealer>, never>; // Success!
export class ListDealersUseCase
implements IUseCase<IListDealersParams, Promise<ListDealersResult>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
private getRepositoryByName<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
async execute(params: Partial<IListDealersParams>): Promise<ListDealersResult> {
const { queryCriteria } = params;
return this.findDealers(queryCriteria);
}
private async findDealers(queryCriteria) {
const transaction = this._adapter.startTransaction();
const dealerRepoBuilder = this.getRepositoryByName<IDealerRepository>("Dealer");
let dealers: ICollection<Dealer> = new Collection();
try {
await transaction.complete(async (t) => {
dealers = await dealerRepoBuilder({ transaction: t }).findAll(queryCriteria);
});
return Result.ok(dealers);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al listar los usurios", _error)
);
}
}
}

View File

@ -0,0 +1,134 @@
import {
IUseCase,
IUseCaseError,
IUseCaseRequest,
UseCaseError,
} from "@/contexts/common/application";
import { IRepositoryManager, Password } from "@/contexts/common/domain";
import { IInfrastructureError } from "@/contexts/common/infrastructure";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import {
DomainError,
Email,
IDomainError,
IUpdateDealer_Request_DTO,
Name,
Result,
UniqueID,
} from "@shared/contexts";
import { Dealer, IDealerRepository } from "../domain";
export interface IUpdateDealerUseCaseRequest extends IUseCaseRequest {
id: UniqueID;
dealerDTO: IUpdateDealer_Request_DTO;
}
export type UpdateDealerResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Dealer, never>; // Success!
export class UpdateDealerUseCase
implements IUseCase<IUpdateDealerUseCaseRequest, Promise<UpdateDealerResponseOrError>>
{
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
this._adapter = props.adapter;
this._repositoryManager = props.repositoryManager;
}
async execute(request: IUpdateDealerUseCaseRequest): Promise<UpdateDealerResponseOrError> {
const { id, dealerDTO } = request;
const dealerRepository = this._getDealerRepository();
// Comprobar que existe el dealer
const idExists = await dealerRepository().exists(id);
if (!idExists) {
const message = `Dealer ID not found`;
return Result.fail(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, message, {
path: "id",
})
);
}
// Crear usuario
const dealerOrError = this._tryCreateDealerInstance(dealerDTO, id);
if (dealerOrError.isFailure) {
const { error: domainError } = dealerOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
// Errores manuales
case DomainError.INVALID_INPUT_DATA:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message = "El usuario tiene algún dato erróneo.";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = domainError.message;
break;
}
return Result.fail(UseCaseError.create(errorCode, message, domainError));
}
return this._updateDealer(dealerOrError.object);
}
private async _updateDealer(dealer: Dealer) {
// Guardar el contacto
const transaction = this._adapter.startTransaction();
const dealerRepository = this._getDealerRepository();
let dealerRepo: IDealerRepository;
try {
await transaction.complete(async (t) => {
dealerRepo = dealerRepository({ transaction: t });
await dealerRepo.update(dealer);
});
return Result.ok<Dealer>(dealer);
} catch (error: unknown) {
const _error = error as IInfrastructureError;
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, _error.message));
}
}
private _tryCreateDealerInstance(
dealerDTO: IUpdateDealer_Request_DTO,
dealerId: UniqueID
): Result<Dealer, IDomainError> {
const nameOrError = Name.create(dealerDTO.name);
if (nameOrError.isFailure) {
return Result.fail(nameOrError.error);
}
const emailOrError = Email.create(dealerDTO.email);
if (emailOrError.isFailure) {
return Result.fail(emailOrError.error);
}
const passwordOrError = Password.createFromPlainTextPassword(dealerDTO.password);
if (passwordOrError.isFailure) {
return Result.fail(passwordOrError.error);
}
return Dealer.create(
{
name: nameOrError.object,
email: emailOrError.object,
password: passwordOrError.object,
},
dealerId
);
}
private _getDealerRepository() {
return this._repositoryManager.getRepository<IDealerRepository>("Dealer");
}
}

View File

@ -0,0 +1,5 @@
export * from "./CreateDealer.useCase";
export * from "./DeleteDealer.useCase";
export * from "./GetDealer.useCase";
export * from "./ListDealers.useCase";
export * from "./UpdateDealer.useCase";

View File

@ -0,0 +1,21 @@
import { AggregateRoot, IDomainError, Name, Result, UniqueID } from "@shared/contexts";
export interface IDealerProps {
name: Name;
}
export interface IDealer {
id: UniqueID;
name: Name;
}
export class Dealer extends AggregateRoot<IDealerProps> implements IDealer {
public static create(props: IDealerProps, id?: UniqueID): Result<Dealer, IDomainError> {
const user = new Dealer(props, id);
return Result.ok<Dealer>(user);
}
get name(): Name {
return this.props.name;
}
}

View File

@ -0,0 +1 @@
export * from "./Dealer";

View File

@ -0,0 +1,2 @@
export * from "./entities";
export * from "./repository";

View File

@ -0,0 +1,14 @@
import { IRepository } from "@/contexts/common/domain";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
import { Dealer } from "../entities";
export interface IDealerRepository extends IRepository<any> {
exists(id: UniqueID): Promise<boolean>;
create(dealer: Dealer): Promise<void>;
update(dealer: Dealer): Promise<void>;
getById(id: UniqueID): Promise<Dealer | null>;
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Dealer>>;
removeById(id: UniqueID): Promise<void>;
}

View File

@ -0,0 +1 @@
export * from "./DealerRepository.interface";

View File

@ -0,0 +1,85 @@
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { IDealerRepository } from "../domain";
import { Dealer } from "../domain/entities";
import { ISalesContext } from "./Sales.context";
import { IDealerMapper, createDealerMapper } from "./mappers";
export type QueryParams = {
pagination: Record<string, any>;
filters: Record<string, any>;
};
export class DealerRepository extends SequelizeRepository<Dealer> implements IDealerRepository {
protected mapper: IDealerMapper;
public constructor(props: {
mapper: IDealerMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Dealer_Model", "id", id.toPrimitive());
}
public async create(user: Dealer): Promise<void> {
const userData = this.mapper.mapToPersistence(user);
await this._save("Dealer_Model", user.id, userData);
}
public async update(user: Dealer): Promise<void> {
const userData = this.mapper.mapToPersistence(user);
// borrando y luego creando
// await this.removeById(user.id, true);
await this._save("Dealer_Model", user.id, userData, {});
}
public async getById(id: UniqueID): Promise<Dealer | null> {
const rawDealer: any = await this._getById("Dealer_Model", id);
if (!rawDealer === true) {
return null;
}
return this.mapper.mapToDomain(rawDealer);
}
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<any>> {
const { rows, count } = await this._findAll(
"Dealer_Model",
queryCriteria
/*{
include: [], // esto es para quitar las asociaciones al hacer la consulta
}*/
);
return this.mapper.mapArrayAndCountToDomain(rows, count);
}
public async removeById(id: UniqueID, force: boolean = false): Promise<void> {
return this._removeById("Dealer_Model", id);
}
}
export const registerDealerRepository = (context: ISalesContext) => {
const adapter = context.adapter;
const repoManager = context.repositoryManager;
repoManager.registerRepository("Dealer", (params = { transaction: null }) => {
const { transaction } = params;
return new DealerRepository({
transaction,
adapter,
mapper: createDealerMapper(context),
});
});
};

View File

@ -0,0 +1,32 @@
import { IRepositoryManager, RepositoryManager } from "@/contexts/common/domain";
import {
ISequelizeAdapter,
createSequelizeAdapter,
} from "@/contexts/common/infrastructure/sequelize";
export interface ISalesContext {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
//services: IApplicationService;
}
export class SalesContext {
private static instance: SalesContext | null = null;
public static getInstance(): ISalesContext {
if (!SalesContext.instance) {
SalesContext.instance = new SalesContext({
adapter: createSequelizeAdapter(),
repositoryManager: RepositoryManager.getInstance(),
});
}
return SalesContext.instance.context;
}
private context: ISalesContext;
private constructor(context: ISalesContext) {
this.context = context;
}
}

View File

@ -0,0 +1,119 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { CreateDealerUseCase } from "@/contexts/sales/application";
import { Dealer } from "@/contexts/sales/domain/entities";
import {
ICreateDealer_Request_DTO,
ICreateDealer_Response_DTO,
ensureCreateDealer_Request_DTOIsValid,
} from "@shared/contexts";
import { ISalesContext } from "../../../Sales.context";
import { ICreateDealerPresenter } from "./presenter";
export class CreateDealerController extends ExpressController {
private useCase: CreateDealerUseCase;
private presenter: ICreateDealerPresenter;
private context: ISalesContext;
constructor(
props: {
useCase: CreateDealerUseCase;
presenter: ICreateDealerPresenter;
},
context: ISalesContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl() {
try {
const dealerDTO: ICreateDealer_Request_DTO = this.req.body;
// Validaciones de DTO
const dealerDTOOrError = ensureCreateDealer_Request_DTOIsValid(dealerDTO);
if (dealerDTOOrError.isFailure) {
const errorMessage = "Dealer data not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
dealerDTOOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Llamar al caso de uso
const result = await this.useCase.execute(dealerDTO);
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
const dealer = <Dealer>result.object;
return this.created<ICreateDealer_Response_DTO>(this.presenter.map(dealer, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.INVALID_INPUT_DATA:
errorMessage = "Dealer data not valid";
infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
error
);
return this.invalidInputError(errorMessage, infraError);
break;
case UseCaseError.RESOURCE_ALREADY_EXITS:
errorMessage = "Dealer already exists";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_ALREADY_REGISTERED,
errorMessage,
error
);
return this.conflictError(errorMessage, infraError);
break;
case UseCaseError.REPOSITORY_ERROR:
errorMessage = "Error saving dealer";
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.conflictError(errorMessage, infraError);
break;
case UseCaseError.UNEXCEPTED_ERROR:
errorMessage = error.message;
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.internalServerError(errorMessage, infraError);
break;
default:
errorMessage = error.message;
return this.clientError(errorMessage);
}
}
}

View File

@ -0,0 +1,23 @@
import { CreateDealerUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerDealerRepository } from "../../../Dealer.repository";
import { ISalesContext } from "../../../Sales.context";
import { CreateDealerController } from "./CreateDealer.controller";
import { CreateDealerPresenter } from "./presenter";
export const createDealerController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerDealerRepository(context);
return new CreateDealerController(
{
useCase: new CreateDealerUseCase(context),
presenter: CreateDealerPresenter,
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,16 @@
import { Dealer } from "@/contexts/sales/domain/entities";
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
import { ICreateDealer_Response_DTO } from "@shared/contexts";
export interface ICreateDealerPresenter {
map: (dealer: Dealer, context: ISalesContext) => ICreateDealer_Response_DTO;
}
export const CreateDealerPresenter: ICreateDealerPresenter = {
map: (dealer: Dealer, context: ISalesContext): ICreateDealer_Response_DTO => {
return {
id: dealer.id.toString(),
name: dealer.name.toString(),
};
},
};

View File

@ -0,0 +1 @@
export * from "./CreateDealer.presenter";

View File

@ -0,0 +1,84 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { DeleteDealerUseCase } from "@/contexts/sales/application";
import { ensureIdIsValid } from "@shared/contexts";
import { ISalesContext } from "../../../Sales.context";
export class DeleteDealerController extends ExpressController {
private useCase: DeleteDealerUseCase;
private context: ISalesContext;
constructor(props: { useCase: DeleteDealerUseCase }, context: ISalesContext) {
super();
const { useCase } = props;
this.useCase = useCase;
this.context = context;
}
async executeImpl(): Promise<any> {
try {
const { dealerId } = this.req.params;
// Validar ID
const dealerIdOrError = ensureIdIsValid(dealerId);
if (dealerIdOrError.isFailure) {
const errorMessage = "Dealer ID is not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
dealerIdOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Llamar al caso de uso
const result = await this.useCase.execute({
id: dealerIdOrError.object,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
return this.noContent();
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "Dealer not found";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
errorMessage,
error
);
return this.notFoundError(errorMessage, infraError);
break;
case UseCaseError.UNEXCEPTED_ERROR:
errorMessage = error.message;
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.internalServerError(errorMessage, infraError);
break;
default:
errorMessage = error.message;
return this.clientError(errorMessage);
}
}
}

View File

@ -0,0 +1,21 @@
import { DeleteDealerUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerDealerRepository } from "../../../Dealer.repository";
import { ISalesContext } from "../../../Sales.context";
import { DeleteDealerController } from "./DeleteDealer.controller";
export const deleteDealerController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerDealerRepository(context);
return new DeleteDealerController(
{
useCase: new DeleteDealerUseCase(context),
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,97 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application/useCases";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { GetDealerUseCase } from "@/contexts/sales/application";
import { Dealer } from "@/contexts/sales/domain";
import { IGetDealerResponse_DTO, ensureIdIsValid } from "@shared/contexts";
import { ISalesContext } from "../../../Sales.context";
import { IGetDealerPresenter } from "./presenter/GetDealer.presenter";
export class GetDealerController extends ExpressController {
private useCase: GetDealerUseCase;
private presenter: IGetDealerPresenter;
private context: ISalesContext;
constructor(
props: {
useCase: GetDealerUseCase;
presenter: IGetDealerPresenter;
},
context: ISalesContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl(): Promise<any> {
const { dealerId } = this.req.params;
// Validar ID
const dealerIdOrError = ensureIdIsValid(dealerId);
if (dealerIdOrError.isFailure) {
const errorMessage = "Dealer ID is not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
dealerIdOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
try {
const result = await this.useCase.execute({
id: dealerIdOrError.object,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
const dealer = <Dealer>result.object;
return this.ok<IGetDealerResponse_DTO>(this.presenter.map(dealer, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "Dealer not found";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
errorMessage,
error
);
return this.notFoundError(errorMessage, infraError);
break;
case UseCaseError.UNEXCEPTED_ERROR:
errorMessage = error.message;
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.internalServerError(errorMessage, infraError);
break;
default:
errorMessage = error.message;
return this.clientError(errorMessage);
}
}
}

View File

@ -0,0 +1,24 @@
import { GetDealerUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerDealerRepository } from "../../../Dealer.repository";
import { ISalesContext } from "../../../Sales.context";
import { GetDealerController } from "./GetDealer.controller";
import { GetDealerPresenter } from "./presenter";
export const getDealerController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerDealerRepository(context);
return new GetDealerController(
{
useCase: new GetDealerUseCase(context),
presenter: GetDealerPresenter,
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,15 @@
import { Dealer } from "../../../../../domain";
import { ISalesContext } from "../../../../Sales.context";
export interface IGetDealerPresenter {
map: (dealer: Dealer, context: ISalesContext) => IGetDealerResponse_DTO;
}
export const GetDealerPresenter: IGetDealerPresenter = {
map: (dealer: Dealer, context: ISalesContext): IGetDealerResponse_DTO => {
return {
id: dealer.id.toString(),
name: dealer.name.toString(),
};
},
};

View File

@ -0,0 +1 @@
export * from "./GetDealer.presenter";

View File

@ -0,0 +1,5 @@
export * from "./createDealer";
export * from "./deleteDealer";
export * from "./getDealer";
export * from "./listDealers";
export * from "./updateDealer";

View File

@ -0,0 +1,84 @@
import Joi from "joi";
import { QueryCriteriaService } from "@/contexts/common/application/services";
import { IServerError } from "@/contexts/common/domain/errors";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { ListDealersResult, ListDealersUseCase } from "@/contexts/sales/application";
import { Dealer } from "@/contexts/sales/domain";
import {
ICollection,
IListDealers_Response_DTO,
IListResponse_DTO,
IQueryCriteria,
Result,
RuleValidator,
} from "@shared/contexts";
import { ISalesContext } from "../../../Sales.context";
import { IListDealersPresenter } from "./presenter";
export class ListDealersController extends ExpressController {
private useCase: ListDealersUseCase;
private presenter: IListDealersPresenter;
private context: ISalesContext;
constructor(
props: {
useCase: ListDealersUseCase;
presenter: IListDealersPresenter;
},
context: ISalesContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
protected validateQuery(query): Result<any> {
const schema = Joi.object({
page: Joi.number().optional(),
limit: Joi.number().optional(),
$sort_by: Joi.string().optional(),
$filters: Joi.string().optional(),
q: Joi.string().optional(),
}).optional();
return RuleValidator.validate(schema, query);
}
async executeImpl() {
const queryOrError = this.validateQuery(this.req.query);
if (queryOrError.isFailure) {
return this.clientError(queryOrError.error.message);
}
const queryParams = queryOrError.object;
try {
const queryCriteria: IQueryCriteria = QueryCriteriaService.parse(queryParams);
console.log(queryCriteria);
const result: ListDealersResult = await this.useCase.execute({
queryCriteria,
});
if (result.isFailure) {
return this.clientError(result.error.message);
}
const dealers = <ICollection<Dealer>>result.object;
return this.ok<IListResponse_DTO<IListDealers_Response_DTO>>(
this.presenter.mapArray(dealers, this.context, {
page: queryCriteria.pagination.offset,
limit: queryCriteria.pagination.limit,
})
);
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
}

View File

@ -0,0 +1,23 @@
import { ListDealersUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerDealerRepository } from "../../../Dealer.repository";
import { ISalesContext } from "../../../Sales.context";
import { ListDealersController } from "./ListDealers.controller";
import { listDealersPresenter } from "./presenter/ListDealers.presenter";
export const listDealersController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerDealerRepository(context);
return new ListDealersController(
{
useCase: new ListDealersUseCase(context),
presenter: listDealersPresenter,
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,50 @@
import { Dealer } from "@/contexts/sales/domain";
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
import { ICollection, IListDealers_Response_DTO, IListResponse_DTO } from "@shared/contexts";
export interface IListDealersPresenter {
map: (dealer: Dealer, context: ISalesContext) => IListDealers_Response_DTO;
mapArray: (
dealers: ICollection<Dealer>,
context: ISalesContext,
params: {
page: number;
limit: number;
}
) => IListResponse_DTO<IListDealers_Response_DTO>;
}
export const listDealersPresenter: IListDealersPresenter = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
map: (dealer: Dealer, context: ISalesContext): IListDealers_Response_DTO => {
return {
id: dealer.id.toString(),
name: dealer.name.toString(),
};
},
mapArray: (
dealers: ICollection<Dealer>,
context: ISalesContext,
params: {
page: number;
limit: number;
}
): IListResponse_DTO<IListDealers_Response_DTO> => {
const { page, limit } = params;
const totalCount = dealers.totalCount ?? 0;
const items = dealers.items.map((dealer: Dealer) => listDealersPresenter.map(dealer, context));
const result = {
page,
per_page: limit,
total_pages: Math.ceil(totalCount / limit),
total_items: totalCount,
items,
};
return result;
},
};

View File

@ -0,0 +1 @@
export * from "./ListDealers.presenter";

View File

@ -0,0 +1,138 @@
import { IUseCaseError, UseCaseError } from "@/contexts/common/application";
import { IServerError } from "@/contexts/common/domain/errors";
import { IInfrastructureError, InfrastructureError } from "@/contexts/common/infrastructure";
import { ExpressController } from "@/contexts/common/infrastructure/express";
import { UpdateDealerResponseOrError, UpdateDealerUseCase } from "@/contexts/sales/application";
import { Dealer } from "@/contexts/sales/domain";
import {
IUpdateDealer_Request_DTO,
IUpdateDealer_Response_DTO,
ensureIdIsValid,
ensureUpdateDealer_Request_DTOIsValid,
} from "@shared/contexts";
import { ISalesContext } from "../../../Sales.context";
import { IUpdateDealerPresenter } from "./presenter/UpdateDealer.presenter";
export class UpdateDealerController extends ExpressController {
private useCase: UpdateDealerUseCase;
private presenter: IUpdateDealerPresenter;
private context: ISalesContext;
constructor(
props: {
useCase: UpdateDealerUseCase;
presenter: IUpdateDealerPresenter;
},
context: ISalesContext
) {
super();
const { useCase, presenter } = props;
this.useCase = useCase;
this.presenter = presenter;
this.context = context;
}
async executeImpl() {
try {
const { dealerId } = this.req.params;
const dealerDTO: IUpdateDealer_Request_DTO = this.req.body;
// Validar ID
const dealerIdOrError = ensureIdIsValid(dealerId);
if (dealerIdOrError.isFailure) {
const errorMessage = "Dealer ID is not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
dealerIdOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Validar DTO de datos
const dealerDTOOrError = ensureUpdateDealer_Request_DTOIsValid(dealerDTO);
if (dealerDTOOrError.isFailure) {
const errorMessage = "Dealer data not valid";
const infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
errorMessage,
dealerDTOOrError.error
);
return this.invalidInputError(errorMessage, infraError);
}
// Llamar al caso de uso
const result: UpdateDealerResponseOrError = await this.useCase.execute({
id: dealerIdOrError.object,
dealerDTO,
});
if (result.isFailure) {
return this._handleExecuteError(result.error);
}
const dealer = <Dealer>result.object;
return this.ok<IUpdateDealer_Response_DTO>(this.presenter.map(dealer, this.context));
} catch (e: unknown) {
return this.fail(e as IServerError);
}
}
private _handleExecuteError(error: IUseCaseError) {
let errorMessage: string;
let infraError: IInfrastructureError;
switch (error.code) {
case UseCaseError.NOT_FOUND_ERROR:
errorMessage = "Dealer not found";
infraError = InfrastructureError.create(
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
errorMessage,
error
);
return this.notFoundError(errorMessage, infraError);
break;
case UseCaseError.INVALID_INPUT_DATA:
errorMessage = "Dealer data not valid";
infraError = InfrastructureError.create(
InfrastructureError.INVALID_INPUT_DATA,
"Datos del cliente a actulizar erróneos",
error
);
return this.invalidInputError(errorMessage, infraError);
break;
case UseCaseError.REPOSITORY_ERROR:
errorMessage = "Error updating dealer";
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.conflictError(errorMessage, infraError);
break;
case UseCaseError.UNEXCEPTED_ERROR:
errorMessage = error.message;
infraError = InfrastructureError.create(
InfrastructureError.UNEXCEPTED_ERROR,
errorMessage,
error
);
return this.internalServerError(errorMessage, infraError);
break;
default:
errorMessage = error.message;
return this.clientError(errorMessage);
}
}
}

View File

@ -0,0 +1,23 @@
import { UpdateDealerUseCase } from "@/contexts/sales/application";
import Express from "express";
import { registerDealerRepository } from "../../../Dealer.repository";
import { ISalesContext } from "../../../Sales.context";
import { UpdateDealerController } from "./UpdateDealer.controller";
import { UpdateDealerPresenter } from "./presenter/UpdateDealer.presenter";
export const updateDealerController = (
req: Express.Request,
res: Express.Response,
next: Express.NextFunction
) => {
const context: ISalesContext = res.locals.context;
registerDealerRepository(context);
return new UpdateDealerController(
{
useCase: new UpdateDealerUseCase(context),
presenter: UpdateDealerPresenter,
},
context
).execute(req, res, next);
};

View File

@ -0,0 +1,16 @@
import { Dealer } from "@/contexts/sales/domain";
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
import { IUpdateDealer_Response_DTO } from "@shared/contexts";
export interface IUpdateDealerPresenter {
map: (dealer: Dealer, context: ISalesContext) => IUpdateDealer_Response_DTO;
}
export const UpdateDealerPresenter: IUpdateDealerPresenter = {
map: (dealer: Dealer, context: ISalesContext): IUpdateDealer_Response_DTO => {
return {
id: dealer.id.toString(),
name: dealer.name.toString(),
};
},
};

View File

@ -0,0 +1 @@
export * from "./UpdateDealer.presenter";

View File

@ -0,0 +1 @@
export * from "./routes";

View File

@ -0,0 +1,21 @@
import { applyMiddleware } from "@/contexts/common/infrastructure/express";
import Express from "express";
import {
createDealerController,
deleteDealerController,
getDealerController,
updateDealerController,
} from "./controllers";
import { listDealersController } from "./controllers/listDealers";
export const DealerRouter = (appRouter: Express.Router) => {
const dealerRoutes: Express.Router = Express.Router({ mergeParams: true });
dealerRoutes.get("/", applyMiddleware("isAdminUser"), listDealersController);
dealerRoutes.get("/:dealerId", applyMiddleware("isAdminUser"), getDealerController);
dealerRoutes.post("/", applyMiddleware("isAdminUser"), createDealerController);
dealerRoutes.put("/:dealerId", applyMiddleware("isAdminUser"), updateDealerController);
dealerRoutes.delete("/:dealerId", applyMiddleware("isAdminUser"), deleteDealerController);
appRouter.use("/dealers", dealerRoutes);
};

View File

@ -0,0 +1 @@
export * from "./Sales.context";

View File

@ -0,0 +1,55 @@
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import { Name, UniqueID } from "@shared/contexts";
import { Dealer, IDealerProps } from "../../domain/entities";
import { ISalesContext } from "../Sales.context";
import { DealerCreationAttributes, DealerStatus, Dealer_Model } from "../sequelize";
export interface IDealerMapper
extends ISequelizeMapper<Dealer_Model, DealerCreationAttributes, Dealer> {}
class DealerMapper
extends SequelizeMapper<Dealer_Model, DealerCreationAttributes, Dealer>
implements IDealerMapper
{
public constructor(props: { context: ISalesContext }) {
super(props);
}
protected toDomainMappingImpl(source: Dealer_Model, params: any): Dealer {
const props: IDealerProps = {
name: this.mapsValue(source, "name", Name.create),
};
const id = this.mapsValue(source, "id", UniqueID.create);
const userOrError = Dealer.create(props, id);
if (userOrError.isFailure) {
throw userOrError.error;
}
return userOrError.object;
}
protected toPersistenceMappingImpl(source: Dealer, params?: MapperParamsType | undefined) {
return {
id: source.id.toPrimitive(),
id_contact: undefined,
name: source.name.toPrimitive(),
contact_information: "",
default_payment_method: "",
default_notes: "",
default_legal_terms: "",
default_quote_validity: "",
status: DealerStatus.ACTIVE,
};
}
}
export const createDealerMapper = (context: ISalesContext): IDealerMapper =>
new DealerMapper({
context,
});

View File

@ -0,0 +1 @@
export * from "./dealer.mapper";

View File

@ -0,0 +1,103 @@
import {
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Op,
Sequelize,
} from "sequelize";
export enum DealerStatus {
ACTIVE = "active",
BLOCKED = "blocked",
}
export type DealerCreationAttributes = InferCreationAttributes<Dealer_Model>;
export class Dealer_Model extends Model<
InferAttributes<Dealer_Model>,
InferCreationAttributes<Dealer_Model>
> {
// To avoid table creation
/*static async sync(): Promise<any> {
return Promise.resolve();
}*/
static associate(connection: Sequelize) {}
declare id: string;
declare id_contact?: string; // number ??
declare name: string;
declare contact_information: string;
declare default_payment_method: string;
declare default_notes: string;
declare default_legal_terms: string;
declare default_quote_validity: string;
declare status: DealerStatus;
}
export default (sequelize: Sequelize) => {
Dealer_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
id_contact: {
type: DataTypes.BIGINT().UNSIGNED,
allowNull: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
contact_information: DataTypes.STRING,
default_payment_method: DataTypes.STRING,
default_notes: DataTypes.STRING,
default_legal_terms: DataTypes.STRING,
default_quote_validity: DataTypes.STRING,
status: {
type: DataTypes.ENUM(...Object.values(DealerStatus)),
allowNull: false,
},
},
{
sequelize,
tableName: "dealers",
paranoid: true, // softs deletes
timestamps: true,
//version: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{ name: "id_contact_idx", fields: ["id_contact"] },
{ name: "status_idx", fields: ["status"] },
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
scopes: {
quickSearch(value) {
return {
where: {
[Op.or]: {
name: {
[Op.like]: `%${value}%`,
},
},
},
};
},
},
}
);
return Dealer_Model;
};

View File

@ -0,0 +1 @@
export * from "./dealer.model";

View File

@ -6,13 +6,13 @@ import {
import { Email, Name, UniqueID } from "@shared/contexts";
import { IUserProps, User } from "../../domain";
import { IUserContext } from "../User.context";
import { TCreationUser_Attributes, User_Model } from "../sequelize/user.model";
import { UserCreationAttributes, User_Model } from "../sequelize/user.model";
export interface IUserMapper
extends ISequelizeMapper<User_Model, TCreationUser_Attributes, User> {}
extends ISequelizeMapper<User_Model, UserCreationAttributes, User> {}
class UserMapper
extends SequelizeMapper<User_Model, TCreationUser_Attributes, User>
extends SequelizeMapper<User_Model, UserCreationAttributes, User>
implements IUserMapper
{
public constructor(props: { context: IUserContext }) {

View File

@ -7,7 +7,7 @@ import {
Sequelize,
} from "sequelize";
export type TCreationUser_Attributes = InferCreationAttributes<User_Model>;
export type UserCreationAttributes = InferCreationAttributes<User_Model>;
export class User_Model extends Model<
InferAttributes<User_Model>,

View File

@ -1,6 +1,7 @@
import { AuthRouter } from "@/contexts/auth";
import { CatalogRouter } from "@/contexts/catalog";
import { createMiddlewareMap } from "@/contexts/common/infrastructure/express";
import { DealerRouter } from "@/contexts/sales/infrastructure/express";
import { UserRouter } from "@/contexts/users";
import Express from "express";
import { createContextMiddleware } from "./context.middleware";
@ -12,28 +13,21 @@ export const v1Routes = () => {
res.send("Hello world!");
});
routes.use(
(
req: Express.Request,
res: Express.Response,
next: Express.NextFunction,
) => {
res.locals["context"] = createContextMiddleware();
res.locals["middlewares"] = createMiddlewareMap();
routes.use((req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
res.locals["context"] = createContextMiddleware();
res.locals["middlewares"] = createMiddlewareMap();
return next();
},
);
return next();
});
routes.use((req, res, next) => {
console.log(
`[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`,
);
console.log(`[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`);
next();
});
AuthRouter(routes);
UserRouter(routes);
CatalogRouter(routes);
DealerRouter(routes);
return routes;
};

View File

@ -2,4 +2,5 @@ export * from "./common";
export * from "./auth";
export * from "./catalog";
export * from "./sales";
export * from "./users";

View File

@ -0,0 +1,22 @@
import Joi from "joi";
import { Result, RuleValidator } from "../../../../common";
export interface ICreateDealer_Request_DTO {
id: string;
name: string;
}
export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) {
const schema = Joi.object({
id: Joi.string(),
name: Joi.string(),
}).unknown(true);
const result = RuleValidator.validate<ICreateDealer_Request_DTO>(schema, dealerDTO);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(true);
}

View File

@ -0,0 +1,4 @@
export interface ICreateDealer_Response_DTO {
id: string;
name: string;
}

View File

@ -0,0 +1,2 @@
export * from "./ICreateDealer_Request.dto";
export * from "./ICreateDealer_Response.dto";

View File

@ -0,0 +1,22 @@
import Joi from "joi";
import { Result, RuleValidator } from "../../../../common";
export interface ICreateDealer_Request_DTO {
id: string;
name: string;
}
export function ensureCreateDealer_Request_DTOIsValid(dealerDTO: ICreateDealer_Request_DTO) {
const schema = Joi.object({
id: Joi.string(),
name: Joi.string(),
}).unknown(true);
const result = RuleValidator.validate<ICreateDealer_Request_DTO>(schema, dealerDTO);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(true);
}

View File

@ -0,0 +1,4 @@
export interface IGetDealerResponse_DTO {
id: string;
name: string;
}

View File

@ -0,0 +1 @@
export * from "./IGetDealer_Response.dto";

View File

@ -0,0 +1,4 @@
export interface IListDealers_Response_DTO {
id: string;
name: string;
}

View File

@ -0,0 +1 @@
export * from "./IListDealers_Response.dto";

View File

@ -0,0 +1,20 @@
import Joi from "joi";
import { Result, RuleValidator } from "../../../../common";
export interface IUpdateDealer_Request_DTO {
name: string;
}
export function ensureUpdateDealer_Request_DTOIsValid(dealerDTO: IUpdateDealer_Request_DTO) {
const schema = Joi.object({
name: Joi.string(),
}).unknown(true);
const result = RuleValidator.validate<IUpdateDealer_Request_DTO>(schema, dealerDTO);
if (result.isFailure) {
return Result.fail(result.error);
}
return Result.ok(true);
}

View File

@ -0,0 +1,4 @@
export interface IUpdateDealer_Response_DTO {
id: string;
name: string;
}

View File

@ -0,0 +1,2 @@
export * from "./IUpdateDealer_Request.dto";
export * from "./IUpdateDealer_Response.dto";

View File

@ -0,0 +1,4 @@
export * from "./CreateDealer.dto";
export * from "./GetDealer.dto";
export * from "./IListDealers.dto";
export * from "./UpdateDealer.dto";

View File

@ -0,0 +1 @@
export * from "./dto";

View File

@ -0,0 +1 @@
export * from "./application";

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"baseUrl": "./lib/*" /* Base directory to resolve non-absolute module names. */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}