.
This commit is contained in:
commit
2194ed1420
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
node_modules/
|
||||
bundle.js
|
||||
npm-debug.log
|
||||
|
||||
/.idea
|
||||
/public
|
||||
|
||||
.env
|
||||
.passport.js
|
||||
.DS_Store
|
||||
|
||||
build/
|
||||
dist/
|
||||
client/.parcel-cache
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
debug*.log*
|
||||
error*.log*
|
||||
.*-audit.json
|
||||
|
||||
|
||||
10
.prettierc.json
Normal file
10
.prettierc.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"useTabs": false,
|
||||
"endOfLine": "auto",
|
||||
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
37
.vscode/launch.json
vendored
Normal file
37
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch firefox localhost",
|
||||
"type": "firefox",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/client"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "launch",
|
||||
"name": "CLIENT: Launch Edge against localhost",
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/client"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "SERVER: Attach to dev:debug",
|
||||
"port": 4321,
|
||||
"restart": true,
|
||||
"cwd": "${workspaceRoot}"
|
||||
},
|
||||
{
|
||||
"name": "Launch via YARN",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["run", "server"],
|
||||
"runtimeExecutable": "yarn",
|
||||
"skipFiles": ["<node_internals>/**", "client/**", "dist/**", "doc/**"],
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
//"typescript.surveys.enabled": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "uecko-presupuestador",
|
||||
"version": "1.0.0",
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"shared"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest --verbose",
|
||||
"client": "cd client; yarn run dev:debug",
|
||||
"server": "cd server; yarn run dev:debug",
|
||||
"start": "concurrently --kill-others-on-fail \"yarn server\" \"yarn client\"",
|
||||
"clean": "concurrently --kill-others-on-fail \"cd server; yarn run clean\" \"cd shared; yarn run clean\" \"cd client; yarn run clean\" \"rm -rf node_modules\""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0",
|
||||
"yarn": ">=1.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "4.1.0",
|
||||
"@types/jest": "^29.5.6",
|
||||
"eslint-plugin-jest": "^27.4.2",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
55
server/.eslintrc.json
Normal file
55
server/.eslintrc.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:jest/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "tsconfig.eslint.json",
|
||||
"tsconfigRootDir": "./server",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "sort-class-members"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/recommended-requiring-type-checking": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"lines-between-class-members": [
|
||||
"error",
|
||||
"always",
|
||||
{ "exceptAfterSingleLine": true }
|
||||
],
|
||||
|
||||
"sort-class-members/sort-class-members": [
|
||||
2,
|
||||
{
|
||||
"order": [
|
||||
"[static-properties]",
|
||||
"[static-methods]",
|
||||
"[conventional-private-properties]",
|
||||
"[properties]",
|
||||
"constructor",
|
||||
"[methods]",
|
||||
"[conventional-private-methods]"
|
||||
],
|
||||
"accessorPairPositioning": "getThenSet"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts"],
|
||||
"env": { "jest": true, "node": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
83
server/package.json
Normal file
83
server/package.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "@uecko-presupuestador/server",
|
||||
"private": false,
|
||||
"version": "1.0.0",
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"start": "node -r ts-node/register/transpile-only -r tsconfig-paths/register ../dist/src/index.js",
|
||||
"dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts",
|
||||
"dev:debug": "ts-node-dev --transpile-only --respawn --inspect=4321 -r tsconfig-paths/register ./src/index.ts",
|
||||
"build": "tsc",
|
||||
"lint": "eslint --ignore-path .gitignore . --ext .ts",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest --verbose",
|
||||
"clean": "rm -rf node_modules"
|
||||
},
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/dinero.js": "^1.9.1",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/module-alias": "^2.0.1",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/node": "^20.4.9",
|
||||
"@types/response-time": "^2.3.5",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/validator": "^13.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.8.0",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-hexagonal-architecture": "^1.0.3",
|
||||
"eslint-plugin-import": "^2.28.0",
|
||||
"eslint-plugin-jest": "^27.4.2",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-sort-class-members": "^1.19.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"prettier": "3.0.1",
|
||||
"supertest": "^6.2.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joi/date": "^2.1.0",
|
||||
"@reis/joi-luxon": "^3.0.0",
|
||||
"cls-rtracer": "^2.6.3",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "5.0.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-openapi-validator": "^5.0.4",
|
||||
"helmet": "^7.0.0",
|
||||
"joi": "^17.12.3",
|
||||
"joi-phone-number": "^5.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.4.0",
|
||||
"moment": "^2.29.4",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.0",
|
||||
"node-firebird": "^1.1.8",
|
||||
"path": "^0.12.7",
|
||||
"remove": "^0.1.5",
|
||||
"response-time": "^2.3.2",
|
||||
"sequelize": "^6.33.0",
|
||||
"sequelize-revision": "^6.0.0",
|
||||
"sequelize-typescript": "^2.1.5",
|
||||
"shallow-equal-object": "^1.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"winston": "^3.10.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
40
server/src/config/environments/development.ts
Normal file
40
server/src/config/environments/development.ts
Normal file
@ -0,0 +1,40 @@
|
||||
module.exports = {
|
||||
database: {
|
||||
username: "rodax",
|
||||
password: "rodax",
|
||||
database: "uecko",
|
||||
host: process.env.HOSTNAME || "localhost",
|
||||
port: 3306,
|
||||
dialect: "mysql",
|
||||
},
|
||||
|
||||
firebird: {
|
||||
host: process.env.HOSTNAME || "192.168.0.133",
|
||||
port: 3050,
|
||||
database: "C:/Codigo/Output/Debug/Database/FACTUGES.FDB",
|
||||
user: "SYSDBA",
|
||||
password: "masterkey",
|
||||
lowercase_keys: false, // set to true to lowercase keys
|
||||
role: null, // default
|
||||
pageSize: 4096, // default when creating database
|
||||
retryConnectionInterval: 1000, // reconnect interval in case of connection drop
|
||||
blobAsText: false, // set to true to get blob as text, only affects blob subtype 1
|
||||
encoding: "UTF-8", // default encoding for connection is UTF-8 },
|
||||
poolCount: 5, // opened sockets
|
||||
},
|
||||
|
||||
server: {
|
||||
hostname: process.env.HOSTNAME || "127.0.0.1",
|
||||
port: process.env.PORT || 4001,
|
||||
public_url: "",
|
||||
},
|
||||
|
||||
uploads: {
|
||||
imports:
|
||||
process.env.UPLOAD_PATH ||
|
||||
"/home/rodax/Documentos/BBDD/server/uploads/imports",
|
||||
documents:
|
||||
process.env.UPLOAD_PATH ||
|
||||
"/home/rodax/Documentos/BBDD/server/uploads/documents",
|
||||
},
|
||||
};
|
||||
21
server/src/config/environments/production.ts
Normal file
21
server/src/config/environments/production.ts
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
database: {
|
||||
username: "uecko",
|
||||
password: "",
|
||||
database: "uecko2",
|
||||
host: process.env.HOSTNAME || "localhost",
|
||||
port: 3306,
|
||||
dialect: "mysql",
|
||||
},
|
||||
|
||||
server: {
|
||||
hostname: process.env.HOSTNAME || "127.0.0.1",
|
||||
port: process.env.PORT || 17777,
|
||||
public_url: "https://...",
|
||||
},
|
||||
|
||||
uploads: {
|
||||
imports: process.env.UPLOAD_PATH || "/opt/bbdd/imports",
|
||||
documents: process.env.UPLOAD_PATH || "/opt/bbdd/documents",
|
||||
},
|
||||
};
|
||||
21
server/src/config/index.ts
Normal file
21
server/src/config/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import path from 'path';
|
||||
|
||||
const enviroment = process.env.NODE_ENV || 'development';
|
||||
const isProduction = enviroment === 'production';
|
||||
const isDevelopment = enviroment === 'development';
|
||||
|
||||
const enviromentConfig = require(path.resolve(
|
||||
__dirname,
|
||||
'environments',
|
||||
enviroment + '.ts'
|
||||
));
|
||||
|
||||
export const config = Object.assign(
|
||||
{
|
||||
enviroment,
|
||||
isProduction,
|
||||
isDevelopment,
|
||||
},
|
||||
enviromentConfig
|
||||
);
|
||||
@ -0,0 +1,79 @@
|
||||
import {
|
||||
IUseCase,
|
||||
IUseCaseError,
|
||||
UseCaseError,
|
||||
handleUseCaseError,
|
||||
} 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 { IFirebirdAdapter } from "@/contexts/common/infrastructure/firebird";
|
||||
import { ICatalogRepository, Product } from "../domain";
|
||||
|
||||
export interface IListProductsParams {
|
||||
queryCriteria: IQueryCriteria;
|
||||
}
|
||||
|
||||
export type ListProductsResult =
|
||||
| Result<never, IUseCaseError> // Misc errors (value objects)
|
||||
| Result<ICollection<Product>, never>; // Success!
|
||||
|
||||
export class ListProductsUseCase
|
||||
implements IUseCase<IListProductsParams, Promise<ListProductsResult>>
|
||||
{
|
||||
private _adapter: IFirebirdAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
|
||||
constructor(props: {
|
||||
adapter: IFirebirdAdapter;
|
||||
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<IListProductsParams>
|
||||
): Promise<ListProductsResult> {
|
||||
const { queryCriteria } = params;
|
||||
|
||||
return this.findProducts(queryCriteria);
|
||||
}
|
||||
|
||||
private async findProducts(queryCriteria) {
|
||||
const transaction = this._adapter.startTransaction();
|
||||
const productRepoBuilder =
|
||||
this.getRepositoryByName<ICatalogRepository>("Product");
|
||||
|
||||
let products: ICollection<Product> = new Collection();
|
||||
|
||||
try {
|
||||
await transaction.complete(async (t) => {
|
||||
products = await productRepoBuilder({ transaction: t }).findAll(
|
||||
queryCriteria
|
||||
);
|
||||
});
|
||||
|
||||
return Result.ok(products);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
handleUseCaseError(
|
||||
UseCaseError.REPOSITORY_ERROR,
|
||||
"Error al listar el catálogo",
|
||||
_error
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
server/src/contexts/catalog/application/index.ts
Normal file
1
server/src/contexts/catalog/application/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./ListProductsUseCase";
|
||||
75
server/src/contexts/catalog/domain/entities/Product.ts
Normal file
75
server/src/contexts/catalog/domain/entities/Product.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import {
|
||||
AggregateRoot,
|
||||
Description,
|
||||
IDomainError,
|
||||
MoneyValueObject,
|
||||
Result,
|
||||
StringValueObject,
|
||||
UniqueID,
|
||||
ValueObject,
|
||||
} from "@shared/contexts";
|
||||
|
||||
export interface IProductProps {
|
||||
reference: StringValueObject;
|
||||
family: StringValueObject;
|
||||
subfamily: StringValueObject;
|
||||
description: StringValueObject;
|
||||
points: ValueObject<number>;
|
||||
pvp: MoneyValueObject;
|
||||
}
|
||||
|
||||
export interface IProduct {
|
||||
id: UniqueID;
|
||||
reference: Description;
|
||||
family: Description;
|
||||
subfamily: Description;
|
||||
description: Description;
|
||||
points: ValueObject<number>;
|
||||
pvp: MoneyValueObject;
|
||||
}
|
||||
|
||||
export class Product extends AggregateRoot<IProductProps> implements IProduct {
|
||||
public static create(
|
||||
props: IProductProps,
|
||||
id?: UniqueID
|
||||
): Result<Product, IDomainError> {
|
||||
//const isNew = !!id === false;
|
||||
|
||||
// Se hace en el constructor de la Entidad
|
||||
/* if (isNew) {
|
||||
id = UniqueEntityID.create();
|
||||
}*/
|
||||
|
||||
const product = new Product(props, id);
|
||||
|
||||
return Result.ok<Product>(product);
|
||||
}
|
||||
|
||||
private constructor(props: IProductProps, id?: UniqueID) {
|
||||
super(props, id);
|
||||
}
|
||||
|
||||
get reference(): Description {
|
||||
return this.props.reference;
|
||||
}
|
||||
|
||||
get family(): Description {
|
||||
return this.props.family;
|
||||
}
|
||||
|
||||
get subfamily(): Description {
|
||||
return this.props.subfamily;
|
||||
}
|
||||
|
||||
get description(): Description {
|
||||
return this.props.description;
|
||||
}
|
||||
|
||||
get points(): ValueObject<number> {
|
||||
return this.props.points;
|
||||
}
|
||||
|
||||
get pvp(): MoneyValueObject {
|
||||
return this.props.pvp;
|
||||
}
|
||||
}
|
||||
1
server/src/contexts/catalog/domain/entities/index.ts
Normal file
1
server/src/contexts/catalog/domain/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Product";
|
||||
2
server/src/contexts/catalog/domain/index.ts
Normal file
2
server/src/contexts/catalog/domain/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./entities";
|
||||
export * from "./repository";
|
||||
@ -0,0 +1,7 @@
|
||||
import { IRepository } from "@/contexts/common/domain";
|
||||
import { ICollection, IQueryCriteria } from "@shared/contexts";
|
||||
import { Product } from "../entities";
|
||||
|
||||
export interface ICatalogRepository extends IRepository<any> {
|
||||
findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Product>>;
|
||||
}
|
||||
1
server/src/contexts/catalog/domain/repository/index.ts
Normal file
1
server/src/contexts/catalog/domain/repository/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./CatalogRepository.interface";
|
||||
@ -0,0 +1,59 @@
|
||||
import {
|
||||
FirebirdRepository,
|
||||
IFirebirdAdapter,
|
||||
} from "@/contexts/common/infrastructure/firebird";
|
||||
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||
import Firebird from "node-firebird";
|
||||
import { Product } from "../domain/entities";
|
||||
import { ICatalogRepository } from "../domain/repository/CatalogRepository.interface";
|
||||
import { Product_Model } from "./firebird";
|
||||
import { IProductMapper } from "./mappers/product.mapper";
|
||||
|
||||
export type QueryParams = {
|
||||
pagination: Record<string, any>;
|
||||
filters: Record<string, any>;
|
||||
};
|
||||
|
||||
export class CatalogRepository
|
||||
extends FirebirdRepository<Product>
|
||||
implements ICatalogRepository
|
||||
{
|
||||
protected mapper: IProductMapper;
|
||||
|
||||
public constructor(props: {
|
||||
mapper: IProductMapper;
|
||||
adapter: IFirebirdAdapter;
|
||||
transaction: Firebird.Transaction;
|
||||
}) {
|
||||
const { adapter, mapper, transaction } = props;
|
||||
super({ adapter, transaction });
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public async getById(id: UniqueID): Promise<Product | null> {
|
||||
const rawProduct: Product_Model = await this.adapter.execute(
|
||||
"SELECT * FROM TABLE WHERE ID=?",
|
||||
[id.toString()]
|
||||
);
|
||||
|
||||
return this.mapper.mapToDomain(rawProduct);
|
||||
}
|
||||
|
||||
public async findAll(
|
||||
queryCriteria?: IQueryCriteria
|
||||
): Promise<ICollection<Product>> {
|
||||
let rows: Product_Model[] = [];
|
||||
const count = await this.adapter.execute<number>(
|
||||
"SELECT count(*) FROM TABLE",
|
||||
[]
|
||||
);
|
||||
if (count) {
|
||||
rows = await this.adapter.execute<Product_Model[]>(
|
||||
"SELECT * FROM TABLE",
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
return this.mapper.mapArrayAndCountToDomain(rows, count);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import express, { NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
import { RepositoryManager } from "@/contexts/common/domain";
|
||||
import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { createListProductsController } from "./controllers/listProducts";
|
||||
|
||||
const catalogRouter: Router = express.Router({ mergeParams: true });
|
||||
|
||||
const logMiddleware = (req, res, next) => {
|
||||
console.log(
|
||||
`[${new Date().toLocaleTimeString()}] Incoming request to ${req.path}`
|
||||
);
|
||||
next();
|
||||
};
|
||||
|
||||
catalogRouter.use(logMiddleware);
|
||||
|
||||
const contextMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||
res.locals["context"] = {
|
||||
adapter: createSequelizeAdapter(),
|
||||
repositoryManager: RepositoryManager.getInstance(),
|
||||
services: {},
|
||||
};
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
catalogRouter.use(contextMiddleware);
|
||||
|
||||
catalogRouter.get("/", (req: Request, res: Response, next: NextFunction) =>
|
||||
createListProductsController(res.locals["context"]).execute(req, res, next)
|
||||
);
|
||||
|
||||
/*catalogRouter.get(
|
||||
"/:articleId",
|
||||
(req: Request, res: Response, next: NextFunction) =>
|
||||
createGetCustomerController(res.locals["context"]).execute(req, res, next)
|
||||
);*/
|
||||
|
||||
export { catalogRouter };
|
||||
@ -0,0 +1,88 @@
|
||||
import Joi from "joi";
|
||||
|
||||
import {
|
||||
ListProductsResult,
|
||||
ListProductsUseCase,
|
||||
} from "@/contexts/catalog/application";
|
||||
import { Product } from "@/contexts/catalog/domain";
|
||||
import { QueryCriteriaService } from "@/contexts/common/application/services";
|
||||
import { IServerError } from "@/contexts/common/domain/errors";
|
||||
import { ExpressController } from "@/contexts/common/infrastructure/express";
|
||||
import {
|
||||
ICollection,
|
||||
IListProducts_Response_DTO,
|
||||
IListResponse_DTO,
|
||||
IQueryCriteria,
|
||||
Result,
|
||||
RuleValidator,
|
||||
} from "@shared/contexts";
|
||||
import { ICatalogContext } from "../../..";
|
||||
import { IListProductsPresenter } from "./presenter";
|
||||
|
||||
export class ListProductsController extends ExpressController {
|
||||
private useCase: ListProductsUseCase;
|
||||
private presenter: IListProductsPresenter;
|
||||
private context: ICatalogContext;
|
||||
|
||||
constructor(
|
||||
props: {
|
||||
useCase: ListProductsUseCase;
|
||||
presenter: IListProductsPresenter;
|
||||
},
|
||||
context: ICatalogContext
|
||||
) {
|
||||
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: ListProductsResult = await this.useCase.execute({
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return this.clientError(result.error.message);
|
||||
}
|
||||
|
||||
const customers = <ICollection<Product>>result.object;
|
||||
|
||||
return this.ok<IListResponse_DTO<IListProducts_Response_DTO>>(
|
||||
this.presenter.mapArray(customers, this.context, {
|
||||
page: queryCriteria.pagination.offset,
|
||||
limit: queryCriteria.pagination.limit,
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
return this.fail(e as IServerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { ListProductsUseCase } from "@/contexts/catalog/application";
|
||||
import { ICatalogContext } from "../../..";
|
||||
import { CatalogRepository } from "../../../Catalog.repository";
|
||||
import { createProductMapper } from "../../../mappers/product.mapper";
|
||||
import { ListProductsController } from "./ListProductsController";
|
||||
import { listProductsPresenter } from "./presenter";
|
||||
|
||||
export const createListProductsController = (context: ICatalogContext) => {
|
||||
const adapter = context.adapter;
|
||||
const repoManager = context.repositoryManager;
|
||||
|
||||
repoManager.registerRepository(
|
||||
"Product",
|
||||
(params = { transaction: null }) => {
|
||||
const { transaction } = params;
|
||||
|
||||
return new CatalogRepository({
|
||||
transaction,
|
||||
adapter,
|
||||
mapper: createProductMapper(context),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const listProductsUseCase = new ListProductsUseCase(context);
|
||||
|
||||
return new ListProductsController(
|
||||
{
|
||||
useCase: listProductsUseCase,
|
||||
presenter: listProductsPresenter,
|
||||
},
|
||||
context
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,71 @@
|
||||
import { Product } from "@/contexts/catalog/domain";
|
||||
import { ICatalogContext } from "@/contexts/catalog/infrastructure";
|
||||
import {
|
||||
ICollection,
|
||||
IListProducts_Response_DTO,
|
||||
IListResponse_DTO,
|
||||
} from "@shared/contexts";
|
||||
|
||||
export interface IListProductsPresenter {
|
||||
map: (
|
||||
product: Product,
|
||||
context: ICatalogContext
|
||||
) => IListProducts_Response_DTO;
|
||||
|
||||
mapArray: (
|
||||
products: ICollection<Product>,
|
||||
context: ICatalogContext,
|
||||
params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
) => IListResponse_DTO<IListProducts_Response_DTO>;
|
||||
}
|
||||
|
||||
export const listProductsPresenter: IListProductsPresenter = {
|
||||
map: (
|
||||
product: Product,
|
||||
context: ICatalogContext
|
||||
): IListProducts_Response_DTO => {
|
||||
console.time("listProductsPresenter.map");
|
||||
|
||||
const result: IListProducts_Response_DTO = {
|
||||
id: product.id.toString(),
|
||||
reference: product.reference.toString(),
|
||||
};
|
||||
|
||||
console.timeEnd("listProductsPresenter.map");
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
mapArray: (
|
||||
products: ICollection<Product>,
|
||||
context: ICatalogContext,
|
||||
params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
): IListResponse_DTO<IListProducts_Response_DTO> => {
|
||||
console.time("listProductsPresenter.mapArray");
|
||||
|
||||
const { page, limit } = params;
|
||||
|
||||
const totalCount = products.totalCount ?? 0;
|
||||
const items = products.items.map((product: Product) =>
|
||||
listProductsPresenter.map(product, context)
|
||||
);
|
||||
|
||||
const result = {
|
||||
page,
|
||||
per_page: limit,
|
||||
total_pages: Math.ceil(totalCount / limit),
|
||||
total_items: totalCount,
|
||||
items,
|
||||
};
|
||||
|
||||
console.timeEnd("listProductsPresenter.mapArray");
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./ListProducts.presenter";
|
||||
@ -0,0 +1 @@
|
||||
export type FirebirdModel = Record<string, any>;
|
||||
@ -0,0 +1 @@
|
||||
export * from "./product.model";
|
||||
@ -0,0 +1,11 @@
|
||||
import { FirebirdModel } from "./firebird.model";
|
||||
|
||||
export type Product_Model = FirebirdModel & {
|
||||
id: string;
|
||||
reference: string;
|
||||
family: string;
|
||||
subfamiliy: string;
|
||||
description: string;
|
||||
points: number;
|
||||
pvp: number;
|
||||
};
|
||||
9
server/src/contexts/catalog/infrastructure/index.ts
Normal file
9
server/src/contexts/catalog/infrastructure/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { IApplicationService } from "@/contexts/common/application/services/ApplicationService";
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { IFirebirdAdapter } from "@/contexts/common/infrastructure/firebird";
|
||||
|
||||
export interface ICatalogContext {
|
||||
adapter: IFirebirdAdapter;
|
||||
repositoryManager: IRepositoryManager;
|
||||
services: IApplicationService;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import {
|
||||
FirebirdMapper,
|
||||
IFirebirdMapper,
|
||||
} from "@/contexts/common/infrastructure/mappers/FirebirdMapper";
|
||||
import { Description, MoneyValue, UniqueID } from "@shared/contexts";
|
||||
import { ICatalogContext } from "..";
|
||||
import { IProductProps, Product } from "../../domain/entities";
|
||||
import { Product_Model } from "../firebird";
|
||||
|
||||
export interface IProductMapper
|
||||
extends IFirebirdMapper<Product_Model, Product> {}
|
||||
|
||||
class ProductMapper
|
||||
extends FirebirdMapper<Product_Model, Product>
|
||||
implements IProductMapper
|
||||
{
|
||||
public constructor(props: { context: ICatalogContext }) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected toDomainMappingImpl(source: Product_Model, params: any): Product {
|
||||
const props: IProductProps = {
|
||||
reference: this.mapsValue(source, "reference", Description.create),
|
||||
family: this.mapsValue(source, "family", Description.create),
|
||||
subfamily: this.mapsValue(source, "subfamily", Description.create),
|
||||
description: this.mapsValue(source, "description", Description.create),
|
||||
points: this.mapsValue(source, "points", Description.create),
|
||||
pvp: this.mapsValue(source, "pvp", (value: any) =>
|
||||
MoneyValue.create({ amount: value })
|
||||
),
|
||||
};
|
||||
|
||||
const id = this.mapsValue(source, "id", UniqueID.create);
|
||||
const productOrError = Product.create(props, id);
|
||||
|
||||
if (productOrError.isFailure) {
|
||||
throw productOrError.error;
|
||||
}
|
||||
|
||||
return productOrError.object;
|
||||
}
|
||||
}
|
||||
|
||||
export const createProductMapper = (context: ICatalogContext): IProductMapper =>
|
||||
new ProductMapper({
|
||||
context,
|
||||
});
|
||||
3
server/src/contexts/common/application/index.ts
Normal file
3
server/src/contexts/common/application/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./services";
|
||||
export * from "./useCases";
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface IApplicationService {}
|
||||
|
||||
export abstract class ApplicationService implements IApplicationService {}
|
||||
@ -0,0 +1,23 @@
|
||||
import { IServerError, ServerError } from "../../domain/errors";
|
||||
|
||||
export interface IApplicationServiceError extends IServerError {}
|
||||
|
||||
export class ApplicationServiceError
|
||||
extends ServerError
|
||||
implements IApplicationServiceError
|
||||
{
|
||||
public static readonly INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM";
|
||||
public static readonly INVALID_INPUT_DATA = "INVALID_INPUT_DATA";
|
||||
public static readonly UNEXCEPTED_ERROR = "UNEXCEPTED_ERROR";
|
||||
public static readonly REPOSITORY_ERROR = "REPOSITORY_ERROR";
|
||||
public static readonly NOT_FOUND_ERROR = "NOT_FOUND_ERROR";
|
||||
public static readonly RESOURCE_ALREADY_EXITS = "RESOURCE_ALREADY_EXITS";
|
||||
|
||||
public static create(
|
||||
code: string,
|
||||
message: string,
|
||||
details?: Record<string, any>
|
||||
): ApplicationServiceError {
|
||||
return new ApplicationServiceError(code, message, details);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
import {
|
||||
FilterCriteria,
|
||||
IQueryCriteria,
|
||||
OffsetPaging,
|
||||
OrderCriteria,
|
||||
QueryCriteria,
|
||||
QuickSearchCriteria,
|
||||
} from "@shared/contexts";
|
||||
import { ApplicationService } from "./ApplicationService";
|
||||
|
||||
export interface IQueryCriteriaServiceProps {
|
||||
page: string;
|
||||
limit: string;
|
||||
|
||||
sort_by: string;
|
||||
fields: string;
|
||||
filters: string;
|
||||
quick_search: string;
|
||||
}
|
||||
|
||||
export class QueryCriteriaService extends ApplicationService {
|
||||
public static parse(
|
||||
params: Partial<IQueryCriteriaServiceProps>
|
||||
): IQueryCriteria {
|
||||
const {
|
||||
page = undefined,
|
||||
limit = undefined,
|
||||
sort_by = undefined,
|
||||
quick_search = undefined,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
fields = null, // fields / select
|
||||
//scopes:
|
||||
} = params || {};
|
||||
|
||||
const filters = params["$filters"];
|
||||
|
||||
// Pagination
|
||||
const _pagination = QueryCriteriaService.parsePagination(page, limit);
|
||||
|
||||
const _filters: FilterCriteria = QueryCriteriaService.parseFilter(filters);
|
||||
|
||||
const _order: OrderCriteria = QueryCriteriaService.parseOrder(sort_by);
|
||||
|
||||
const _quickSearch: QuickSearchCriteria =
|
||||
QueryCriteriaService.parseQuickSearch(quick_search);
|
||||
|
||||
return QueryCriteria.create({
|
||||
pagination: _pagination,
|
||||
quickSearch: _quickSearch,
|
||||
filters: _filters,
|
||||
order: _order,
|
||||
}).object;
|
||||
}
|
||||
|
||||
protected static parsePagination(page?: string, limit?: string) {
|
||||
if (!page && !limit) {
|
||||
return OffsetPaging.createWithDefaultValues().object;
|
||||
}
|
||||
|
||||
const paginationOrError = OffsetPaging.create({
|
||||
offset: String(page),
|
||||
limit: String(limit),
|
||||
});
|
||||
if (paginationOrError.isFailure) {
|
||||
throw paginationOrError.error;
|
||||
}
|
||||
|
||||
return paginationOrError.object;
|
||||
}
|
||||
|
||||
protected static parseFilter(filter?: string): FilterCriteria {
|
||||
const filterOrError = FilterCriteria.create(filter);
|
||||
if (filterOrError.isFailure) {
|
||||
throw filterOrError.error;
|
||||
}
|
||||
|
||||
return filterOrError.object;
|
||||
}
|
||||
|
||||
protected static parseOrder(sort_by?: string): OrderCriteria {
|
||||
const orderOrError = OrderCriteria.create(sort_by);
|
||||
if (orderOrError.isFailure) {
|
||||
throw orderOrError.error;
|
||||
}
|
||||
|
||||
return orderOrError.object;
|
||||
}
|
||||
|
||||
protected static parseQuickSearch(quickSearch?: string): QuickSearchCriteria {
|
||||
const quickSearchOrError = QuickSearchCriteria.create(quickSearch);
|
||||
if (quickSearchOrError.isFailure) {
|
||||
throw quickSearchOrError.error;
|
||||
}
|
||||
|
||||
return quickSearchOrError.object;
|
||||
}
|
||||
}
|
||||
1
server/src/contexts/common/application/services/index.ts
Normal file
1
server/src/contexts/common/application/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './QueryCriteriaService';
|
||||
@ -0,0 +1,6 @@
|
||||
export interface IUseCaseRequest {}
|
||||
export interface IUseCaseResponse {}
|
||||
|
||||
export interface IUseCase<IUseCaseRequest, IUseCaseResponse> {
|
||||
execute(useCaseRequest: IUseCaseRequest): IUseCaseResponse;
|
||||
}
|
||||
81
server/src/contexts/common/application/useCases/UseCaseError.ts
Executable file
81
server/src/contexts/common/application/useCases/UseCaseError.ts
Executable file
@ -0,0 +1,81 @@
|
||||
import { IServerError, ServerError } from "../../domain/errors";
|
||||
|
||||
export interface IUseCaseError extends IServerError {}
|
||||
|
||||
export class UseCaseError extends ServerError implements IUseCaseError {
|
||||
public static readonly INVALID_REQUEST_PARAM = "INVALID_REQUEST_PARAM";
|
||||
public static readonly INVALID_INPUT_DATA = "INVALID_INPUT_DATA";
|
||||
public static readonly UNEXCEPTED_ERROR = "UNEXCEPTED_ERROR";
|
||||
public static readonly REPOSITORY_ERROR = "REPOSITORY_ERROR";
|
||||
public static readonly NOT_FOUND_ERROR = "NOT_FOUND_ERROR";
|
||||
public static readonly RESOURCE_ALREADY_EXITS = "RESOURCE_ALREADY_EXITS";
|
||||
|
||||
public static create(
|
||||
code: string,
|
||||
message: string,
|
||||
details?: Record<string, any>
|
||||
): UseCaseError {
|
||||
return new UseCaseError(code, message, details);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleUseCaseError(
|
||||
code: string,
|
||||
message: string,
|
||||
payload?: Record<string, any>
|
||||
): IUseCaseError {
|
||||
return UseCaseError.create(code, message, payload);
|
||||
}
|
||||
|
||||
/*export function handleNotFoundError(
|
||||
message: string,
|
||||
validationError: Error,
|
||||
details?: Record<string, any>
|
||||
): Result<never, IUseCaseError> {
|
||||
return handleUseCaseError(
|
||||
UseCaseError.NOT_FOUND_ERROR,
|
||||
message,
|
||||
validationError,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
export function handleInvalidInputDataError(
|
||||
message: string,
|
||||
validationError: Error,
|
||||
details?: Record<string, any>
|
||||
): Result<never, IUseCaseError> {
|
||||
return handleUseCaseError(
|
||||
UseCaseError.INVALID_INPUT_DATA,
|
||||
message,
|
||||
validationError,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
export function handleResourceAlreadyExitsError(
|
||||
message: string,
|
||||
validationError: Error,
|
||||
details?: Record<string, any>
|
||||
): Result<never, IUseCaseError> {
|
||||
return handleUseCaseError(
|
||||
UseCaseError.RESOURCE_ALREADY_EXITS,
|
||||
message,
|
||||
validationError,
|
||||
details
|
||||
);
|
||||
}
|
||||
|
||||
export function handleRepositoryError(
|
||||
message: string,
|
||||
repositoryError: Error,
|
||||
details?: Record<string, any>
|
||||
): Result<never, IUseCaseError> {
|
||||
return handleUseCaseError(
|
||||
UseCaseError.REPOSITORY_ERROR,
|
||||
message,
|
||||
repositoryError,
|
||||
details
|
||||
);
|
||||
}
|
||||
*/
|
||||
2
server/src/contexts/common/application/useCases/index.ts
Normal file
2
server/src/contexts/common/application/useCases/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./UseCase.interface";
|
||||
export * from "./UseCaseError";
|
||||
24
server/src/contexts/common/domain/Mapper.interface.ts
Normal file
24
server/src/contexts/common/domain/Mapper.interface.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ICollection, IListResponse_DTO } from "@shared/contexts";
|
||||
|
||||
interface IGenericMapper<S, D, M, N> {
|
||||
map?: (source: S) => D;
|
||||
mapArray?: (sourceArray: M) => N;
|
||||
}
|
||||
|
||||
export interface IMapper<S, D>
|
||||
extends IGenericMapper<S, D, ICollection<S>, ICollection<D>> {}
|
||||
|
||||
export interface IDTOMapper<S, D> {
|
||||
map: (source: S) => D;
|
||||
mapArray: (
|
||||
sourceArray: ICollection<S>,
|
||||
params: {
|
||||
page: number;
|
||||
limit: number;
|
||||
},
|
||||
) => IListResponse_DTO<D>;
|
||||
}
|
||||
|
||||
export interface IDomainMapper<S, D> {
|
||||
map: (source: S) => D;
|
||||
}
|
||||
163
server/src/contexts/common/domain/Specification.ts
Normal file
163
server/src/contexts/common/domain/Specification.ts
Normal file
@ -0,0 +1,163 @@
|
||||
interface IBaseSpecification<T> {
|
||||
isSatisfiedBy(candidate: T): boolean;
|
||||
}
|
||||
|
||||
export interface ICompositeSpecification<T> extends IBaseSpecification<T> {
|
||||
and(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
|
||||
andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
|
||||
or(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
|
||||
orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T>;
|
||||
not(): ICompositeSpecification<T>;
|
||||
}
|
||||
|
||||
export abstract class CompositeSpecification<T>
|
||||
implements ICompositeSpecification<T>
|
||||
{
|
||||
abstract isSatisfiedBy(candidate: T): boolean;
|
||||
|
||||
public and(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
|
||||
return new AndSpecification<T>(this, other);
|
||||
}
|
||||
|
||||
public andNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
|
||||
return new AndNotSpecification<T>(this, other);
|
||||
}
|
||||
|
||||
public or(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
|
||||
return new OrSpecification<T>(this, other);
|
||||
}
|
||||
|
||||
public orNot(other: ICompositeSpecification<T>): ICompositeSpecification<T> {
|
||||
return new OrNotSpecification<T>(this, other);
|
||||
}
|
||||
|
||||
public not(): ICompositeSpecification<T> {
|
||||
return new NotSpecification<T>(this);
|
||||
}
|
||||
}
|
||||
|
||||
class AndSpecification<T> extends CompositeSpecification<T> {
|
||||
public left: ICompositeSpecification<T>;
|
||||
public right: ICompositeSpecification<T>;
|
||||
|
||||
constructor(
|
||||
left: ICompositeSpecification<T>,
|
||||
right: ICompositeSpecification<T>,
|
||||
) {
|
||||
super();
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
public isSatisfiedBy(candidate: T): boolean {
|
||||
return (
|
||||
this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `(${this.left.toString()} and ${this.right.toString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
class AndNotSpecification<T> extends AndSpecification<T> {
|
||||
isSatisfiedBy(candidate: T): boolean {
|
||||
return super.isSatisfiedBy(candidate) !== true;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `not ${super.toString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
class OrSpecification<T> extends CompositeSpecification<T> {
|
||||
public left: ICompositeSpecification<T>;
|
||||
public right: ICompositeSpecification<T>;
|
||||
|
||||
constructor(
|
||||
left: ICompositeSpecification<T>,
|
||||
right: ICompositeSpecification<T>,
|
||||
) {
|
||||
super();
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
public isSatisfiedBy(candidate: T): boolean {
|
||||
return (
|
||||
this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `(${this.left.toString()} or ${this.right.toString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
class OrNotSpecification<T> extends OrSpecification<T> {
|
||||
isSatisfiedBy(candidate: T): boolean {
|
||||
return super.isSatisfiedBy(candidate) !== true;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `not ${super.toString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotSpecification<T> extends CompositeSpecification<T> {
|
||||
public spec: ICompositeSpecification<T>;
|
||||
|
||||
constructor(spec: ICompositeSpecification<T>) {
|
||||
super();
|
||||
this.spec = spec;
|
||||
}
|
||||
|
||||
public isSatisfiedBy(candidate: T): boolean {
|
||||
return !this.spec.isSatisfiedBy(candidate);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `(not ${this.spec.toString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class RangeSpecification<T> extends CompositeSpecification<T> {
|
||||
private a: T;
|
||||
private b: T;
|
||||
|
||||
constructor(a: T, b: T) {
|
||||
super();
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
|
||||
isSatisfiedBy(candidate: T): boolean {
|
||||
return candidate >= this.a && candidate <= this.b;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `range (${this.a}, ${this.b})`;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export class OrSpecification2<T> {
|
||||
private first: ISpecification<T>;
|
||||
private second: ISpecification<T>;
|
||||
|
||||
public OrSpecification(first: ISpecification<T>, second: ISpecification<T>) {
|
||||
this.first = first;
|
||||
this.second = second;
|
||||
}
|
||||
|
||||
public IsSatisfiedBy(entity: T): IResult<boolean> {
|
||||
const result: ResultCollection = new ResultCollection()
|
||||
.add(this.first.IsSatisfiedBy(entity))
|
||||
.add(this.second.IsSatisfiedBy(entity));
|
||||
|
||||
return result.hasSomeFaultyResult()
|
||||
? result.getFirstFaultyResult()
|
||||
: Result.ok<boolean>(true);
|
||||
}
|
||||
}
|
||||
*/
|
||||
68
server/src/contexts/common/domain/errors/ServerError.ts
Normal file
68
server/src/contexts/common/domain/errors/ServerError.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { GenericError, IGenericError } from "@shared/contexts";
|
||||
import { BaseError } from "sequelize";
|
||||
|
||||
export interface IServerError extends IGenericError {}
|
||||
|
||||
export class ServerError extends GenericError implements IServerError {}
|
||||
|
||||
export class InternalServerError extends ServerError {
|
||||
public static create(code: string, message: string) {
|
||||
return new InternalServerError(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
export class RepositoryError extends ServerError {
|
||||
public static isRepositoryError = (error: unknown) => {
|
||||
return error instanceof BaseError;
|
||||
};
|
||||
|
||||
public static create(error: unknown): ServerError {
|
||||
const _error = error as BaseError;
|
||||
|
||||
return new GenericError("", _error.message, {
|
||||
name: _error.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends ServerError {
|
||||
public static create(id: string, resource: string): NotFoundError {
|
||||
return new NotFoundError(`${resource} not found`, id, resource);
|
||||
}
|
||||
|
||||
public readonly id: string;
|
||||
public readonly resource: string;
|
||||
|
||||
constructor(message: string, id: string, resource: string) {
|
||||
super(message, `${resource} with id '${id}' not found`);
|
||||
this.id = id;
|
||||
this.resource = resource;
|
||||
}
|
||||
}
|
||||
|
||||
export class RequiredFieldMissingError extends ServerError {
|
||||
public static field(
|
||||
fieldName: string,
|
||||
message?: string
|
||||
): RequiredFieldMissingError {
|
||||
return new RequiredFieldMissingError(
|
||||
"",
|
||||
`Required field '${fieldName}' is missing`,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldValueError extends ServerError {
|
||||
public static field(
|
||||
fieldName: string,
|
||||
value: any,
|
||||
message?: string
|
||||
): FieldValueError {
|
||||
return new RequiredFieldMissingError(
|
||||
"",
|
||||
`Incorrect value '${String(value)}' for field '${fieldName}'`,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
1
server/src/contexts/common/domain/errors/index.ts
Normal file
1
server/src/contexts/common/domain/errors/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./ServerError";
|
||||
3
server/src/contexts/common/domain/index.ts
Normal file
3
server/src/contexts/common/domain/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./Mapper.interface";
|
||||
export * from "./Specification";
|
||||
export * from "./repositories";
|
||||
@ -0,0 +1,5 @@
|
||||
import { TBusinessTransaction } from "./BusinessTransaction.interface";
|
||||
|
||||
export interface IAdapter {
|
||||
startTransaction: () => TBusinessTransaction;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
type TUnitOfWork = {
|
||||
start(): unknown;
|
||||
};
|
||||
|
||||
export type TBusinessTransaction = TUnitOfWork;
|
||||
@ -0,0 +1,2 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
export interface IRepository<T> {}
|
||||
@ -0,0 +1 @@
|
||||
export type RepositoryBuilder<T> = (params?: any) => T;
|
||||
@ -0,0 +1,49 @@
|
||||
import { InfrastructureError } from "../../infrastructure/InfrastructureError";
|
||||
import { RepositoryBuilder } from "./RepositoryBuilder";
|
||||
|
||||
export interface IRepositoryManager {
|
||||
getRepository: <T>(name: string) => RepositoryBuilder<T>;
|
||||
registerRepository: <T>(
|
||||
name: string,
|
||||
repository: RepositoryBuilder<T>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export class RepositoryManager implements IRepositoryManager {
|
||||
private static instance: RepositoryManager | null = null;
|
||||
public static getInstance(): RepositoryManager {
|
||||
if (!RepositoryManager.instance) {
|
||||
RepositoryManager.instance = new RepositoryManager();
|
||||
}
|
||||
|
||||
return RepositoryManager.instance;
|
||||
}
|
||||
|
||||
private repositories: Map<string, RepositoryBuilder<any>>;
|
||||
|
||||
private constructor() {
|
||||
this.repositories = new Map();
|
||||
}
|
||||
|
||||
public registerRepository<T>(
|
||||
name: string,
|
||||
repository: RepositoryBuilder<T>,
|
||||
): void {
|
||||
if (!this.repositories.has(name)) {
|
||||
this.repositories.set(name, repository);
|
||||
}
|
||||
}
|
||||
|
||||
public getRepository<T>(name: string): RepositoryBuilder<T> {
|
||||
const repository = this.repositories.get(name) as RepositoryBuilder<T>;
|
||||
|
||||
if (!repository) {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
|
||||
`Repository "${name}" not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export interface IRepositoryQueryOptions {
|
||||
query: any;
|
||||
}
|
||||
|
||||
export interface IRepositoryQueryBuilder {}
|
||||
6
server/src/contexts/common/domain/repositories/index.ts
Normal file
6
server/src/contexts/common/domain/repositories/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./Adapter.interface";
|
||||
export * from "./BusinessTransaction.interface";
|
||||
export * from "./Repository.interface";
|
||||
export * from "./RepositoryBuilder";
|
||||
export * from "./RepositoryManager";
|
||||
export * from "./RepositoryQueryBuilder.interface";
|
||||
0
server/src/contexts/common/domain/services/index.ts
Normal file
0
server/src/contexts/common/domain/services/index.ts
Normal file
36
server/src/contexts/common/infrastructure/ContextFactory.ts
Normal file
36
server/src/contexts/common/infrastructure/ContextFactory.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { IRepositoryManager, RepositoryManager } from "../domain";
|
||||
import { ISequelizeAdapter, createSequelizeAdapter } from "./sequelize";
|
||||
|
||||
// ContextFactory.ts
|
||||
export interface IContext<T> {
|
||||
adapter: ISequelizeAdapter;
|
||||
repositoryManager: IRepositoryManager;
|
||||
services: T;
|
||||
}
|
||||
|
||||
export class ContextFactory<T> {
|
||||
private static instances: Map<string, ContextFactory<any>> = new Map();
|
||||
|
||||
public static getInstance<T>(constructor: new () => T): ContextFactory<T> {
|
||||
const key = constructor.name;
|
||||
if (!ContextFactory.instances.has(key)) {
|
||||
ContextFactory.instances.set(key, new ContextFactory<T>(constructor));
|
||||
}
|
||||
|
||||
return ContextFactory.instances.get(key)! as ContextFactory<T>;
|
||||
}
|
||||
|
||||
private context: IContext<T>;
|
||||
|
||||
private constructor(constructor: new () => T) {
|
||||
this.context = {
|
||||
adapter: createSequelizeAdapter(),
|
||||
repositoryManager: RepositoryManager.getInstance(),
|
||||
services: new constructor(),
|
||||
};
|
||||
}
|
||||
|
||||
public getContext(): IContext<T> {
|
||||
return this.context;
|
||||
}
|
||||
}
|
||||
57
server/src/contexts/common/infrastructure/InfrastructureError.ts
Executable file
57
server/src/contexts/common/infrastructure/InfrastructureError.ts
Executable file
@ -0,0 +1,57 @@
|
||||
import { ValidationError } from "joi";
|
||||
import { UseCaseError } from "../application";
|
||||
import { IServerError, ServerError } from "../domain/errors";
|
||||
|
||||
export interface IInfrastructureError extends IServerError {}
|
||||
|
||||
export class InfrastructureError
|
||||
extends ServerError
|
||||
implements IInfrastructureError
|
||||
{
|
||||
public static readonly UNEXCEPTED_ERROR = "UNEXCEPTED_ERROR";
|
||||
public static readonly INVALID_INPUT_DATA = "INVALID_INPUT_DATA";
|
||||
public static readonly RESOURCE_NOT_READY = "RESOURCE_NOT_READY";
|
||||
public static readonly RESOURCE_NOT_FOUND_ERROR = "RESOURCE_NOT_FOUND_ERROR";
|
||||
public static readonly RESOURCE_ALREADY_REGISTERED =
|
||||
"RESOURCE_ALREADY_REGISTERED";
|
||||
|
||||
public static create(
|
||||
code: string,
|
||||
message: string,
|
||||
payload?: Record<string, any>
|
||||
): InfrastructureError {
|
||||
return new InfrastructureError(code, message, payload);
|
||||
}
|
||||
}
|
||||
|
||||
function _isJoiError(error: Error) {
|
||||
return error.name === "ValidationError";
|
||||
}
|
||||
|
||||
export function handleInfrastructureError(
|
||||
code: string,
|
||||
message: string,
|
||||
error: Error // UseCaseError | ValidationError
|
||||
): IInfrastructureError {
|
||||
let payload = {};
|
||||
|
||||
if (_isJoiError(error)) {
|
||||
//Joi => error.details
|
||||
payload = (<ValidationError>error).details;
|
||||
} else {
|
||||
// UseCaseError
|
||||
/*const useCaseError = <UseCaseError>error;
|
||||
if (useCaseError.payload.path) {
|
||||
const errorItem = {};
|
||||
errorItem[`${useCaseError.payload.path}`] = useCaseError.message;
|
||||
payload = {+
|
||||
errors: [errorItem],
|
||||
};
|
||||
}*/
|
||||
payload = (<UseCaseError>error).payload;
|
||||
}
|
||||
|
||||
console.log(payload);
|
||||
|
||||
return InfrastructureError.create(code, message, payload);
|
||||
}
|
||||
@ -0,0 +1,232 @@
|
||||
import * as express from "express";
|
||||
import { URL } from "url";
|
||||
|
||||
import {
|
||||
IErrorExtra_Response_DTO,
|
||||
IError_Response_DTO,
|
||||
} from "@shared/contexts";
|
||||
import { UseCaseError } from "../../application";
|
||||
import { IServerError } from "../../domain/errors";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
import { ProblemDocument, ProblemDocumentExtension } from "./ProblemDocument";
|
||||
|
||||
export interface IController {}
|
||||
|
||||
export abstract class ExpressController implements IController {
|
||||
protected req: express.Request;
|
||||
protected res: express.Response;
|
||||
protected next: express.NextFunction;
|
||||
|
||||
protected serverURL: string = "";
|
||||
protected file: any;
|
||||
|
||||
protected abstract executeImpl(): Promise<void | any>;
|
||||
|
||||
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
|
||||
}/api/v1`;
|
||||
|
||||
this.file = this.req && this.req["file"]; // <-- ????
|
||||
|
||||
this.executeImpl();
|
||||
}
|
||||
|
||||
public ok<T>(dto?: T) {
|
||||
if (dto) {
|
||||
return this._jsonResponse(200, dto);
|
||||
}
|
||||
|
||||
return this.res.status(200).send();
|
||||
}
|
||||
|
||||
public fail(error: IServerError) {
|
||||
console.group("ExpressController FAIL RESPONSE ====================");
|
||||
console.log(error);
|
||||
console.trace("Show me");
|
||||
console.groupEnd();
|
||||
|
||||
return this._errorResponse(500, error ? error.toString() : "Fail");
|
||||
}
|
||||
|
||||
public created<T>(dto?: T) {
|
||||
if (dto) {
|
||||
return this.res.status(201).json(dto).send();
|
||||
}
|
||||
|
||||
return this.res.status(201).send();
|
||||
}
|
||||
|
||||
public noContent() {
|
||||
return this.res.status(204).send();
|
||||
}
|
||||
|
||||
public download(filepath: string, filename: string, done?: any) {
|
||||
return this.res.download(filepath, filename, done);
|
||||
}
|
||||
|
||||
public clientError(message?: string) {
|
||||
return this._errorResponse(400, message);
|
||||
}
|
||||
|
||||
public unauthorizedError(message?: string) {
|
||||
return this._errorResponse(401, message);
|
||||
}
|
||||
|
||||
public paymentRequiredError(message?: string) {
|
||||
return this._errorResponse(402, message);
|
||||
}
|
||||
|
||||
public forbiddenError(message?: string) {
|
||||
return this._errorResponse(403, message);
|
||||
}
|
||||
|
||||
public notFoundError(message: string, error?: IServerError) {
|
||||
return this._errorResponse(404, message, error);
|
||||
}
|
||||
|
||||
public conflictError(message: string, error?: IServerError) {
|
||||
return this._errorResponse(409, message, error);
|
||||
}
|
||||
|
||||
public invalidInputError(message?: string, error?: InfrastructureError) {
|
||||
return this._errorResponse(422, message, error);
|
||||
}
|
||||
|
||||
public tooManyError(message: string, error?: Error) {
|
||||
return this._errorResponse(429, message, error);
|
||||
}
|
||||
|
||||
public internalServerError(message?: string, error?: IServerError) {
|
||||
return this._errorResponse(500, message, error);
|
||||
}
|
||||
|
||||
public todoError(message?: string) {
|
||||
return this._errorResponse(501, message);
|
||||
}
|
||||
|
||||
public unavailableError(message?: string) {
|
||||
return this._errorResponse(503, message);
|
||||
}
|
||||
|
||||
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
|
||||
): express.Response<IError_Response_DTO> {
|
||||
const context = {};
|
||||
|
||||
if (Object.keys(this.res.locals).length) {
|
||||
if ("user" in this.res.locals) {
|
||||
context["user"] = this.res.locals.user;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(this.req.params).length) {
|
||||
context["params"] = this.req.params;
|
||||
}
|
||||
|
||||
if (Object.keys(this.req.query).length) {
|
||||
context["query"] = this.req.query;
|
||||
}
|
||||
|
||||
if (Object.keys(this.req.body).length) {
|
||||
context["body"] = this.req.body;
|
||||
}
|
||||
|
||||
const extension = new ProblemDocumentExtension({
|
||||
context,
|
||||
extra: error ? { ...this._processError(error) } : {},
|
||||
});
|
||||
|
||||
return this._jsonResponse(
|
||||
statusCode,
|
||||
new ProblemDocument(
|
||||
{
|
||||
status: statusCode,
|
||||
detail: message,
|
||||
instance: this.req.baseUrl,
|
||||
},
|
||||
extension
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _processError(
|
||||
error: Error | InfrastructureError
|
||||
): IErrorExtra_Response_DTO {
|
||||
/**
|
||||
*
|
||||
*
|
||||
*
|
||||
{
|
||||
code: "INVALID_INPUT_DATA",
|
||||
payload: {
|
||||
label: "tin",
|
||||
path: "tin", // [{path: "first_name"}, {path: "last_name"}]
|
||||
},
|
||||
name: "UseCaseError",
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
code: "INVALID_INPUT_DATA",
|
||||
payload: [
|
||||
{
|
||||
tin: "{tin} is not allowed to be empty",
|
||||
},
|
||||
{
|
||||
first_name: "{first_name} is not allowed to be empty",
|
||||
},
|
||||
{
|
||||
last_name: "{last_name} is not allowed to be empty",
|
||||
},
|
||||
{
|
||||
company_name: "{company_name} is not allowed to be empty",
|
||||
},
|
||||
],
|
||||
name: "InfrastructureError",
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
const useCaseError = <UseCaseError>error;
|
||||
|
||||
const payload = !Array.isArray(useCaseError.payload)
|
||||
? Array(useCaseError.payload)
|
||||
: useCaseError.payload;
|
||||
|
||||
const errors = payload.map((item) => {
|
||||
if (item.path) {
|
||||
return item.path
|
||||
? {
|
||||
[String(item.path)]: useCaseError.message,
|
||||
}
|
||||
: {};
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
// https://raw.githubusercontent.com/PDMLab/http-problem-details/master/src/StatusCodes.ts
|
||||
|
||||
export const httpStatusCodes = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Payload Too Large",
|
||||
414: "URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Entity",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
};
|
||||
@ -0,0 +1,106 @@
|
||||
// RFC 7807 - Problem Details for HTTP APIs
|
||||
// https://datatracker.ietf.org/doc/html/rfc7807
|
||||
// https://raw.githubusercontent.com/PDMLab/http-problem-details/master/src/ProblemDocument.ts
|
||||
|
||||
import { httpStatusCodes } from "./HttpStatusCodes";
|
||||
|
||||
/**
|
||||
* A problem details object can have the following members:
|
||||
|
||||
o "type" (string) - A URI reference [RFC3986] that identifies the
|
||||
problem type. This specification encourages that, when
|
||||
dereferenced, it provide human-readable documentation for the
|
||||
problem type (e.g., using HTML [W3C.REC-html5-20141028]). When
|
||||
this member is not present, its value is assumed to be
|
||||
"about:blank".
|
||||
|
||||
o "title" (string) - A short, human-readable summary of the problem
|
||||
type. It SHOULD NOT change from occurrence to occurrence of the
|
||||
problem, except for purposes of localization (e.g., using
|
||||
proactive content negotiation; see [RFC7231], Section 3.4).
|
||||
|
||||
o "status" (number) - The HTTP status code ([RFC7231], Section 6)
|
||||
generated by the origin server for this occurrence of the problem.
|
||||
|
||||
o "detail" (string) - A human-readable explanation specific to this
|
||||
occurrence of the problem.
|
||||
|
||||
o "instance" (string) - A URI reference that identifies the specific
|
||||
occurrence of the problem. It may or may not yield further
|
||||
information if dereferenced.
|
||||
|
||||
*/
|
||||
|
||||
export class ProblemDocument {
|
||||
public detail?: string;
|
||||
public instance?: string;
|
||||
public status: number;
|
||||
public title: string;
|
||||
public type?: string;
|
||||
//public status_text: string;
|
||||
|
||||
public constructor(
|
||||
options: ProblemDocumentOptions,
|
||||
extension?: ProblemDocumentExtension | Record<string, object>
|
||||
) {
|
||||
const { title, detail, instance, status } = options;
|
||||
let { type } = options;
|
||||
|
||||
if (status && !type) {
|
||||
type = "about:blank";
|
||||
}
|
||||
|
||||
/*if (instance) {
|
||||
// eslint-disable-next-line node/no-deprecated-api
|
||||
url.parse(instance);
|
||||
}*/
|
||||
|
||||
/*if (type) {
|
||||
// eslint-disable-next-line node/no-deprecated-api
|
||||
url.parse(type);
|
||||
// eslint-disable-next-line node/no-deprecated-api
|
||||
}*/
|
||||
|
||||
// const result = {
|
||||
this.type = type;
|
||||
|
||||
//if (detail) {
|
||||
this.detail = detail;
|
||||
//}
|
||||
|
||||
this.instance = instance;
|
||||
this.status = Number(status);
|
||||
this.title = title ? String(title) : httpStatusCodes[this.status];
|
||||
//this.status_text = status ? httpStatusCodes[status] : "";
|
||||
// };
|
||||
|
||||
if (extension) {
|
||||
const extensionProperties =
|
||||
extension instanceof ProblemDocumentExtension
|
||||
? extension.extensionProperties
|
||||
: extension;
|
||||
|
||||
for (const propertyName in extensionProperties) {
|
||||
if (propertyName in extensionProperties) {
|
||||
this[propertyName] = extensionProperties[propertyName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProblemDocumentOptions {
|
||||
public detail?: string;
|
||||
public instance?: string;
|
||||
public type?: string;
|
||||
public title?: string;
|
||||
public status?: number;
|
||||
}
|
||||
|
||||
export class ProblemDocumentExtension {
|
||||
public extensionProperties: Record<string, object>;
|
||||
|
||||
public constructor(extensionProperties: Record<string, object>) {
|
||||
this.extensionProperties = extensionProperties;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from './ExpressController';
|
||||
@ -0,0 +1,107 @@
|
||||
import { config } from "@/config";
|
||||
import { initLogger } from "@/infrastructure/logger";
|
||||
import rTracer from "cls-rtracer";
|
||||
import Firebird from "node-firebird";
|
||||
import { IAdapter, IRepositoryQueryBuilder } from "../../domain";
|
||||
import { FirebirdBusinessTransaction } from "./FirebirdBusinessTransaction";
|
||||
|
||||
export interface IFirebirdAdapter extends IAdapter {
|
||||
queryBuilder: IRepositoryQueryBuilder;
|
||||
|
||||
disconnect: () => void;
|
||||
execute: <T>(query: string, params: any[]) => Promise<T>;
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
export class FirebirdAdapter implements IFirebirdAdapter {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: FirebirdAdapter;
|
||||
|
||||
public static getInstance(params: {
|
||||
queryBuilder: IRepositoryQueryBuilder;
|
||||
}): FirebirdAdapter {
|
||||
if (!FirebirdAdapter.instance) {
|
||||
FirebirdAdapter.instance = FirebirdAdapter.create(params);
|
||||
}
|
||||
|
||||
return FirebirdAdapter.instance;
|
||||
}
|
||||
|
||||
private static create(params: { queryBuilder: IRepositoryQueryBuilder }) {
|
||||
const { queryBuilder } = params;
|
||||
const connection = initConnection();
|
||||
|
||||
return new FirebirdAdapter(connection, queryBuilder);
|
||||
}
|
||||
|
||||
private _connection: Firebird.ConnectionPool;
|
||||
private _queryBuilder: IRepositoryQueryBuilder;
|
||||
|
||||
protected constructor(
|
||||
connection: Firebird.ConnectionPool,
|
||||
queryBuilder: IRepositoryQueryBuilder
|
||||
) {
|
||||
this._connection = connection;
|
||||
this._queryBuilder = queryBuilder;
|
||||
}
|
||||
|
||||
get queryBuilder(): IRepositoryQueryBuilder {
|
||||
return this._queryBuilder;
|
||||
}
|
||||
|
||||
public startTransaction(): FirebirdBusinessTransaction {
|
||||
return new FirebirdBusinessTransaction(this._connection).start();
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this._connection) {
|
||||
this._connection.destroy((err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
endConnection();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async execute<T>(query: string, params: any[] = []): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._connection.get((err, db) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
db.query(query, params, (err, result) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
db.detach();
|
||||
resolve(<T>result);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public sync() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.execute("select current_connection from rdb$database")
|
||||
.then(() => resolve(this))
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initConnection() {
|
||||
const { poolCount, ...firebirdOptions } = config.firebird;
|
||||
const logger = initLogger(rTracer);
|
||||
|
||||
logger.debug("=========================> CONECTO A FIREBIRD");
|
||||
|
||||
return Firebird.pool(poolCount, firebirdOptions);
|
||||
}
|
||||
|
||||
function endConnection() {
|
||||
const logger = initLogger(rTracer);
|
||||
|
||||
logger.debug("<========================= DESCONECTO DE FIREBIRD");
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import Firebird from "node-firebird";
|
||||
import { TBusinessTransaction } from "../../domain/repositories";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
|
||||
export type FirebirdBusinessTransactionType = TBusinessTransaction & {
|
||||
start: (a: unknown) => unknown;
|
||||
complete(
|
||||
work: (t: Firebird.Transaction, db: Firebird.Database) => unknown
|
||||
): void;
|
||||
};
|
||||
|
||||
export class FirebirdBusinessTransaction
|
||||
implements FirebirdBusinessTransactionType
|
||||
{
|
||||
private _connection: Firebird.ConnectionPool;
|
||||
|
||||
constructor(connection: Firebird.ConnectionPool) {
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
public start() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public complete(
|
||||
work: (t: Firebird.Transaction, db: Firebird.Database) => unknown
|
||||
): void {
|
||||
this._connection.get((err, db: Firebird.Database) => {
|
||||
if (err) {
|
||||
InfrastructureError.create(InfrastructureError.UNEXCEPTED_ERROR, err);
|
||||
}
|
||||
|
||||
db.transaction(
|
||||
Firebird.ISOLATION_READ_COMMITTED,
|
||||
(err, transaction: Firebird.Transaction) => {
|
||||
if (err) {
|
||||
InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
work(transaction, db);
|
||||
transaction.commit();
|
||||
} catch (e: unknown) {
|
||||
transaction.rollback();
|
||||
} finally {
|
||||
db.detach();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||
import Firebird from "node-firebird";
|
||||
|
||||
import {
|
||||
IRepository,
|
||||
IRepositoryQueryBuilder,
|
||||
} from "../../domain/repositories";
|
||||
import { IFirebirdAdapter } from "./FirebirdAdapter";
|
||||
|
||||
export abstract class FirebirdRepository<T> implements IRepository<T> {
|
||||
protected queryBuilder: IRepositoryQueryBuilder;
|
||||
protected transaction: Firebird.Transaction;
|
||||
protected adapter: IFirebirdAdapter;
|
||||
|
||||
public constructor(props: {
|
||||
adapter: IFirebirdAdapter;
|
||||
transaction: Firebird.Transaction;
|
||||
}) {
|
||||
this.adapter = props.adapter;
|
||||
this.transaction = props.transaction;
|
||||
this.queryBuilder = this.adapter.queryBuilder;
|
||||
}
|
||||
|
||||
protected getById(id: UniqueID): Promise<T | null> {
|
||||
throw new Error("[FirebirdRepository] getById not implemented!");
|
||||
}
|
||||
|
||||
protected findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<T>> {
|
||||
throw new Error("[FirebirdRepository] findAll not implemented!");
|
||||
}
|
||||
|
||||
protected save(t: T): Promise<void> {
|
||||
throw new Error("[FirebirdRepository] save not implemented!");
|
||||
}
|
||||
|
||||
/* protected remove(t: T): Promise<boolean> {
|
||||
throw new Error('[FirebirdRepository] remove not implemented!');
|
||||
}*/
|
||||
|
||||
protected removeById(id: UniqueID): Promise<void> {
|
||||
throw new Error("[FirebirdRepository] removeById not implemented!");
|
||||
}
|
||||
|
||||
protected async _getBy(
|
||||
modelName: string,
|
||||
field: string,
|
||||
value: any,
|
||||
params: any = {}
|
||||
): Promise<T> {
|
||||
throw new Error("[FirebirdRepository] _getBy not implemented!");
|
||||
}
|
||||
|
||||
protected async _getById(
|
||||
modelName: string,
|
||||
id: UniqueID | string,
|
||||
params: any = {}
|
||||
): Promise<T> {
|
||||
throw new Error("[FirebirdRepository] _getById not implemented!");
|
||||
}
|
||||
|
||||
protected async _findAll(
|
||||
modelName: string,
|
||||
queryCriteria?: IQueryCriteria,
|
||||
params: any = {}
|
||||
): Promise<{ rows: any[]; count: number }> {
|
||||
console.time("_findAll");
|
||||
throw new Error("[FirebirdRepository] _findAll not implemented!");
|
||||
}
|
||||
|
||||
protected async _exists(
|
||||
modelName: string,
|
||||
field: string,
|
||||
value: any,
|
||||
params: any = {}
|
||||
): Promise<boolean> {
|
||||
throw new Error("[FirebirdRepository] _exists not implemented!");
|
||||
}
|
||||
|
||||
protected async _save(
|
||||
modelName: string,
|
||||
id: UniqueID,
|
||||
data: any,
|
||||
params: any = {}
|
||||
): Promise<void> {
|
||||
throw new Error("[FirebirdRepository] _save not implemented!");
|
||||
}
|
||||
|
||||
protected async _removeById(
|
||||
modelName: string,
|
||||
id: UniqueID,
|
||||
force: boolean = false,
|
||||
params: any = {}
|
||||
): Promise<void> {
|
||||
throw new Error("[FirebirdRepository] _removeById not implemented!");
|
||||
}
|
||||
}
|
||||
12
server/src/contexts/common/infrastructure/firebird/index.ts
Normal file
12
server/src/contexts/common/infrastructure/firebird/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { FirebirdAdapter, IFirebirdAdapter } from "./FirebirdAdapter";
|
||||
import { createFirebirdQueryBuilder } from "./queryBuilder";
|
||||
|
||||
const createFirebirdAdapter = () => {
|
||||
return FirebirdAdapter.getInstance({
|
||||
queryBuilder: createFirebirdQueryBuilder(),
|
||||
});
|
||||
};
|
||||
|
||||
export { IFirebirdAdapter, createFirebirdAdapter };
|
||||
|
||||
export * from "./FirebirdRepository";
|
||||
@ -0,0 +1,12 @@
|
||||
import {
|
||||
IRepositoryQueryBuilder,
|
||||
IRepositoryQueryOptions,
|
||||
} from "@/contexts/common/domain";
|
||||
|
||||
export interface ISequelizeQueryOptions extends IRepositoryQueryOptions {}
|
||||
|
||||
export class FirebirdQueryBuilder implements IRepositoryQueryBuilder {
|
||||
public static create() {
|
||||
return new FirebirdQueryBuilder();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { FirebirdQueryBuilder } from "./FirebirdQueryBuilder";
|
||||
|
||||
const createFirebirdQueryBuilder = () => FirebirdQueryBuilder.create();
|
||||
|
||||
export { createFirebirdQueryBuilder };
|
||||
3
server/src/contexts/common/infrastructure/index.ts
Normal file
3
server/src/contexts/common/infrastructure/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./ContextFactory";
|
||||
export * from "./InfrastructureError";
|
||||
export * from "./mappers";
|
||||
@ -0,0 +1,175 @@
|
||||
import { FirebirdModel } from "@/contexts/catalog/infrastructure/firebird/firebird.model";
|
||||
import { Collection, Entity, Result } from "@shared/contexts";
|
||||
import { ValidationError } from "sequelize";
|
||||
import {
|
||||
FieldValueError,
|
||||
RequiredFieldMissingError,
|
||||
} from "../../domain/errors";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
|
||||
export interface IFirebirdMapper<
|
||||
TModel extends FirebirdModel,
|
||||
TEntity extends Entity<any>,
|
||||
> {
|
||||
mapToDomain(source: TModel, params?: Record<string, any>): TEntity;
|
||||
|
||||
mapArrayToDomain(
|
||||
source: TModel[],
|
||||
params?: Record<string, any>
|
||||
): Collection<TEntity>;
|
||||
|
||||
mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: Record<string, any>
|
||||
): Collection<TEntity>;
|
||||
}
|
||||
|
||||
export abstract class FirebirdMapper<
|
||||
TModel extends FirebirdModel = any,
|
||||
TModelAttributes = any,
|
||||
TEntity extends Entity<any> = any,
|
||||
> implements IFirebirdMapper<TModel, TEntity>
|
||||
{
|
||||
public constructor(protected props: any) {}
|
||||
|
||||
public mapToDomain(source: TModel, params?: Record<string, any>): 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 mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: Record<string, any>
|
||||
): Collection<TEntity> {
|
||||
const items = source
|
||||
? source.map((value, index: number) =>
|
||||
this.toDomainMappingImpl!(value, { index, ...params })
|
||||
)
|
||||
: [];
|
||||
return new Collection(items, totalCount);
|
||||
}
|
||||
|
||||
protected toDomainMappingImpl(
|
||||
source: TModel,
|
||||
params?: Record<string, any>
|
||||
): TEntity {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
'Method "toDomainMappingImpl" not implemented!'
|
||||
);
|
||||
}
|
||||
|
||||
protected handleRequiredFieldError(key: string, error: Error) {
|
||||
throw RequiredFieldMissingError.field(key, error.message);
|
||||
}
|
||||
|
||||
protected handleInvalidFieldError(key: string, error: ValidationError) {
|
||||
throw FieldValueError.field(key, error.message);
|
||||
}
|
||||
|
||||
protected mapsValue(
|
||||
row: TModel,
|
||||
key: string,
|
||||
customMapFn: (
|
||||
value: any,
|
||||
params: Record<string, any>
|
||||
) => Result<any, Error>,
|
||||
params: Record<string, any> = {
|
||||
defaultValue: null,
|
||||
}
|
||||
) {
|
||||
let value = params.defaultValue;
|
||||
|
||||
if (!row || typeof row !== "object") {
|
||||
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 {
|
||||
value = row.getDataValue(key);
|
||||
}
|
||||
|
||||
const valueOrError = customMapFn(value, params);
|
||||
|
||||
if (valueOrError.isFailure) {
|
||||
this.handleFailure(valueOrError.error, key);
|
||||
}
|
||||
return valueOrError.object;
|
||||
}
|
||||
|
||||
protected mapsAssociation(
|
||||
row: TModel,
|
||||
associationName: string,
|
||||
customMapper: any,
|
||||
params: Record<string, any> = {}
|
||||
) {
|
||||
if (!customMapper) {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
'Custom mapper undefined at "mapsAssociation"!'
|
||||
);
|
||||
}
|
||||
|
||||
const { filter, ...otherParams } = params;
|
||||
|
||||
let associationRows = [];
|
||||
|
||||
if (Object.keys(row).length === 0) {
|
||||
console.debug(
|
||||
`Data row has not keys! Association ${associationName} not exists in data row!`
|
||||
);
|
||||
} else if (!Object.hasOwn(row.dataValues, associationName)) {
|
||||
console.debug(`Association ${associationName} not exists in data row!`);
|
||||
} else {
|
||||
associationRows = row.getDataValue(associationName);
|
||||
}
|
||||
|
||||
const customMapFn =
|
||||
Array.isArray(associationRows) && associationRows.length > 0
|
||||
? customMapper.mapArrayToDomain
|
||||
: customMapper.mapToDomain;
|
||||
|
||||
if (filter) {
|
||||
associationRows = Array.isArray(associationRows)
|
||||
? associationRows.filter(filter)
|
||||
: filter(associationRows);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
//const associatedData = row[association.accessors.get]();
|
||||
//return associatedData;
|
||||
}
|
||||
|
||||
private handleFailure(error: Error, key: string) {
|
||||
if (error instanceof ValidationError) {
|
||||
this.handleInvalidFieldError(key, error);
|
||||
} else {
|
||||
this.handleRequiredFieldError(key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,211 @@
|
||||
import { Collection, Entity, ICollection, Result } from "@shared/contexts";
|
||||
import { Model, ValidationError } from "sequelize";
|
||||
import {
|
||||
FieldValueError,
|
||||
RequiredFieldMissingError,
|
||||
} from "../../domain/errors";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
|
||||
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>;
|
||||
|
||||
mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: Record<string, any>
|
||||
): Collection<TEntity>;
|
||||
|
||||
mapToPersistence(
|
||||
source: TEntity,
|
||||
params?: Record<string, any>
|
||||
): TModelAttributes;
|
||||
|
||||
mapCollectionToPersistence(
|
||||
source: ICollection<TEntity>,
|
||||
params?: Record<string, any>
|
||||
): TModelAttributes[];
|
||||
}
|
||||
|
||||
export abstract class SequelizeMapper<
|
||||
TModel extends Model = any,
|
||||
TModelAttributes = any,
|
||||
TEntity extends Entity<any> = any,
|
||||
> implements ISequelizeMapper<TModel, TModelAttributes, TEntity>
|
||||
{
|
||||
public constructor(protected props: any) {}
|
||||
|
||||
public mapToDomain(source: TModel, params?: Record<string, any>): 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 mapArrayAndCountToDomain(
|
||||
source: TModel[],
|
||||
totalCount: number,
|
||||
params?: Record<string, any>
|
||||
): Collection<TEntity> {
|
||||
const items = source
|
||||
? source.map((value, index: number) =>
|
||||
this.toDomainMappingImpl!(value, { index, ...params })
|
||||
)
|
||||
: [];
|
||||
return new Collection(items, totalCount);
|
||||
}
|
||||
|
||||
public mapToPersistence(
|
||||
source: TEntity,
|
||||
params?: Record<string, any>
|
||||
): TModelAttributes {
|
||||
return this.toPersistenceMappingImpl(source, params);
|
||||
}
|
||||
|
||||
public mapCollectionToPersistence(
|
||||
source: ICollection<TEntity>,
|
||||
params?: Record<string, any>
|
||||
): TModelAttributes[] {
|
||||
return source.items.map((value: TEntity, index: number) =>
|
||||
this.toPersistenceMappingImpl!(value, { index, ...params })
|
||||
);
|
||||
}
|
||||
|
||||
protected toDomainMappingImpl(
|
||||
source: TModel,
|
||||
params?: Record<string, any>
|
||||
): TEntity {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
'Method "toDomainMappingImpl" not implemented!'
|
||||
);
|
||||
}
|
||||
|
||||
protected toPersistenceMappingImpl(
|
||||
source: TEntity,
|
||||
params?: Record<string, any>
|
||||
): TModelAttributes {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
'Method "toPersistenceMappingImpl" not implemented!'
|
||||
);
|
||||
}
|
||||
|
||||
protected handleRequiredFieldError(key: string, error: Error) {
|
||||
throw RequiredFieldMissingError.field(key, error.message);
|
||||
}
|
||||
|
||||
protected handleInvalidFieldError(key: string, error: ValidationError) {
|
||||
throw FieldValueError.field(key, error.message);
|
||||
}
|
||||
|
||||
protected mapsValue(
|
||||
row: TModel,
|
||||
key: string,
|
||||
customMapFn: (
|
||||
value: any,
|
||||
params: Record<string, any>
|
||||
) => Result<any, Error>,
|
||||
params: Record<string, any> = {
|
||||
defaultValue: null,
|
||||
}
|
||||
) {
|
||||
let value = params.defaultValue;
|
||||
|
||||
if (!row || typeof row !== "object") {
|
||||
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 {
|
||||
value = row.getDataValue(key);
|
||||
}
|
||||
|
||||
const valueOrError = customMapFn(value, params);
|
||||
|
||||
if (valueOrError.isFailure) {
|
||||
this.handleFailure(valueOrError.error, key);
|
||||
}
|
||||
return valueOrError.object;
|
||||
}
|
||||
|
||||
protected mapsAssociation(
|
||||
row: TModel,
|
||||
associationName: string,
|
||||
customMapper: any,
|
||||
params: Record<string, any> = {}
|
||||
) {
|
||||
if (!customMapper) {
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
'Custom mapper undefined at "mapsAssociation"!'
|
||||
);
|
||||
}
|
||||
|
||||
const { filter, ...otherParams } = params;
|
||||
|
||||
let associationRows = [];
|
||||
|
||||
if (Object.keys(row).length === 0) {
|
||||
console.debug(
|
||||
`Data row has not keys! Association ${associationName} not exists in data row!`
|
||||
);
|
||||
} else if (!Object.hasOwn(row.dataValues, associationName)) {
|
||||
console.debug(`Association ${associationName} not exists in data row!`);
|
||||
} else {
|
||||
associationRows = row.getDataValue(associationName);
|
||||
}
|
||||
|
||||
const customMapFn =
|
||||
Array.isArray(associationRows) && associationRows.length > 0
|
||||
? customMapper.mapArrayToDomain
|
||||
: customMapper.mapToDomain;
|
||||
|
||||
if (filter) {
|
||||
associationRows = Array.isArray(associationRows)
|
||||
? associationRows.filter(filter)
|
||||
: filter(associationRows);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
//const associatedData = row[association.accessors.get]();
|
||||
//return associatedData;
|
||||
}
|
||||
|
||||
private handleFailure(error: Error, key: string) {
|
||||
if (error instanceof ValidationError) {
|
||||
this.handleInvalidFieldError(key, error);
|
||||
} else {
|
||||
this.handleRequiredFieldError(key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./SequelizeMapper";
|
||||
@ -0,0 +1,189 @@
|
||||
import { config } from "@/config";
|
||||
import rTracer from "cls-rtracer";
|
||||
import { DataTypes, Sequelize } from "sequelize";
|
||||
import { SequelizeRevision } from "sequelize-revision";
|
||||
|
||||
import { initLogger } from "@/infrastructure/logger";
|
||||
import * as glob from "glob";
|
||||
import * as path from "path";
|
||||
import { IAdapter } from "../../domain";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
import {
|
||||
SequelizeBusinessTransaction,
|
||||
SequelizeBusinessTransactionType,
|
||||
} from "./SequelizeBusinessTransaction";
|
||||
import { ISequelizeModels } from "./SequelizeModel.interface";
|
||||
import { ISequelizeQueryBuilder } from "./queryBuilder/SequelizeQueryBuilder";
|
||||
|
||||
//import * as dotenv from "dotenv";
|
||||
//dotenv.config();
|
||||
|
||||
export interface ISequelizeAdapter extends IAdapter {
|
||||
queryBuilder: ISequelizeQueryBuilder;
|
||||
|
||||
getModel: (modelName: string) => any;
|
||||
hasModel: (modelName: string) => boolean;
|
||||
}
|
||||
|
||||
export class SequelizeAdapter implements ISequelizeAdapter {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: SequelizeAdapter;
|
||||
|
||||
public static getInstance(params: {
|
||||
queryBuilder: ISequelizeQueryBuilder;
|
||||
}): SequelizeAdapter {
|
||||
if (!SequelizeAdapter.instance) {
|
||||
SequelizeAdapter.instance = SequelizeAdapter.create(params);
|
||||
}
|
||||
|
||||
return SequelizeAdapter.instance;
|
||||
}
|
||||
|
||||
private static create(params: { queryBuilder: ISequelizeQueryBuilder }) {
|
||||
const { queryBuilder } = params;
|
||||
|
||||
const connection = initConnection();
|
||||
const sequelizeRevision = new SequelizeRevision(connection, {
|
||||
UUID: true,
|
||||
tableName: "revisions",
|
||||
//changeTableName: "",
|
||||
underscored: true,
|
||||
underscoredAttributes: true,
|
||||
});
|
||||
const models = registerModels(connection, sequelizeRevision);
|
||||
|
||||
return new SequelizeAdapter(
|
||||
connection,
|
||||
models,
|
||||
queryBuilder,
|
||||
sequelizeRevision
|
||||
);
|
||||
}
|
||||
|
||||
private _connection: Sequelize;
|
||||
private _models: ISequelizeModels;
|
||||
private _queryBuilder: ISequelizeQueryBuilder;
|
||||
private _revisions: SequelizeRevision<any>;
|
||||
|
||||
protected constructor(
|
||||
connection: Sequelize,
|
||||
models: ISequelizeModels,
|
||||
queryBuilder: ISequelizeQueryBuilder,
|
||||
revisions: SequelizeRevision<any>
|
||||
) {
|
||||
this._connection = connection;
|
||||
this._models = models;
|
||||
this._queryBuilder = queryBuilder;
|
||||
this._revisions = revisions;
|
||||
}
|
||||
|
||||
get queryBuilder(): ISequelizeQueryBuilder {
|
||||
return this._queryBuilder;
|
||||
}
|
||||
|
||||
public startTransaction(): SequelizeBusinessTransactionType {
|
||||
return new SequelizeBusinessTransaction(this._connection);
|
||||
}
|
||||
|
||||
public sync(params) {
|
||||
return this._connection.sync(params);
|
||||
}
|
||||
|
||||
public getModel(modelName: string) {
|
||||
if (this.hasModel(modelName)) {
|
||||
return this._models[modelName];
|
||||
}
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.RESOURCE_NOT_FOUND_ERROR,
|
||||
`[SequelizeAdapter] ${modelName} sequelize model not exists!`
|
||||
);
|
||||
}
|
||||
|
||||
public hasModel(modelName: string): boolean {
|
||||
return !!this._models[modelName];
|
||||
}
|
||||
}
|
||||
|
||||
function initConnection(): Sequelize {
|
||||
const { username, password, database, host, dialect, port } = config.database;
|
||||
const logger = initLogger(rTracer);
|
||||
|
||||
logger.debug("=========================> CONECTO A SEQUELIZE");
|
||||
|
||||
return new Sequelize(database, username, password, {
|
||||
host,
|
||||
dialect,
|
||||
port,
|
||||
dialectOptions: {
|
||||
multipleStatements: true,
|
||||
dateStrings: true,
|
||||
typeCast: true,
|
||||
//timezone: "Z",
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
acquire: 60000,
|
||||
idle: 10000,
|
||||
},
|
||||
logQueryParameters: true,
|
||||
logging: (sql, timing) => console.debug(sql), //logger.debug(sql, timing),
|
||||
|
||||
define: {
|
||||
charset: "utf8mb4",
|
||||
collate: "utf8mb4_unicode_ci",
|
||||
//freezeTableName: true,
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function registerModels(
|
||||
connection: Sequelize,
|
||||
sequelizeRevision: SequelizeRevision<any>
|
||||
): ISequelizeModels {
|
||||
const cwd = path.resolve(`${__dirname}/../../../`);
|
||||
const models: ISequelizeModels = {};
|
||||
|
||||
// Get all models
|
||||
const globOptions = {
|
||||
cwd,
|
||||
nocase: true,
|
||||
nodir: true,
|
||||
absolute: true,
|
||||
};
|
||||
|
||||
glob.sync("**/*.model.{js,ts}", globOptions).forEach(function (file) {
|
||||
console.log(`>> ${file}`);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const modelDef = require(path.join(file)).default;
|
||||
const model =
|
||||
typeof modelDef === "function" ? modelDef(connection, DataTypes) : false;
|
||||
if (model) models[model.name] = model;
|
||||
});
|
||||
|
||||
// Register revisions models
|
||||
const [Revision, RevisionChanges] = sequelizeRevision.defineModels();
|
||||
//models[Revision.name] = Revision;
|
||||
//models[RevisionChanges.name] = RevisionChanges;
|
||||
|
||||
for (const modelName in models) {
|
||||
const model = models[modelName];
|
||||
|
||||
if (model.trackRevision) {
|
||||
model.trackRevision(connection, sequelizeRevision);
|
||||
}
|
||||
|
||||
if (model.associate) {
|
||||
model.associate(connection, models);
|
||||
}
|
||||
|
||||
if (model.hooks) {
|
||||
model.hooks(connection);
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { Sequelize, Transaction } from "sequelize";
|
||||
import { TBusinessTransaction } from "../../domain/repositories";
|
||||
import { InfrastructureError } from "../InfrastructureError";
|
||||
|
||||
export type SequelizeBusinessTransactionType = TBusinessTransaction & {
|
||||
start(): void;
|
||||
complete<T>(work: (t: Transaction) => Promise<T>): Promise<T>;
|
||||
};
|
||||
|
||||
export class SequelizeBusinessTransaction
|
||||
implements SequelizeBusinessTransactionType
|
||||
{
|
||||
private _connection: Sequelize;
|
||||
|
||||
constructor(connection: Sequelize) {
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
public async complete<T>(work: (t: Transaction) => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await this._connection.transaction(work);
|
||||
} catch (error: unknown) {
|
||||
//error instanceof BaseError;
|
||||
|
||||
/*
|
||||
{
|
||||
name: "SequelizeValidationError",
|
||||
errors: [
|
||||
{
|
||||
message: "Customer.entity_type cannot be null",
|
||||
type: "notNull Violation",
|
||||
path: "entity_type",
|
||||
value: null,
|
||||
origin: "CORE",
|
||||
instance: {
|
||||
dataValues: {
|
||||
id: "85ac4089-6ad7-4058-a16a-adf7fbbfe388",
|
||||
created_at: "2023-08-02T10:42:49.248Z",
|
||||
},
|
||||
...
|
||||
...
|
||||
},
|
||||
isNewRecord: true,
|
||||
},
|
||||
validatorKey: "is_null",
|
||||
validatorName: null,
|
||||
validatorArgs: [
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
*/
|
||||
|
||||
throw InfrastructureError.create(
|
||||
InfrastructureError.UNEXCEPTED_ERROR,
|
||||
(error as Error).message
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { Model, Sequelize } from "sequelize";
|
||||
import { SequelizeRevision } from "sequelize-revision";
|
||||
|
||||
interface ISequelizeModel extends Model {}
|
||||
|
||||
interface ISequelizeModels {
|
||||
[prop: string]: ISequelizeModel;
|
||||
}
|
||||
interface ISequelizeModel extends Model {
|
||||
associate?: (connection: Sequelize, models?: ISequelizeModels) => void;
|
||||
hooks?: (connection: Sequelize) => void;
|
||||
trackRevision?: (
|
||||
connection: Sequelize,
|
||||
sequelizeRevision: SequelizeRevision<any>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export { ISequelizeModel, ISequelizeModels };
|
||||
@ -0,0 +1,266 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
|
||||
import { ModelDefined, Transaction } from "sequelize";
|
||||
|
||||
import { IRepository } from "../../domain/repositories";
|
||||
import { ISequelizeAdapter } from "./SequelizeAdapter";
|
||||
import { ISequelizeQueryBuilder } from "./queryBuilder/SequelizeQueryBuilder";
|
||||
|
||||
export abstract class SequelizeRepository<T> implements IRepository<T> {
|
||||
protected queryBuilder: ISequelizeQueryBuilder;
|
||||
protected transaction: Transaction;
|
||||
protected adapter: ISequelizeAdapter;
|
||||
public constructor(props: {
|
||||
adapter: ISequelizeAdapter;
|
||||
transaction: Transaction;
|
||||
}) {
|
||||
this.adapter = props.adapter;
|
||||
this.transaction = props.transaction;
|
||||
this.queryBuilder = this.adapter.queryBuilder;
|
||||
}
|
||||
|
||||
protected getById(id: UniqueID): Promise<T | null> {
|
||||
throw new Error("[SequelizeRepository] getById not implemented!");
|
||||
}
|
||||
|
||||
/*protected getBy(field: string, value: any): Promise<T> {
|
||||
throw new Error('[SequelizeRepository] getBy not implemented!');
|
||||
}*/
|
||||
|
||||
protected findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<T>> {
|
||||
throw new Error("[SequelizeRepository] findAll not implemented!");
|
||||
}
|
||||
|
||||
/*protected totalCount(queryCriteria?: IQueryCriteria): Promise<number> {
|
||||
throw new Error('[SequelizeRepository] totalCount not implemented!');
|
||||
}*/
|
||||
|
||||
protected save(t: T): Promise<void> {
|
||||
throw new Error("[SequelizeRepository] save not implemented!");
|
||||
}
|
||||
|
||||
/* protected remove(t: T): Promise<boolean> {
|
||||
throw new Error('[SequelizeRepository] remove not implemented!');
|
||||
}*/
|
||||
|
||||
protected removeById(id: UniqueID): Promise<void> {
|
||||
throw new Error("[SequelizeRepository] removeById not implemented!");
|
||||
}
|
||||
|
||||
protected async _getBy(
|
||||
modelName: string,
|
||||
field: string,
|
||||
value: any,
|
||||
params: any = {}
|
||||
): Promise<T> {
|
||||
const _model = this.adapter.getModel(modelName);
|
||||
const where: { [key: string]: any } = {};
|
||||
|
||||
where[field] = value;
|
||||
|
||||
return _model.findOne({
|
||||
where,
|
||||
transaction: this.transaction,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getById(
|
||||
modelName: string,
|
||||
id: UniqueID | string,
|
||||
params: any = {}
|
||||
): Promise<T> {
|
||||
const _model = this.adapter.getModel(modelName);
|
||||
return _model.findByPk(id.toString(), params);
|
||||
}
|
||||
|
||||
protected async _findAll(
|
||||
modelName: string,
|
||||
queryCriteria?: IQueryCriteria,
|
||||
params: any = {}
|
||||
): Promise<{ rows: any[]; count: number }> {
|
||||
console.time("_findAll");
|
||||
|
||||
const { model: _model, query } = this.queryBuilder.generateQuery({
|
||||
model: this.adapter.getModel(modelName),
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
if (!_model) {
|
||||
throw new Error(`[SequelizeRepository] Model ${modelName} not found!`);
|
||||
}
|
||||
|
||||
const args = {
|
||||
...query,
|
||||
distinct: true,
|
||||
transaction: this.transaction,
|
||||
...params,
|
||||
};
|
||||
|
||||
const result = _model.findAndCountAll(args);
|
||||
|
||||
console.timeEnd("_findAll");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async _exists(
|
||||
modelName: string,
|
||||
field: string,
|
||||
value: any,
|
||||
params: any = {}
|
||||
): Promise<boolean> {
|
||||
const _model = this.adapter.getModel(modelName);
|
||||
const where = {};
|
||||
where[field] = value;
|
||||
|
||||
const count: number = await _model.count({
|
||||
where,
|
||||
transaction: this.transaction,
|
||||
});
|
||||
|
||||
return Promise.resolve(Boolean(count !== 0));
|
||||
}
|
||||
|
||||
protected async _save(
|
||||
modelName: string,
|
||||
id: UniqueID,
|
||||
data: any,
|
||||
params: any = {}
|
||||
): Promise<void> {
|
||||
const _model = this.adapter.getModel(modelName);
|
||||
|
||||
if (await this._exists(modelName, "id", id.toString())) {
|
||||
await _model.update(
|
||||
{
|
||||
...data,
|
||||
id: undefined,
|
||||
},
|
||||
{
|
||||
where: { id: id.toString() },
|
||||
transaction: this.transaction,
|
||||
...params,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await _model.create(
|
||||
{
|
||||
...data,
|
||||
id: id.toString(),
|
||||
},
|
||||
{
|
||||
include: [{ all: true }],
|
||||
transaction: this.transaction,
|
||||
...params,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async _removeById(
|
||||
modelName: string,
|
||||
id: UniqueID,
|
||||
force: boolean = false,
|
||||
params: any = {}
|
||||
): Promise<void> {
|
||||
const model: ModelDefined<any, any> = this.adapter.getModel(modelName);
|
||||
|
||||
await model.destroy({
|
||||
where: {
|
||||
id: id.toString(),
|
||||
},
|
||||
transaction: this.transaction,
|
||||
force,
|
||||
logging: console.log,
|
||||
});
|
||||
}
|
||||
|
||||
/*protected _totalCount(
|
||||
modelName: string,
|
||||
queryCriteria?: IQueryCriteria
|
||||
): Promise<number> {
|
||||
const { model: _model, query } = this.queryBuilder.generateQuery({
|
||||
model: this.adapter.getModel(modelName),
|
||||
queryCriteria,
|
||||
});
|
||||
|
||||
return _model.count({
|
||||
distinct: true,
|
||||
...query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected async _removeByIds(
|
||||
modelName: string,
|
||||
ids: UniqueID[] | string[]
|
||||
): Promise<boolean> {
|
||||
const _ids = ids.map((id: UniqueID | string) => id.toString());
|
||||
|
||||
const destroyedRows: Promise<number> = await this.adapter.models[
|
||||
modelName
|
||||
].destroy({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: _ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!destroyedRows;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
protected _debugModelInfo(model) {
|
||||
if (!model.name)
|
||||
return;
|
||||
|
||||
console.log("\n\n----------------------------------\n",
|
||||
model.name,
|
||||
"\n----------------------------------");
|
||||
|
||||
console.log("\nAttributes");
|
||||
console.log("\n----------------------------------\n");
|
||||
if (model._options.attributes) {
|
||||
model._options.attributes.forEach(attr => console.log(model.name + '.' + attr));
|
||||
}
|
||||
|
||||
console.log("\nAssociations");
|
||||
console.log("\n----------------------------------\n");
|
||||
if (model._options.includeNames) {
|
||||
const names: [string] = model._options.includeNames;
|
||||
const map: [] = model._options.includeMap;
|
||||
|
||||
names.forEach((name: string) => {
|
||||
console.log('\nas: ', map[name].association.as, 'type: ', map[name].association.associationType);
|
||||
console.log("----------------------------------\n");
|
||||
const accessors = map[name].association.accessors;
|
||||
for (const accessor of Object.keys(accessors)) {
|
||||
console.log(accessor, ' => ', map[name].association.accessors[accessor]);
|
||||
//console.log(model.name + '.' + model.associations[assoc].accessors[accessor] + '()');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (model.Instance && model.Instance.super_) {
|
||||
console.log("\nCommon");
|
||||
for (const func of Object.keys(model.Instance.super_.prototype)) {
|
||||
if (func === 'constructor' || func === 'sequelize')
|
||||
continue;
|
||||
console.log(model.name + '.' + func + '()');
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n\n----------------------------------\n",
|
||||
"END",
|
||||
"\n----------------------------------");
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
*/
|
||||
}
|
||||
12
server/src/contexts/common/infrastructure/sequelize/index.ts
Normal file
12
server/src/contexts/common/infrastructure/sequelize/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ISequelizeAdapter, SequelizeAdapter } from "./SequelizeAdapter";
|
||||
import { createSequelizeQueryBuilder } from "./queryBuilder";
|
||||
|
||||
const createSequelizeAdapter = () => {
|
||||
return SequelizeAdapter.getInstance({
|
||||
queryBuilder: createSequelizeQueryBuilder(),
|
||||
});
|
||||
};
|
||||
|
||||
export { ISequelizeAdapter, createSequelizeAdapter };
|
||||
|
||||
export * from "./SequelizeRepository";
|
||||
@ -0,0 +1,123 @@
|
||||
import Sequelize = require("sequelize");
|
||||
|
||||
// https://github.com/Hodor9898/sequelize-query-builder/blob/master/index.ts
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
|
||||
export enum CONNECTING_OPERATORS {
|
||||
OR = "OR",
|
||||
AND = "AND",
|
||||
}
|
||||
|
||||
export enum OPERATORS {
|
||||
EQ = "EQ",
|
||||
NOT = "NOT",
|
||||
IN = "IN",
|
||||
NOTIN = "NOTIN",
|
||||
BETWEEN = "BETWEEN",
|
||||
NOTBETWEEN = "NOTBETWEEN",
|
||||
LT = "LT",
|
||||
LTE = "LTE",
|
||||
GT = "GT",
|
||||
GTE = "GTE",
|
||||
NULL = "NULL",
|
||||
LIKE = "LIKE",
|
||||
NOTLIKE = "NOTLIKE",
|
||||
}
|
||||
|
||||
export enum FUNCTIONS {
|
||||
INCLUDES = "INCLUDES",
|
||||
}
|
||||
|
||||
const SEQUELIZE_OP_MAP: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
[CONNECTING_OPERATORS.OR]: Op.or,
|
||||
[CONNECTING_OPERATORS.AND]: Op.and,
|
||||
[OPERATORS.EQ]: Op.eq,
|
||||
[OPERATORS.NOT]: Op.not,
|
||||
[OPERATORS.IN]: Op.in,
|
||||
[OPERATORS.NOTIN]: Op.notIn,
|
||||
[OPERATORS.BETWEEN]: Op.between,
|
||||
[OPERATORS.NOTBETWEEN]: Op.notBetween,
|
||||
[OPERATORS.LT]: Op.lt,
|
||||
[OPERATORS.LTE]: Op.lte,
|
||||
[OPERATORS.GT]: Op.gt,
|
||||
[OPERATORS.GTE]: Op.gte,
|
||||
[OPERATORS.NULL]: Op.is,
|
||||
[OPERATORS.LIKE]: Op.like,
|
||||
[OPERATORS.NOTLIKE]: Op.notLike,
|
||||
};
|
||||
|
||||
const SEQUELIZE_FN_MAP: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
[FUNCTIONS.INCLUDES]: (field: string, val: string) =>
|
||||
Sequelize.where(
|
||||
Sequelize.fn(
|
||||
"FIND_IN_SET",
|
||||
Sequelize.literal(`'${val}'`),
|
||||
Sequelize.col(field),
|
||||
),
|
||||
SEQUELIZE_OP_MAP[OPERATORS.GT],
|
||||
0,
|
||||
),
|
||||
};
|
||||
|
||||
const OPERATOR_VALUE_TRANSFORMER: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
[OPERATORS.LIKE]: (val: string) => `%${val}%`,
|
||||
[OPERATORS.NOTLIKE]: (val: string) => `%${val}%`,
|
||||
[OPERATORS.IN]: (val: string) => val.split("-."),
|
||||
[OPERATORS.BETWEEN]: (val: string) => val,
|
||||
};
|
||||
|
||||
export type FilterObject = {
|
||||
operator: string;
|
||||
field: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export class SequelizeParseFilter {
|
||||
static parseFilter(filterRoot: Partial<FilterObject>): any {
|
||||
if (filterRoot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { operator = null, field = null, value = null } = filterRoot;
|
||||
|
||||
if (operator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const _op: any =
|
||||
CONNECTING_OPERATORS[operator as keyof typeof CONNECTING_OPERATORS] ||
|
||||
OPERATORS[operator as keyof typeof OPERATORS] ||
|
||||
FUNCTIONS[operator as keyof typeof FUNCTIONS];
|
||||
|
||||
if (Object.values(CONNECTING_OPERATORS).includes(_op)) {
|
||||
return {
|
||||
[SEQUELIZE_OP_MAP[operator]]: value.map((val: any) => ({
|
||||
...SequelizeParseFilter.parseFilter(val),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.values(FUNCTIONS).includes(_op) && field !== null) {
|
||||
return [SEQUELIZE_FN_MAP[_op](field, value)];
|
||||
}
|
||||
|
||||
if (Object.values(OPERATORS).includes(_op) && field !== null) {
|
||||
return {
|
||||
[field]: {
|
||||
[SEQUELIZE_OP_MAP[operator]]: OPERATOR_VALUE_TRANSFORMER[operator]
|
||||
? OPERATOR_VALUE_TRANSFORMER[operator](value)
|
||||
: value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Filter operator ${operator} is invalid!`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// https://github.com/Hodor9898/sequelize-query-builder/blob/master/index.ts
|
||||
|
||||
import { IOrder, IOrderCollection } from "@shared/contexts";
|
||||
|
||||
export class SequelizeParseOrder {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
static parseOrder(orderCollection: IOrderCollection): any {
|
||||
if (orderCollection.totalCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return orderCollection.items.map((item: IOrder) => {
|
||||
if (!item.field) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [model, field] = String(item.field).split(".");
|
||||
|
||||
return [
|
||||
//field ? model : undefined,
|
||||
field ? field : model,
|
||||
item.type === "-" ? "DESC" : "ASC",
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
import { ModelDefined } from "sequelize";
|
||||
|
||||
import {
|
||||
IRepositoryQueryBuilder,
|
||||
IRepositoryQueryOptions,
|
||||
} from "@/contexts/common/domain/repositories";
|
||||
|
||||
import {
|
||||
FilterCriteria,
|
||||
IQueryCriteria,
|
||||
OffsetPaging,
|
||||
OrderCriteria,
|
||||
QuickSearchCriteria,
|
||||
} from "@shared/contexts";
|
||||
import { SequelizeParseFilter } from "./SequelizeParseFilter";
|
||||
import { SequelizeParseOrder } from "./SequelizeParseOrder";
|
||||
|
||||
export interface ISequelizeQueryOptions extends IRepositoryQueryOptions {
|
||||
model: ModelDefined<any, any>;
|
||||
}
|
||||
|
||||
export interface ISequelizeQueryBuilder extends IRepositoryQueryBuilder {
|
||||
generateQuery: (props: {
|
||||
model: any;
|
||||
queryCriteria?: IQueryCriteria;
|
||||
}) => ISequelizeQueryOptions;
|
||||
}
|
||||
|
||||
export class SequelizeQueryBuilder implements ISequelizeQueryBuilder {
|
||||
public static create() {
|
||||
return new SequelizeQueryBuilder();
|
||||
}
|
||||
|
||||
private applyPagination(pagination: OffsetPaging): any {
|
||||
const limit = pagination.limit;
|
||||
const offset = pagination.offset * limit;
|
||||
|
||||
return {
|
||||
offset,
|
||||
limit,
|
||||
subQuery: false, // <- https://selleo.com/til/posts/ddesmudzmi-offset-pagination-with-subquery-in-sequelize-
|
||||
};
|
||||
}
|
||||
|
||||
private applyQuickSearch(
|
||||
model: ModelDefined<any, any>,
|
||||
quickSearchCriteria: QuickSearchCriteria
|
||||
): any {
|
||||
let _model = model;
|
||||
if (!quickSearchCriteria.isEmpty()) {
|
||||
if (
|
||||
_model &&
|
||||
_model.options.scopes &&
|
||||
_model.options.scopes["quickSearch"]
|
||||
) {
|
||||
_model = _model.scope({
|
||||
method: ["quickSearch", quickSearchCriteria.value],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return _model;
|
||||
}
|
||||
|
||||
private applyFilters(filterCriteria: FilterCriteria): any {
|
||||
let where = undefined;
|
||||
|
||||
if (!filterCriteria.isEmpty()) {
|
||||
const filterRoot = filterCriteria.getFilterRoot();
|
||||
|
||||
where = SequelizeParseFilter.parseFilter(filterRoot);
|
||||
}
|
||||
|
||||
return {
|
||||
where,
|
||||
};
|
||||
}
|
||||
|
||||
private applyOrder(orderCriteria: OrderCriteria): any {
|
||||
let order = [];
|
||||
|
||||
if (!orderCriteria.isEmpty()) {
|
||||
const orderCollection = orderCriteria.getOrderCollection();
|
||||
|
||||
order = SequelizeParseOrder.parseOrder(orderCollection);
|
||||
}
|
||||
|
||||
return {
|
||||
order,
|
||||
};
|
||||
}
|
||||
|
||||
public generateQuery(props: {
|
||||
model: ModelDefined<any, any>;
|
||||
queryCriteria?: IQueryCriteria;
|
||||
}): ISequelizeQueryOptions {
|
||||
const { model, queryCriteria } = props;
|
||||
|
||||
let _model = model;
|
||||
|
||||
const defaultOptions: any = {
|
||||
include: [
|
||||
{
|
||||
all: true,
|
||||
|
||||
// Ejecutar consultas de forma separada para poder paginar
|
||||
separate: true,
|
||||
|
||||
// Poder referenciar cualquier campo de los joins. P.e.: %emailAddresses.value%
|
||||
// Al activar esto sale el error: "Only HasMany associations support include.separate"
|
||||
// nested: true,
|
||||
|
||||
duplicating: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let paginateOptions = {};
|
||||
let whereOptions = {};
|
||||
let orderOptions = {};
|
||||
|
||||
if (queryCriteria) {
|
||||
// Paginate
|
||||
if (queryCriteria.pagination) {
|
||||
paginateOptions = this.applyPagination(queryCriteria.pagination);
|
||||
}
|
||||
|
||||
// QuickSearch
|
||||
if (queryCriteria.quickSearch) {
|
||||
_model = this.applyQuickSearch(_model, queryCriteria.quickSearch);
|
||||
}
|
||||
|
||||
// Filters
|
||||
if (queryCriteria.filters) {
|
||||
whereOptions = this.applyFilters(queryCriteria.filters);
|
||||
}
|
||||
|
||||
// Order
|
||||
if (queryCriteria.order) {
|
||||
orderOptions = this.applyOrder(queryCriteria.order);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
model: _model,
|
||||
query: {
|
||||
...defaultOptions,
|
||||
...paginateOptions,
|
||||
...whereOptions,
|
||||
...orderOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { SequelizeQueryBuilder } from "./SequelizeQueryBuilder";
|
||||
|
||||
const createSequelizeQueryBuilder = () => SequelizeQueryBuilder.create();
|
||||
|
||||
export { createSequelizeQueryBuilder };
|
||||
2
server/src/index.ts
Normal file
2
server/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Infra
|
||||
import "./infrastructure/http/server";
|
||||
9
server/src/infrastructure/express/api/v1.ts
Normal file
9
server/src/infrastructure/express/api/v1.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import express from "express";
|
||||
|
||||
const v1Router = express.Router({ mergeParams: true });
|
||||
|
||||
v1Router.get("/hello", (req, res) => {
|
||||
res.send("Hello world!");
|
||||
});
|
||||
|
||||
export { v1Router };
|
||||
57
server/src/infrastructure/express/app.ts
Normal file
57
server/src/infrastructure/express/app.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import rTracer from "cls-rtracer";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import morgan from "morgan";
|
||||
import responseTime from "response-time";
|
||||
|
||||
import { initLogger } from "../logger";
|
||||
import { v1Router } from "./api/v1";
|
||||
|
||||
const logger = initLogger(rTracer);
|
||||
|
||||
// Create Express server
|
||||
const app = express();
|
||||
|
||||
app.use(rTracer.expressMiddleware());
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json());
|
||||
app.use(express.text());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// set up the response-time middleware
|
||||
app.use(responseTime());
|
||||
|
||||
// enable CORS - Cross Origin Resource Sharing
|
||||
app.use(
|
||||
cors({
|
||||
origin: "http://localhost:5173",
|
||||
credentials: true,
|
||||
|
||||
exposedHeaders: [
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Content-Disposition",
|
||||
"Content-Type",
|
||||
"Content-Length",
|
||||
"X-Total-Count",
|
||||
"Pagination-Count",
|
||||
"Pagination-Page",
|
||||
"Pagination-Limit",
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// secure apps by setting various HTTP headers
|
||||
app.use(helmet());
|
||||
|
||||
// request logging. dev: console | production: file
|
||||
//app.use(morgan('common'));
|
||||
app.use(morgan("dev"));
|
||||
|
||||
// Express configuration
|
||||
app.set("port", process.env.PORT ?? 3000);
|
||||
|
||||
app.use("/api/v1", v1Router);
|
||||
|
||||
export default app;
|
||||
144
server/src/infrastructure/http/server.ts
Normal file
144
server/src/infrastructure/http/server.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import rTracer from "cls-rtracer";
|
||||
import http from "http";
|
||||
import { assign } from "lodash";
|
||||
import { DateTime, Settings } from "luxon";
|
||||
|
||||
import { createFirebirdAdapter } from "@/contexts/common/infrastructure/firebird";
|
||||
import { createSequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { trace } from "console";
|
||||
import { config } from "../../config";
|
||||
import app from "../express/app";
|
||||
import { initLogger } from "../logger";
|
||||
|
||||
process.env.TZ = "UTC";
|
||||
Settings.defaultLocale = "es-ES";
|
||||
Settings.defaultZone = "utc";
|
||||
|
||||
const logger = initLogger(rTracer);
|
||||
|
||||
export const currentState = assign(
|
||||
{
|
||||
launchedAt: DateTime.now(),
|
||||
appPath: process.cwd(),
|
||||
//host: process.env.HOST || process.env.HOSTNAME || 'localhost',
|
||||
//port: process.env.PORT || 18888,
|
||||
environment: config.enviroment,
|
||||
connections: {},
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
const serverStop = (server: http.Server) => {
|
||||
const forceTimeout = 30000;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
logger.warn("⚡️ Shutting down server");
|
||||
|
||||
setTimeout(() => {
|
||||
logger.error(
|
||||
"Could not close connections in time, forcefully shutting down"
|
||||
);
|
||||
resolve();
|
||||
}, forceTimeout).unref();
|
||||
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
logger.info("Closed out remaining connections.");
|
||||
logger.info("Bye!");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
/*const now = DateTime.now();
|
||||
// Destroy server and available connections.
|
||||
logger.info(`Time: ${now.toLocaleString()}`);
|
||||
logger.info('Shutting down at: ' + new Date());
|
||||
if (server) {
|
||||
server.close();
|
||||
}
|
||||
|
||||
logger.info('Bye!');
|
||||
process.exit(1);*/
|
||||
};
|
||||
|
||||
const serverError = (error: any) => {
|
||||
if (error.code === "EADDRINUSE") {
|
||||
logger.debug(`⛔️ Server wasn't able to start properly.`);
|
||||
logger.error(
|
||||
`The port ${error.port} is already used by another application.`
|
||||
);
|
||||
} else {
|
||||
logger.debug(`⛔️ Server wasn't able to start properly.`);
|
||||
logger.error(error);
|
||||
trace(error);
|
||||
}
|
||||
|
||||
serverStop(server);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
const serverConnection = (conn: any) => {
|
||||
const key = `${conn.remoteAddress}:${conn.remotePort}`;
|
||||
currentState.connections[key] = conn;
|
||||
|
||||
logger.debug(currentState.connections);
|
||||
|
||||
conn.on("close", () => {
|
||||
delete currentState.connections[key];
|
||||
});
|
||||
};
|
||||
|
||||
const sequelizeConn = createSequelizeAdapter();
|
||||
const firebirdConn = createFirebirdAdapter();
|
||||
|
||||
const server: http.Server = http
|
||||
.createServer(app)
|
||||
.once("listening", () =>
|
||||
process.on("SIGINT", () => {
|
||||
firebirdConn.disconnect();
|
||||
serverStop(server);
|
||||
})
|
||||
)
|
||||
.on("close", () =>
|
||||
logger.info(
|
||||
`Shut down at: ${DateTime.now().toLocaleString(DateTime.DATETIME_FULL)}`
|
||||
)
|
||||
)
|
||||
.on("connection", serverConnection)
|
||||
.on("error", serverError);
|
||||
|
||||
try {
|
||||
firebirdConn.sync().then(() => {
|
||||
sequelizeConn.sync({ force: false, alter: true }).then(() => {
|
||||
// Launch server
|
||||
server.listen(currentState.server.port, () => {
|
||||
const now = DateTime.now();
|
||||
logger.info(
|
||||
`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`
|
||||
);
|
||||
logger.info(
|
||||
`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`
|
||||
);
|
||||
logger.info(`Environment: ${currentState.environment}`);
|
||||
logger.info(`Process PID: ${process.pid}`);
|
||||
logger.info("To shut down your server, press <CTRL> + C at any time");
|
||||
logger.info(
|
||||
`⚡️ Server: http://${currentState.server.hostname}:${currentState.server.port}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
serverError(error);
|
||||
}
|
||||
|
||||
process.on("uncaughtException", (error: any) => {
|
||||
logger.error(`${new Date().toUTCString()} uncaughtException:`, error.message);
|
||||
logger.error(error.stack);
|
||||
//process.exit(1);
|
||||
});
|
||||
85
server/src/infrastructure/logger/index.ts
Normal file
85
server/src/infrastructure/logger/index.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import path from "path";
|
||||
import { createLogger, format, transports } from "winston";
|
||||
import DailyRotateFile from "winston-daily-rotate-file";
|
||||
import { config } from "../../config";
|
||||
|
||||
function initLogger(rTracer) {
|
||||
// a custom format that outputs request id
|
||||
|
||||
const consoleFormat = format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp(),
|
||||
format.align(),
|
||||
format.splat(),
|
||||
format.printf((info) => {
|
||||
const rid = rTracer.id();
|
||||
|
||||
let out =
|
||||
config.isProduction && rid
|
||||
? `${info.timestamp} [request-id:${rid}] - ${info.level}: [${info.label}]: ${info.message}`
|
||||
: `${info.timestamp} - ${info.level}: [${info.label}]: ${info.message}`;
|
||||
|
||||
if (info.metadata?.error) {
|
||||
out = `${out} ${info.metadata.error}`;
|
||||
if (info.metadata?.error?.stack) {
|
||||
out = `${out} ${info.metadata.error.stack}`;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}),
|
||||
);
|
||||
|
||||
const fileFormat = format.combine(
|
||||
format.timestamp(),
|
||||
format.splat(),
|
||||
format.label({ label: path.basename(String(require.main?.filename)) }),
|
||||
//format.metadata(),
|
||||
format.metadata({ fillExcept: ["message", "level", "timestamp", "label"] }),
|
||||
format.simple(),
|
||||
format.json(),
|
||||
);
|
||||
|
||||
const logger = createLogger({
|
||||
level: process.env.NODE_ENV === "production" ? "info" : "debug",
|
||||
|
||||
format: fileFormat,
|
||||
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
filename: "error-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
utc: true,
|
||||
level: "error",
|
||||
maxSize: "5m",
|
||||
maxFiles: "1d",
|
||||
}),
|
||||
new DailyRotateFile({
|
||||
filename: "debug-%DATE%.log",
|
||||
datePattern: "YYYY-MM-DD",
|
||||
utc: true,
|
||||
level: "debug",
|
||||
maxSize: "5m",
|
||||
maxFiles: "1d",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console` with the format:
|
||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||
//
|
||||
if (!config.isProduction) {
|
||||
logger.add(
|
||||
new transports.Console({
|
||||
format: consoleFormat,
|
||||
level: "debug",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
export { initLogger };
|
||||
12
server/tsconfig.eslint.json
Normal file
12
server/tsconfig.eslint.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest"],
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
81
server/tsconfig.json
Normal file
81
server/tsconfig.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
] /* Specify library files to be included in the compilation. */,
|
||||
|
||||
"allowJs": false /* Allow javascript files to be compiled. */,
|
||||
"pretty": true,
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true /* Generates corresponding '.map' file. */,
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "../dist/" /* Redirect output structure to the directory. */,
|
||||
//"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
"removeComments": true /* Do not emit comments to output. */,
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"skipLibCheck": false /* Skip type checking of declaration files. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"strictNullChecks": true /* Enable strict null checks. */,
|
||||
"strictFunctionTypes": true /* Enable strict checking of function types. */,
|
||||
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
|
||||
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": false /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": false /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||
//"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
|
||||
"paths": {
|
||||
/* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/lib/*"]
|
||||
},
|
||||
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [] /* List of folders to include type definitions from. */,
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
"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,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
||||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
|
||||
|
||||
/* Advanced Options */
|
||||
"resolveJsonModule": true /* Include modules imported with '.json' extension */,
|
||||
"suppressImplicitAnyIndexErrors": false
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/__tests__/*",
|
||||
"src/**/*.mock.*",
|
||||
"src/**/*.test.*",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
21
shared/.eslintrc.json
Normal file
21
shared/.eslintrc.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"prettier",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
10
shared/.prettierc.json
Normal file
10
shared/.prettierc.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": true,
|
||||
"printWidth": 80,
|
||||
"useTabs": false,
|
||||
"endOfLine": "auto",
|
||||
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { IMoney_Response_DTO } from "../../../../common";
|
||||
|
||||
export interface IListProducts_Response_DTO {
|
||||
id: string;
|
||||
reference: string;
|
||||
family: string;
|
||||
subfamily: string;
|
||||
description: string;
|
||||
points: number;
|
||||
pvp: IMoney_Response_DTO;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./IListProducts_Response.dto";
|
||||
1
shared/lib/contexts/catalog/application/dto/index.ts
Normal file
1
shared/lib/contexts/catalog/application/dto/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./IListProducts.dto";
|
||||
1
shared/lib/contexts/catalog/application/index.ts
Normal file
1
shared/lib/contexts/catalog/application/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./dto";
|
||||
1
shared/lib/contexts/catalog/index.ts
Normal file
1
shared/lib/contexts/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./application";
|
||||
@ -0,0 +1,20 @@
|
||||
export interface IError_Response_DTO {
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
status: number;
|
||||
title: string;
|
||||
type?: string;
|
||||
context: IErrorContext_Response_DTO;
|
||||
extra: IErrorExtra_Response_DTO;
|
||||
}
|
||||
|
||||
export interface IErrorContext_Response_DTO {
|
||||
user?: unknown;
|
||||
params?: Record<string, any>;
|
||||
query?: Record<string, any>;
|
||||
body?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface IErrorExtra_Response_DTO {
|
||||
errors: Record<string, any>[];
|
||||
}
|
||||
7
shared/lib/contexts/common/application/dto/IMoney.dto.ts
Normal file
7
shared/lib/contexts/common/application/dto/IMoney.dto.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface IMoney_DTO {
|
||||
amount: number;
|
||||
precision: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface IMoney_Response_DTO extends IMoney_DTO {}
|
||||
@ -0,0 +1,6 @@
|
||||
export interface IPercentage_DTO {
|
||||
amount: number;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
export interface IPercentage_Response_DTO extends IPercentage_DTO {}
|
||||
11
shared/lib/contexts/common/application/dto/ITaxType.dto.ts
Normal file
11
shared/lib/contexts/common/application/dto/ITaxType.dto.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { IPercentage_DTO } from "./IPercentage.dto";
|
||||
|
||||
export interface ITaxType_DTO {
|
||||
id: string;
|
||||
type_code: string,
|
||||
tax_slug: string,
|
||||
tax_rate: IPercentage_DTO,
|
||||
equivalence_surcharge: IPercentage_DTO,
|
||||
}
|
||||
|
||||
export interface ITaxType_Response_DTO extends ITaxType_DTO {}
|
||||
4
shared/lib/contexts/common/application/dto/index.ts
Normal file
4
shared/lib/contexts/common/application/dto/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./IError_Response.dto";
|
||||
export * from "./IMoney.dto";
|
||||
export * from "./IPercentage.dto";
|
||||
export * from "./ITaxType.dto";
|
||||
1
shared/lib/contexts/common/application/index.ts
Normal file
1
shared/lib/contexts/common/application/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./dto";
|
||||
6
shared/lib/contexts/common/domain/EntityError.ts
Normal file
6
shared/lib/contexts/common/domain/EntityError.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class EntityError extends Error {
|
||||
constructor(field: string, message?: string) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
25
shared/lib/contexts/common/domain/IListResponse.dto.ts
Normal file
25
shared/lib/contexts/common/domain/IListResponse.dto.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export interface IListResponse_DTO<T> {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
total_items: number;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export const IsResponseAListDTO = <T>(
|
||||
response: any
|
||||
): response is IListResponse_DTO<T> => {
|
||||
return (
|
||||
typeof response === "object" &&
|
||||
response !== null &&
|
||||
response.hasOwnProperty("total_items")
|
||||
);
|
||||
};
|
||||
|
||||
export const existsMoreReponsePages = <T>(
|
||||
response: any
|
||||
): response is IListResponse_DTO<T> => {
|
||||
return (
|
||||
IsResponseAListDTO(response) && response.page + 1 < response.total_pages
|
||||
);
|
||||
};
|
||||
66
shared/lib/contexts/common/domain/RuleValidator.ts
Normal file
66
shared/lib/contexts/common/domain/RuleValidator.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import Joi, { ValidationError } from "joi";
|
||||
import { Result } from "./entities/Result";
|
||||
|
||||
export type TRuleValidatorResult<T> = Result<T, ValidationError>;
|
||||
|
||||
export class RuleValidator {
|
||||
public static readonly RULE_NOT_NULL_OR_UNDEFINED = Joi.any()
|
||||
.required() // <- undefined
|
||||
.invalid(null); // <- null
|
||||
|
||||
public static readonly RULE_ALLOW_NULL_OR_UNDEFINED = Joi.any()
|
||||
.optional() // <- undefined
|
||||
.valid(null); // <- null
|
||||
|
||||
public static readonly RULE_ALLOW_NULL = Joi.any().valid(null); // <- null
|
||||
|
||||
public static readonly RULE_ALLOW_EMPTY = Joi.any()
|
||||
.optional() // <- undefined
|
||||
.valid(null, ""); //
|
||||
|
||||
public static readonly RULE_IS_TYPE_STRING = Joi.string();
|
||||
public static readonly RULE_IS_TYPE_NUMBER = Joi.number();
|
||||
|
||||
public static validate<T>(
|
||||
rule: Joi.AnySchema<any> | Joi.AnySchema<any>[],
|
||||
value: any,
|
||||
options: Joi.ValidationOptions = {}
|
||||
): TRuleValidatorResult<T> {
|
||||
if (!Joi.isSchema(rule)) {
|
||||
throw new RuleValidator_Error("Rule provided is not a valid Joi schema!");
|
||||
}
|
||||
|
||||
const _options: Joi.ValidationOptions = {
|
||||
abortEarly: false,
|
||||
errors: {
|
||||
wrap: {
|
||||
label: "{}",
|
||||
},
|
||||
},
|
||||
//messages: SpanishJoiMessages,
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = rule.validate(value, _options);
|
||||
|
||||
if (validationResult.error) {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
return Result.ok<T>(validationResult.value);
|
||||
}
|
||||
|
||||
public static validateFnc(ruleFnc: (value: any) => any) {
|
||||
return (value: any, helpers) => {
|
||||
const result = ruleFnc(value);
|
||||
console.log(value);
|
||||
return result.isSuccess
|
||||
? value
|
||||
: helpers.message({
|
||||
custom: result.error.message,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class RuleValidator_Error extends Error {}
|
||||
@ -0,0 +1,51 @@
|
||||
import Joi from "joi";
|
||||
|
||||
import { UndefinedOr } from "../../../../../utilities";
|
||||
|
||||
import { RuleValidator } from "../../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { Result } from "../Result";
|
||||
import {
|
||||
IStringValueObjectOptions,
|
||||
StringValueObject,
|
||||
} from "../StringValueObject";
|
||||
|
||||
export class AddressTitle extends StringValueObject {
|
||||
protected static validate(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions
|
||||
) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
.default("")
|
||||
.trim()
|
||||
.label(options.label ? options.label : "value");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions = {}
|
||||
) {
|
||||
const _options = {
|
||||
label: "title",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = AddressTitle.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(
|
||||
DomainError.INVALID_INPUT_DATA,
|
||||
validationResult.error
|
||||
)
|
||||
);
|
||||
}
|
||||
return Result.ok(new AddressTitle(validationResult.object));
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressTitle_ValidationError extends Joi.ValidationError {}
|
||||
@ -0,0 +1,59 @@
|
||||
import Joi from "joi";
|
||||
|
||||
import { UndefinedOr } from "../../../../../utilities";
|
||||
|
||||
import { RuleValidator } from "../../RuleValidator";
|
||||
import { DomainError, handleDomainError } from "../../errors";
|
||||
import { Result } from "../Result";
|
||||
import {
|
||||
IStringValueObjectOptions,
|
||||
StringValueObject,
|
||||
} from "../StringValueObject";
|
||||
|
||||
export class AddressType extends StringValueObject {
|
||||
protected static validate(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions
|
||||
) {
|
||||
const rule = Joi.string()
|
||||
.allow(null)
|
||||
.allow("")
|
||||
.default("")
|
||||
.trim()
|
||||
.label(options.label ? options.label : "value");
|
||||
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: UndefinedOr<string>,
|
||||
options: IStringValueObjectOptions = {}
|
||||
) {
|
||||
const _options = {
|
||||
label: "type",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = AddressType.validate(value, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
handleDomainError(
|
||||
DomainError.INVALID_INPUT_DATA,
|
||||
validationResult.error
|
||||
)
|
||||
);
|
||||
}
|
||||
return Result.ok(new AddressType(validationResult.object));
|
||||
}
|
||||
|
||||
public isBilling(): Boolean {
|
||||
return this.toString() === "billing";
|
||||
}
|
||||
|
||||
public isShipping(): Boolean {
|
||||
return this.toString() === "shipping";
|
||||
}
|
||||
}
|
||||
|
||||
export class AddressType_ValidationError extends Joi.ValidationError {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user