framework-do-dede 3.3.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +515 -4
  2. package/dist/application/controller.d.ts +6 -2
  3. package/dist/application/controller.js +10 -18
  4. package/dist/application/index.d.ts +2 -2
  5. package/dist/application/index.js +2 -2
  6. package/dist/application/services.d.ts +2 -1
  7. package/dist/application/services.js +3 -3
  8. package/dist/dede.d.ts +10 -1
  9. package/dist/dede.js +30 -9
  10. package/dist/domain/entity.d.ts +4 -0
  11. package/dist/domain/entity.js +25 -0
  12. package/dist/domain/errors/app-error.d.ts +12 -0
  13. package/dist/domain/errors/app-error.js +14 -0
  14. package/dist/domain/errors/http-errors.d.ts +42 -0
  15. package/dist/domain/errors/http-errors.js +40 -0
  16. package/dist/domain/index.d.ts +2 -0
  17. package/dist/domain/index.js +2 -0
  18. package/dist/http/controller.handler.d.ts +4 -5
  19. package/dist/http/controller.handler.js +27 -119
  20. package/dist/http/errors/server.d.ts +2 -28
  21. package/dist/http/errors/server.js +2 -49
  22. package/dist/http/http-server.d.ts +2 -0
  23. package/dist/http/http-server.js +1 -1
  24. package/dist/http/index.d.ts +2 -2
  25. package/dist/http/index.js +2 -2
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +4 -3
  28. package/dist/infra/di/registry.d.ts +4 -6
  29. package/dist/infra/di/registry.js +7 -10
  30. package/dist/{application → infra/serialization}/entity.d.ts +8 -1
  31. package/dist/{application → infra/serialization}/entity.js +87 -23
  32. package/dist/interface/errors/http-error-mapper.d.ts +10 -0
  33. package/dist/interface/errors/http-error-mapper.js +31 -0
  34. package/dist/interface/http/middleware-executor.d.ts +10 -0
  35. package/dist/interface/http/middleware-executor.js +33 -0
  36. package/dist/interface/http/request-mapper.d.ts +21 -0
  37. package/dist/interface/http/request-mapper.js +55 -0
  38. package/dist/interface/validation/class-validator.d.ts +6 -0
  39. package/dist/interface/validation/class-validator.js +28 -0
  40. package/dist/interface/validation/validator.d.ts +5 -0
  41. package/dist/interface/validation/validator.js +1 -0
  42. package/dist/protocols/repository.d.ts +1 -1
  43. package/package.json +5 -2
