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.
- package/README.md +515 -4
- package/dist/application/controller.d.ts +6 -2
- package/dist/application/controller.js +10 -18
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +2 -2
- package/dist/application/services.d.ts +2 -1
- package/dist/application/services.js +3 -3
- package/dist/dede.d.ts +10 -1
- package/dist/dede.js +30 -9
- package/dist/domain/entity.d.ts +4 -0
- package/dist/domain/entity.js +25 -0
- package/dist/domain/errors/app-error.d.ts +12 -0
- package/dist/domain/errors/app-error.js +14 -0
- package/dist/domain/errors/http-errors.d.ts +42 -0
- package/dist/domain/errors/http-errors.js +40 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +2 -0
- package/dist/http/controller.handler.d.ts +4 -5
- package/dist/http/controller.handler.js +27 -119
- package/dist/http/errors/server.d.ts +2 -28
- package/dist/http/errors/server.js +2 -49
- package/dist/http/http-server.d.ts +2 -0
- package/dist/http/http-server.js +1 -1
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/infra/di/registry.d.ts +4 -6
- package/dist/infra/di/registry.js +7 -10
- package/dist/{application → infra/serialization}/entity.d.ts +8 -1
- package/dist/{application → infra/serialization}/entity.js +87 -23
- package/dist/interface/errors/http-error-mapper.d.ts +10 -0
- package/dist/interface/errors/http-error-mapper.js +31 -0
- package/dist/interface/http/middleware-executor.d.ts +10 -0
- package/dist/interface/http/middleware-executor.js +33 -0
- package/dist/interface/http/request-mapper.d.ts +21 -0
- package/dist/interface/http/request-mapper.js +55 -0
- package/dist/interface/validation/class-validator.d.ts +6 -0
- package/dist/interface/validation/class-validator.js +28 -0
- package/dist/interface/validation/validator.d.ts +5 -0
- package/dist/interface/validation/validator.js +1 -0
- package/dist/protocols/repository.d.ts +1 -1
- package/package.json +5 -2
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
class
|
|
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
|
|
24
|
-
export function
|
|
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 =
|
|
25
|
+
const dependency = container.inject(name);
|
|
29
26
|
return dependency[propertyKey];
|
|
30
27
|
}
|
|
31
28
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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,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 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framework-do-dede",
|
|
3
|
-
"version": "
|
|
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"
|