.
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