@@ -1,13 +1,7 @@
1
- class ComponentRegistry {
1
+ export class Container {
2
2
  constructor() {
3
3
  this.dependencies = new Map();
4
4
  }
5
- static getInstance() {
6
- if (!this.instance) {
7
- this.instance = new ComponentRegistry();
8
- }
9
- return this.instance;
10
- }
11
5
  load(name, dependency) {
12
6
  this.dependencies.set(name, dependency);
13
7
  }
@@ -20,12 +14,15 @@ class ComponentRegistry {
20
14
  this.dependencies.delete(name);
21
15
  }
22
16
  }
23
- export const Registry = ComponentRegistry.getInstance();
24
- export function Inject(name) {
17
+ export let DefaultContainer = new Container();
18
+ export function setDefaultContainer(container) {
19
+ DefaultContainer = container;
20
+ }
21
+ export function Inject(name, container = DefaultContainer) {
25
22
  return function (target, propertyKey) {
26
23
  target[propertyKey] = new Proxy({}, {
27
24
  get(_, propertyKey) {
28
- const dependency = Registry.inject(name);
25
+ const dependency = container.inject(name);
29
26
  return dependency[propertyKey];
30
27
  }
31
28
  });
@@ -1,5 +1,9 @@
1
- export declare abstract class Entity {
1
+ import { Entity as DomainEntity } from "../../domain/entity";
2
+ export declare abstract class SerializableEntity extends DomainEntity {
2
3
  [x: string]: any;
4
+ private buildRawEntityObject;
5
+ private getEntityHooks;
6
+ private runEntityHooks;
3
7
  toEntity(): Record<string, any>;
4
8
  toAsyncEntity(): Promise<Record<string, any>>;
5
9
  toData({ serialize }?: {
@@ -14,3 +18,6 @@ export declare function Restrict(): (target: any, propertyKey: string) => void;
14
18
  export declare function VirtualProperty(propertyName: string): (target: any, methodName: string) => void;
15
19
  export declare function Serialize(callback: (value: any) => any): PropertyDecorator;
16
20
  export declare function GetterPrefix(prefix: string): (target: any, propertyKey: string) => void;
21
+ export { SerializableEntity as Entity };
22
+ export declare function BeforeToEntity(): MethodDecorator;
23
+ export declare function AfterToEntity(): MethodDecorator;
@@ -1,5 +1,61 @@
1
- export class Entity {
1
+ import { Entity as DomainEntity } from "../../domain/entity";
2
+ export class SerializableEntity extends DomainEntity {
3
+ buildRawEntityObject() {
4
+ const result = {};
5
+ for (const [propName] of Object.entries(this)) {
6
+ let value = this[propName];
7
+ if (typeof value === 'function')
8
+ continue;
9
+ if (value === undefined)
10
+ continue;
11
+ result[propName] = value;
12
+ }
13
+ return result;
14
+ }
15
+ getEntityHooks(hookKey) {
16
+ const hooks = [];
17
+ let current = this.constructor;
18
+ while (current && current !== SerializableEntity) {
19
+ const currentHooks = current[hookKey];
20
+ if (currentHooks && currentHooks.length) {
21
+ hooks.unshift(...currentHooks);
22
+ }
23
+ current = Object.getPrototypeOf(current);
24
+ }
25
+ return hooks;
26
+ }
27
+ runEntityHooks(hookKey, payload, awaitHooks) {
28
+ const hooks = this.getEntityHooks(hookKey);
29
+ if (!hooks.length)
30
+ return;
31
+ if (awaitHooks) {
32
+ return (async () => {
33
+ for (const hookName of hooks) {
34
+ const hook = this[hookName];
35
+ if (typeof hook !== 'function')
36
+ continue;
37
+ await hook.call(this, payload);
38
+ }
39
+ })();
40
+ }
41
+ for (const hookName of hooks) {
42
+ const hook = this[hookName];
43
+ if (typeof hook !== 'function')
44
+ continue;
45
+ try {
46
+ const result = hook.call(this, payload);
47
+ if (result && typeof result.then === 'function') {
48
+ void result.catch(() => undefined);
49
+ }
50
+ }
51
+ catch (error) {
52
+ throw error;
53
+ }
54
+ }
55
+ }
2
56
  toEntity() {
57
+ const raw = this.buildRawEntityObject();
58
+ this.runEntityHooks(BEFORE_TO_ENTITY, raw, false);
3
59
  // @ts-ignore
4
60
  const propertiesConfigs = this.constructor.propertiesConfigs;
5
61
  const result = {};
@@ -31,9 +87,12 @@ export class Entity {
31
87
  value = null;
32
88
  result[propertyName] = value;
33
89
  }
90
+ this.runEntityHooks(AFTER_TO_ENTITY, result, false);
34
91
  return result;
35
92
  }
36
93
  async toAsyncEntity() {
94
+ const raw = this.buildRawEntityObject();
95
+ await this.runEntityHooks(BEFORE_TO_ENTITY, raw, true);
37
96
  // @ts-ignore
38
97
  const propertiesConfigs = this.constructor.propertiesConfigs;
39
98
  const result = {};
@@ -45,7 +104,7 @@ export class Entity {
45
104
  if (value === undefined)
46
105
  continue;
47
106
  // @ts-ignore
48
- if (propertiesConfigs && propertiesConfigs[propName]?.serialize && (value || valueIsZero)) {
107
+ if (propertiesConfigs && propertiesConfigs[propName]?.serialize && (value || value === 0)) {
49
108
  const serializedValue = await propertiesConfigs[propName].serialize(value);
50
109
  if (serializedValue && typeof serializedValue === 'object' && !Array.isArray(serializedValue)) {
51
110
  const entries = Object.entries(serializedValue);
@@ -65,6 +124,7 @@ export class Entity {
65
124
  value = null;
66
125
  result[propertyName] = value;
67
126
  }
127
+ await this.runEntityHooks(AFTER_TO_ENTITY, result, true);
68
128
  return result;
69
129
  }
70
130
  toData({ serialize = false } = {}) {
@@ -122,27 +182,7 @@ export class Entity {
122
182
  return result;
123
183
  }
124
184
  generateGetters() {
125
- for (const property of Object.keys(this)) {
126
- if (typeof this[property] === 'function')
127
- continue;
128
- let prefixName = null;
129
- // @ts-ignore
130
- if (this.constructor.propertiesConfigs && this.constructor.propertiesConfigs[property] && this.constructor.propertiesConfigs[property].prefix) {
131
- // @ts-ignore
132
- prefixName = this.constructor.propertiesConfigs[property].prefix;
133
- }
134
- else {
135
- const isBoolean = this[property] ? typeof this[property] === 'boolean' : false;
136
- prefixName = isBoolean ? 'is' : 'get';
137
- }
138
- let getterName = null;
139
- if (property[0]) {
140
- getterName = `${prefixName}${property[0].toUpperCase()}${property.slice(1)}`;
141
- if (this[getterName])
142
- continue;
143
- this[getterName] = () => this[property];
144
- }
145
- }
185
+ super.generateGetters();
146
186
  }
147
187
  }
148
188
  export function Restrict() {
@@ -178,3 +218,27 @@ const loadPropertiesConfig = (target, propertyKey) => {
178
218
  target.constructor.propertiesConfigs[propertyKey] = {};
179
219
  }
180
220
  };
221
+ const BEFORE_TO_ENTITY = Symbol('beforeToEntity');
222
+ const AFTER_TO_ENTITY = Symbol('afterToEntity');
223
+ const assertEntityDecoratorTarget = (target, decoratorName) => {
224
+ if (!SerializableEntity.prototype.isPrototypeOf(target)) {
225
+ throw new Error(`${decoratorName} can only be used on Entity classes`);
226
+ }
227
+ };
228
+ export { SerializableEntity as Entity };
229
+ export function BeforeToEntity() {
230
+ return function (target, propertyKey) {
231
+ assertEntityDecoratorTarget(target, 'BeforeToEntity');
232
+ const cls = target.constructor;
233
+ cls[BEFORE_TO_ENTITY] = cls[BEFORE_TO_ENTITY] || [];
234
+ cls[BEFORE_TO_ENTITY].push(propertyKey);
235
+ };
236
+ }
237
+ export function AfterToEntity() {
238
+ return function (target, propertyKey) {
239
+ assertEntityDecoratorTarget(target, 'AfterToEntity');
240
+ const cls = target.constructor;
241
+ cls[AFTER_TO_ENTITY] = cls[AFTER_TO_ENTITY] || [];
242
+ cls[AFTER_TO_ENTITY].push(propertyKey);
243
+ };
244
+ }
@@ -0,0 +1,10 @@
1
+ import type HttpServer from '../../http/http-server';
2
+ export type MappedError = {
3
+ message: string;
4
+ statusCode: number;
5
+ custom?: boolean;
6
+ unexpectedError?: string;
7
+ };
8
+ export declare class HttpErrorMapper {
9
+ map(error: any, httpServer: HttpServer): MappedError;
10
+ }
@@ -0,0 +1,31 @@
1
+ import { AppError } from '../../domain/errors/app-error';
2
+ import { InternalServerError } from '../../domain/errors/http-errors';
3
+ export class HttpErrorMapper {
4
+ map(error, httpServer) {
5
+ if (error instanceof AppError) {
6
+ return {
7
+ message: error.message,
8
+ statusCode: error.getStatusCode()
9
+ };
10
+ }
11
+ if (error && typeof error.getStatusCode === 'function' && typeof error.getCustom === 'function') {
12
+ return {
13
+ ...error.getCustom(),
14
+ statusCode: error.getStatusCode(),
15
+ custom: true
16
+ };
17
+ }
18
+ const debugError = {
19
+ sourceUrl: error?.sourceURL,
20
+ line: error?.line,
21
+ column: error?.column,
22
+ };
23
+ const internal = new InternalServerError(error?.message || 'Unexpected error', httpServer.getDefaultMessageError());
24
+ return {
25
+ message: internal.message,
26
+ statusCode: internal.getStatusCode(),
27
+ unexpectedError: internal.getUnexpectedError(),
28
+ ...debugError
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ import type { Request } from "../../http/http-server";
2
+ import type { Middleware } from "../../application/controller";
3
+ export type ExecutedMiddleware = {
4
+ elapsedTime: string;
5
+ middleware: string;
6
+ error?: any;
7
+ };
8
+ export declare class MiddlewareExecutor {
9
+ execute(middlewares: Middleware[] | undefined, request: Request): Promise<ExecutedMiddleware[]>;
10
+ }
@@ -0,0 +1,33 @@
1
+ export class MiddlewareExecutor {
2
+ async execute(middlewares = [], request) {
3
+ const executed = [];
4
+ if (middlewares && middlewares.length > 0) {
5
+ for (const middleware of middlewares) {
6
+ let startTime = 0;
7
+ let endTime = 0;
8
+ let elapsedTime;
9
+ try {
10
+ startTime = performance.now();
11
+ const middlewareResult = await middleware.execute(request);
12
+ request.context = Object.assign(request.context, middlewareResult);
13
+ endTime = performance.now();
14
+ elapsedTime = `${(endTime - startTime).toFixed(2)} ms`;
15
+ executed.push({
16
+ elapsedTime,
17
+ middleware: middleware.constructor.name,
18
+ });
19
+ }
20
+ catch (error) {
21
+ elapsedTime = `${(endTime - startTime).toFixed(2)} ms`;
22
+ executed.push({
23
+ elapsedTime,
24
+ middleware: middleware.constructor.name,
25
+ error,
26
+ });
27
+ throw error;
28
+ }
29
+ }
30
+ }
31
+ return executed;
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ import type { Request } from "../../http/http-server";
2
+ type BodyFilter = 'none' | 'restrict';
3
+ type RouteInputConfig = {
4
+ params?: string[];
5
+ query?: string[];
6
+ headers?: string[];
7
+ body?: string[];
8
+ bodyFilter?: BodyFilter;
9
+ };
10
+ type HttpInput = {
11
+ headers: any;
12
+ body: any;
13
+ params: any;
14
+ query: any;
15
+ };
16
+ export declare class HttpRequestMapper {
17
+ map(input: HttpInput, config: RouteInputConfig): Request;
18
+ private filter;
19
+ private normalizeBracketNotation;
20
+ }
21
+ export {};
@@ -0,0 +1,55 @@
1
+ export class HttpRequestMapper {
2
+ map(input, config) {
3
+ const filterParams = this.filter(input.params, config.params);
4
+ const filterQueryParams = this.filter(input.query, config.query);
5
+ const filterHeaders = this.filter(input.headers, config.headers);
6
+ const normalizeBody = this.normalizeBracketNotation(input.body);
7
+ let filterBody = this.filter(normalizeBody, config.body);
8
+ if (config.bodyFilter !== 'restrict') {
9
+ filterBody = { ...normalizeBody, ...filterBody };
10
+ }
11
+ const mergedParams = { ...filterHeaders, ...filterParams, ...filterQueryParams, ...filterBody };
12
+ return { data: mergedParams, context: {} };
13
+ }
14
+ filter(params, filterParams) {
15
+ const filter = {};
16
+ for (const paramName of filterParams || []) {
17
+ const [paramNameFiltered, type] = paramName.split('|');
18
+ let value = params[paramName] ?? params[paramNameFiltered];
19
+ if (value === undefined || value === null)
20
+ continue;
21
+ if (type === 'boolean')
22
+ value = value === 'true';
23
+ if (type === 'integer') {
24
+ value = value.replace(/[^0-9]/g, '');
25
+ value = value ? parseInt(value) : 0;
26
+ }
27
+ if (type === 'string')
28
+ value = value.toString();
29
+ if (type === 'number')
30
+ value = parseFloat(value);
31
+ filter[paramNameFiltered] = value;
32
+ }
33
+ return filter;
34
+ }
35
+ normalizeBracketNotation(data) {
36
+ if (!data || typeof data !== "object")
37
+ return data;
38
+ const normalized = {};
39
+ for (const [rawKey, value] of Object.entries(data)) {
40
+ const key = String(rawKey);
41
+ const match = key.match(/^([^\[\]]+)\[([^\[\]]+)\]$/);
42
+ if (match) {
43
+ const parent = match[1];
44
+ const child = match[2];
45
+ if (!normalized[parent] || typeof normalized[parent] !== "object") {
46
+ normalized[parent] = {};
47
+ }
48
+ normalized[parent][child] = value;
49
+ continue;
50
+ }
51
+ normalized[key] = value;
52
+ }
53
+ return normalized;
54
+ }
55
+ }
@@ -0,0 +1,6 @@
1
+ export type ValidationErrorDetail = {
2
+ path: string;
3
+ message: string;
4
+ rule: string;
5
+ };
6
+ export declare function validateWithClassValidator<T extends object>(dtoClass: new () => T, input: T): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { UnprocessableEntity } from '../../domain/errors/http-errors';
2
+ import { validate } from 'class-validator';
3
+ export async function validateWithClassValidator(dtoClass, input) {
4
+ const instance = Object.assign(new dtoClass(), input);
5
+ const errors = await validate(instance);
6
+ if (errors.length === 0)
7
+ return;
8
+ const details = flattenErrors(errors);
9
+ throw new UnprocessableEntity('Validation error', {
10
+ code: 'validation_error',
11
+ details
12
+ });
13
+ }
14
+ function flattenErrors(errors, parentPath = '') {
15
+ const output = [];
16
+ for (const error of errors) {
17
+ const path = parentPath ? `${parentPath}.${error.property}` : error.property;
18
+ if (error.constraints) {
19
+ for (const [rule, message] of Object.entries(error.constraints)) {
20
+ output.push({ path, rule, message });
21
+ }
22
+ }
23
+ if (error.children && error.children.length > 0) {
24
+ output.push(...flattenErrors(error.children, path));
25
+ }
26
+ }
27
+ return output;
28
+ }
@@ -0,0 +1,5 @@
1
+ export interface Validator<T = any> {
2
+ validate(input: T): void | Promise<void>;
3
+ }
4
+ export type ValidatorClass<T = any> = new () => T;
5
+ export type ValidatorLike<T = any> = Validator<T> | ValidatorClass<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { Entity } from "../application";
1
+ import { Entity } from "../domain";
2
2
  export interface RepositoryCreate<T extends Entity> {
3
3
  create(input: T): Promise<void>;
4
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framework-do-dede",
3
- "version": "3.3.1",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -16,7 +16,9 @@
16
16
  "test:watch": "jest --watch",
17
17
  "clean": "rimraf dist",
18
18
  "build": "npm run clean && tsc -p tsconfig.build.json && tsc-alias",
19
- "prepare": "npm run build"
19
+ "prepare": "npm run build",
20
+ "bench": "node bench/benchmark.mjs",
21
+ "bench:compare": "node bench/run.mjs"
20
22
  },
21
23
  "files": [
22
24
  "dist"
@@ -46,6 +48,7 @@
46
48
  "typescript": "^5.8.2"
47
49
  },
48
50
  "peerDependencies": {
51
+ "class-validator": "^0.14.3",
49
52
  "elysia": "^1.3.5",
50
53
  "express": "^5.1.0",
51
54
  "typescript": "^5.8.2"