This commit is contained in:
David Arranz 2024-04-23 17:29:38 +02:00
commit 2194ed1420
182 changed files with 10155 additions and 0 deletions

22
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
}
}

View 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",
},
};

View 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",
},
};

View 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
);

View File

@ -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
)
);
}
}
}

View File

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

View 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;
}
}

View File

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

View File

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

View File

@ -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>>;
}

View File

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

View File

@ -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);
}
}

View File

@ -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 };

View File

@ -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);
}
}
}

View File

@ -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
);
};

View File

@ -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;
},
};

View File

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

View File

@ -0,0 +1 @@
export type FirebirdModel = Record<string, any>;

View File

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

View File

@ -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;
};

View 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;
}

View File

@ -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,
});

View File

@ -0,0 +1,3 @@
export * from "./services";
export * from "./useCases";

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface IApplicationService {}
export abstract class ApplicationService implements IApplicationService {}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export * from './QueryCriteriaService';

View File

@ -0,0 +1,6 @@
export interface IUseCaseRequest {}
export interface IUseCaseResponse {}
export interface IUseCase<IUseCaseRequest, IUseCaseResponse> {
execute(useCaseRequest: IUseCaseRequest): IUseCaseResponse;
}

View 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
);
}
*/

View File

@ -0,0 +1,2 @@
export * from "./UseCase.interface";
export * from "./UseCaseError";

View 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;
}

View 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);
}
}
*/

View 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
);
}
}

View File

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

View File

@ -0,0 +1,3 @@
export * from "./Mapper.interface";
export * from "./Specification";
export * from "./repositories";

View File

@ -0,0 +1,5 @@
import { TBusinessTransaction } from "./BusinessTransaction.interface";
export interface IAdapter {
startTransaction: () => TBusinessTransaction;
}

View File

@ -0,0 +1,5 @@
type TUnitOfWork = {
start(): unknown;
};
export type TBusinessTransaction = TUnitOfWork;

View File

@ -0,0 +1,2 @@
/* eslint-disable no-unused-vars */
export interface IRepository<T> {}

View File

@ -0,0 +1 @@
export type RepositoryBuilder<T> = (params?: any) => T;

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
export interface IRepositoryQueryOptions {
query: any;
}
export interface IRepositoryQueryBuilder {}

View 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";

View 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;
}
}

View 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);
}

View File

@ -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,
};
}
}

View File

@ -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",
};

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export * from './ExpressController';

View File

@ -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");
}

View File

@ -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();
}
}
);
});
}
}

View File

@ -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!");
}
}

View 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";

View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
import { FirebirdQueryBuilder } from "./FirebirdQueryBuilder";
const createFirebirdQueryBuilder = () => FirebirdQueryBuilder.create();
export { createFirebirdQueryBuilder };

View File

@ -0,0 +1,3 @@
export * from "./ContextFactory";
export * from "./InfrastructureError";
export * from "./mappers";

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

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

View File

@ -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;
}

View File

@ -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
);
}
}
}

View File

@ -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 };

View File

@ -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;
}
*/
}

View 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";

View File

@ -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!`);
}
}

View File

@ -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",
];
});
}
}

View File

@ -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,
},
};
}
}

View File

@ -0,0 +1,5 @@
import { SequelizeQueryBuilder } from "./SequelizeQueryBuilder";
const createSequelizeQueryBuilder = () => SequelizeQueryBuilder.create();
export { createSequelizeQueryBuilder };

2
server/src/index.ts Normal file
View File

@ -0,0 +1,2 @@
// Infra
import "./infrastructure/http/server";

View 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 };

View 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;

View 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);
});

View 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 };

View 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
View 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
View 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
View File

@ -0,0 +1,10 @@
{
"semi": true,
"printWidth": 80,
"useTabs": false,
"endOfLine": "auto",
"trailingComma": "all",
"singleQuote": false,
"bracketSpacing": true
}

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>[];
}

View File

@ -0,0 +1,7 @@
export interface IMoney_DTO {
amount: number;
precision: number;
currency: string;
}
export interface IMoney_Response_DTO extends IMoney_DTO {}

View File

@ -0,0 +1,6 @@
export interface IPercentage_DTO {
amount: number;
precision: number;
}
export interface IPercentage_Response_DTO extends IPercentage_DTO {}

View 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 {}

View File

@ -0,0 +1,4 @@
export * from "./IError_Response.dto";
export * from "./IMoney.dto";
export * from "./IPercentage.dto";
export * from "./ITaxType.dto";

View File

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

View File

@ -0,0 +1,6 @@
export class EntityError extends Error {
constructor(field: string, message?: string) {
super(message);
this.name = this.constructor.name;
}
}

View 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
);
};

View 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 {}

View File

@ -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 {}

View File

@ -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