@velajs/vela 0.4.1 → 0.4.3
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/dist/container/container.d.ts +2 -0
- package/dist/container/container.js +5 -0
- package/dist/container/types.d.ts +1 -0
- package/dist/factory.js +25 -11
- package/dist/http/route.manager.d.ts +9 -1
- package/dist/http/route.manager.js +80 -52
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/module/module-loader.d.ts +4 -0
- package/dist/module/module-loader.js +55 -3
- package/dist/schedule/schedule.executor.d.ts +11 -2
- package/dist/schedule/schedule.executor.js +134 -19
- package/dist/serialization/index.d.ts +2 -0
- package/dist/serialization/index.js +2 -0
- package/dist/serialization/serialize.decorator.d.ts +6 -0
- package/dist/serialization/serialize.decorator.js +7 -0
- package/dist/serialization/serializer.interceptor.d.ts +4 -0
- package/dist/serialization/serializer.interceptor.js +16 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/testing.builder.d.ts +29 -0
- package/dist/testing/testing.builder.js +148 -0
- package/dist/testing/testing.module.d.ts +9 -0
- package/dist/testing/testing.module.js +15 -0
- package/dist/validation/create-zod-dto.d.ts +8 -0
- package/dist/validation/create-zod-dto.js +6 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/validation.pipe.d.ts +4 -0
- package/dist/validation/validation.pipe.js +19 -0
- package/package.json +3 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Scope } from '../constants';
|
|
1
2
|
import type { ProviderOptions, Token, Type } from './types';
|
|
2
3
|
export declare class Container {
|
|
3
4
|
private providers;
|
|
@@ -9,6 +10,7 @@ export declare class Container {
|
|
|
9
10
|
private registerOptions;
|
|
10
11
|
resolve<T>(token: Token<T>): T;
|
|
11
12
|
has(token: Token): boolean;
|
|
13
|
+
getProviderScope(token: Token): Scope | undefined;
|
|
12
14
|
getTokens(): Token[];
|
|
13
15
|
/**
|
|
14
16
|
* Create a child container for request-scoped resolution.
|
|
@@ -40,6 +40,8 @@ export class Container {
|
|
|
40
40
|
} else if (options.useFactory) {
|
|
41
41
|
registration.useFactory = options.useFactory;
|
|
42
42
|
registration.inject = options.inject;
|
|
43
|
+
} else if (options.useClass) {
|
|
44
|
+
registration.useClass = options.useClass;
|
|
43
45
|
} else if (options.useExisting) {
|
|
44
46
|
registration.useExisting = options.useExisting;
|
|
45
47
|
} else if (typeof token === 'function') {
|
|
@@ -68,6 +70,9 @@ export class Container {
|
|
|
68
70
|
has(token) {
|
|
69
71
|
return this.providers.has(token);
|
|
70
72
|
}
|
|
73
|
+
getProviderScope(token) {
|
|
74
|
+
return this.providers.get(token)?.scope;
|
|
75
|
+
}
|
|
71
76
|
getTokens() {
|
|
72
77
|
return Array.from(this.providers.keys());
|
|
73
78
|
}
|
package/dist/factory.js
CHANGED
|
@@ -15,21 +15,35 @@ export const VelaFactory = {
|
|
|
15
15
|
ComponentManager.init(container);
|
|
16
16
|
const loader = new ModuleLoader(container, routeManager);
|
|
17
17
|
loader.load(rootModule);
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
routeManager.
|
|
18
|
+
const appGuards = loader.getAppProviderTokens(APP_GUARD);
|
|
19
|
+
if (appGuards.length > 0) {
|
|
20
|
+
routeManager.useGlobalGuardTokens(...appGuards);
|
|
21
|
+
} else if (container.has(APP_GUARD)) {
|
|
22
|
+
routeManager.useGlobalGuardTokens(APP_GUARD);
|
|
21
23
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const appPipes = loader.getAppProviderTokens(APP_PIPE);
|
|
25
|
+
if (appPipes.length > 0) {
|
|
26
|
+
routeManager.useGlobalPipeTokens(...appPipes);
|
|
27
|
+
} else if (container.has(APP_PIPE)) {
|
|
28
|
+
routeManager.useGlobalPipeTokens(APP_PIPE);
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const appInterceptors = loader.getAppProviderTokens(APP_INTERCEPTOR);
|
|
31
|
+
if (appInterceptors.length > 0) {
|
|
32
|
+
routeManager.useGlobalInterceptorTokens(...appInterceptors);
|
|
33
|
+
} else if (container.has(APP_INTERCEPTOR)) {
|
|
34
|
+
routeManager.useGlobalInterceptorTokens(APP_INTERCEPTOR);
|
|
27
35
|
}
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
const appFilters = loader.getAppProviderTokens(APP_FILTER);
|
|
37
|
+
if (appFilters.length > 0) {
|
|
38
|
+
routeManager.useGlobalFilterTokens(...appFilters);
|
|
39
|
+
} else if (container.has(APP_FILTER)) {
|
|
40
|
+
routeManager.useGlobalFilterTokens(APP_FILTER);
|
|
30
41
|
}
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
const appMiddleware = loader.getAppProviderTokens(APP_MIDDLEWARE);
|
|
43
|
+
if (appMiddleware.length > 0) {
|
|
44
|
+
routeManager.useGlobalMiddlewareTokens(...appMiddleware);
|
|
45
|
+
} else if (container.has(APP_MIDDLEWARE)) {
|
|
46
|
+
routeManager.useGlobalMiddlewareTokens(APP_MIDDLEWARE);
|
|
33
47
|
}
|
|
34
48
|
const app = new VelaApplication(container, routeManager);
|
|
35
49
|
const instances = loader.resolveAllInstances();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import type { Container } from '../container/container';
|
|
3
|
-
import type { Type } from '../container/types';
|
|
3
|
+
import type { Token, Type } from '../container/types';
|
|
4
|
+
import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
|
|
4
5
|
import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from '../registry/types';
|
|
5
6
|
import type { ControllerRegistration } from './types';
|
|
6
7
|
export declare class RouteManager {
|
|
@@ -15,11 +16,18 @@ export declare class RouteManager {
|
|
|
15
16
|
constructor(container: Container);
|
|
16
17
|
setGlobalPrefix(prefix: string): this;
|
|
17
18
|
useGlobalMiddleware(...middleware: MiddlewareType[]): this;
|
|
19
|
+
useGlobalMiddlewareTokens(...middlewareTokens: Array<Token<NestMiddleware>>): this;
|
|
18
20
|
useGlobalPipes(...pipes: PipeType[]): this;
|
|
21
|
+
useGlobalPipeTokens(...pipeTokens: Array<Token<PipeTransform>>): this;
|
|
19
22
|
useGlobalGuards(...guards: GuardType[]): this;
|
|
23
|
+
useGlobalGuardTokens(...guardTokens: Array<Token<CanActivate>>): this;
|
|
20
24
|
useGlobalInterceptors(...interceptors: InterceptorType[]): this;
|
|
25
|
+
useGlobalInterceptorTokens(...interceptorTokens: Array<Token<NestInterceptor>>): this;
|
|
21
26
|
useGlobalFilters(...filters: FilterType[]): this;
|
|
27
|
+
useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
|
|
22
28
|
private instantiate;
|
|
29
|
+
private instantiateMany;
|
|
30
|
+
private getRequestContainer;
|
|
23
31
|
registerController(controller: Type): this;
|
|
24
32
|
build(): Promise<Hono>;
|
|
25
33
|
private createExecutionContext;
|
|
@@ -35,43 +35,68 @@ export class RouteManager {
|
|
|
35
35
|
return this;
|
|
36
36
|
}
|
|
37
37
|
useGlobalMiddleware(...middleware) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
this.globalMiddleware.push(...middleware);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
useGlobalMiddlewareTokens(...middlewareTokens) {
|
|
42
|
+
this.globalMiddleware.push(...middlewareTokens);
|
|
41
43
|
return this;
|
|
42
44
|
}
|
|
43
45
|
useGlobalPipes(...pipes) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
this.globalPipes.push(...pipes);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
useGlobalPipeTokens(...pipeTokens) {
|
|
50
|
+
this.globalPipes.push(...pipeTokens);
|
|
47
51
|
return this;
|
|
48
52
|
}
|
|
49
53
|
useGlobalGuards(...guards) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
this.globalGuards.push(...guards);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
useGlobalGuardTokens(...guardTokens) {
|
|
58
|
+
this.globalGuards.push(...guardTokens);
|
|
53
59
|
return this;
|
|
54
60
|
}
|
|
55
61
|
useGlobalInterceptors(...interceptors) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
this.globalInterceptors.push(...interceptors);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
useGlobalInterceptorTokens(...interceptorTokens) {
|
|
66
|
+
this.globalInterceptors.push(...interceptorTokens);
|
|
59
67
|
return this;
|
|
60
68
|
}
|
|
61
69
|
useGlobalFilters(...filters) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
this.globalFilters.push(...filters);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
useGlobalFilterTokens(...filterTokens) {
|
|
74
|
+
this.globalFilters.push(...filterTokens);
|
|
65
75
|
return this;
|
|
66
76
|
}
|
|
67
|
-
instantiate(classOrInstance) {
|
|
68
|
-
if (typeof classOrInstance
|
|
69
|
-
|
|
77
|
+
instantiate(classOrInstance, container) {
|
|
78
|
+
if (typeof classOrInstance === 'function') {
|
|
79
|
+
if (container.has(classOrInstance)) {
|
|
80
|
+
return container.resolve(classOrInstance);
|
|
81
|
+
}
|
|
82
|
+
return new classOrInstance();
|
|
83
|
+
}
|
|
84
|
+
if (container.has(classOrInstance)) {
|
|
85
|
+
return container.resolve(classOrInstance);
|
|
70
86
|
}
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
return classOrInstance;
|
|
88
|
+
}
|
|
89
|
+
instantiateMany(items, container) {
|
|
90
|
+
return items.map((item)=>this.instantiate(item, container));
|
|
91
|
+
}
|
|
92
|
+
getRequestContainer(c) {
|
|
93
|
+
const existing = c.get('container');
|
|
94
|
+
if (existing) {
|
|
95
|
+
return existing;
|
|
73
96
|
}
|
|
74
|
-
|
|
97
|
+
const child = this.container.createChild();
|
|
98
|
+
c.set('container', child);
|
|
99
|
+
return child;
|
|
75
100
|
}
|
|
76
101
|
registerController(controller) {
|
|
77
102
|
const prefix = MetadataRegistry.getControllerPath(controller);
|
|
@@ -94,12 +119,15 @@ export class RouteManager {
|
|
|
94
119
|
const app = new Hono();
|
|
95
120
|
// Register global middleware on the Hono app
|
|
96
121
|
for (const mw of this.globalMiddleware){
|
|
97
|
-
app.use('*', (c, next)=>
|
|
122
|
+
app.use('*', (c, next)=>{
|
|
123
|
+
const requestContainer = this.getRequestContainer(c);
|
|
124
|
+
const resolved = this.instantiate(mw, requestContainer);
|
|
125
|
+
return resolved.use(c, next);
|
|
126
|
+
});
|
|
98
127
|
}
|
|
99
128
|
// First pass: register all custom routes (must come before CRUD /:id routes)
|
|
100
129
|
for (const { controller, metadata, routes } of this.controllers){
|
|
101
130
|
if (routes.length > 0) {
|
|
102
|
-
const instance = this.container.resolve(controller);
|
|
103
131
|
const allParamMetadata = MetadataRegistry.getParameters(controller);
|
|
104
132
|
for (const route of routes){
|
|
105
133
|
// Determine effective version: route-level overrides controller-level
|
|
@@ -108,11 +136,14 @@ export class RouteManager {
|
|
|
108
136
|
const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
|
|
109
137
|
// Register per-route middleware as Hono middleware before the handler
|
|
110
138
|
const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
|
|
111
|
-
const
|
|
112
|
-
const handler = this.createHandler(instance, route, controller, allParamMetadata);
|
|
139
|
+
const handler = this.createHandler(route, controller, allParamMetadata);
|
|
113
140
|
for (const fullPath of versionedPaths){
|
|
114
|
-
for (const
|
|
115
|
-
app.use(fullPath, (c, next)=>
|
|
141
|
+
for (const middlewareItem of middlewareItems){
|
|
142
|
+
app.use(fullPath, (c, next)=>{
|
|
143
|
+
const requestContainer = this.getRequestContainer(c);
|
|
144
|
+
const resolved = this.instantiate(middlewareItem, requestContainer);
|
|
145
|
+
return resolved.use(c, next);
|
|
146
|
+
});
|
|
116
147
|
}
|
|
117
148
|
this.registerRoute(app, route.method, fullPath, handler);
|
|
118
149
|
}
|
|
@@ -128,7 +159,7 @@ export class RouteManager {
|
|
|
128
159
|
const { buildCrudRoutes } = await import(pkg);
|
|
129
160
|
await buildCrudRoutes(app, controller, metadata.prefix, crudConfig, {
|
|
130
161
|
globalPrefix: this.globalPrefix,
|
|
131
|
-
globalGuards: this.globalGuards,
|
|
162
|
+
globalGuards: this.instantiateMany(this.globalGuards, this.container),
|
|
132
163
|
joinPaths: this.joinPaths.bind(this)
|
|
133
164
|
});
|
|
134
165
|
} catch {
|
|
@@ -146,49 +177,46 @@ export class RouteManager {
|
|
|
146
177
|
getRequest: ()=>c.req.raw
|
|
147
178
|
};
|
|
148
179
|
}
|
|
149
|
-
createHandler(
|
|
180
|
+
createHandler(route, controller, allParamMetadata) {
|
|
150
181
|
const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
|
|
151
|
-
//
|
|
182
|
+
// Read param types once at build time for metatype population
|
|
183
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
|
|
184
|
+
// Collect controller + method level components at build time
|
|
152
185
|
const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
|
|
153
186
|
const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
|
|
154
187
|
const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
|
|
155
|
-
const methodFilters = ComponentManager.getComponents('filter', controller, route.handlerName);
|
|
156
|
-
const resolvedMethodGuards = ComponentManager.resolveGuards(methodGuards);
|
|
157
|
-
const resolvedMethodPipes = ComponentManager.resolvePipes(methodPipes);
|
|
158
|
-
const resolvedMethodInterceptors = ComponentManager.resolveInterceptors(methodInterceptors);
|
|
159
188
|
// Filters: reverse order (handler → controller → global) — closest to handler runs first
|
|
160
|
-
const
|
|
161
|
-
...
|
|
162
|
-
].reverse()
|
|
189
|
+
const methodFilters = [
|
|
190
|
+
...ComponentManager.getComponents('filter', controller, route.handlerName)
|
|
191
|
+
].reverse();
|
|
163
192
|
// Read response decorators at build time
|
|
164
193
|
const httpCode = getHttpCode(controller, route.handlerName);
|
|
165
194
|
const responseHeaders = getResponseHeaders(controller, route.handlerName);
|
|
166
195
|
const redirect = getRedirect(controller, route.handlerName);
|
|
167
196
|
return async (c)=>{
|
|
168
|
-
// Create a child container for request-scoped providers
|
|
169
|
-
const requestContainer = this.container.createChild();
|
|
170
|
-
c.set('container', requestContainer);
|
|
171
197
|
// Combine global + method at request time (allows post-create registration)
|
|
198
|
+
const requestContainer = this.getRequestContainer(c);
|
|
172
199
|
const guards = [
|
|
173
|
-
...this.globalGuards,
|
|
174
|
-
...
|
|
200
|
+
...this.instantiateMany(this.globalGuards, requestContainer),
|
|
201
|
+
...this.instantiateMany(methodGuards, requestContainer)
|
|
175
202
|
];
|
|
176
203
|
const pipes = [
|
|
177
|
-
...this.globalPipes,
|
|
178
|
-
...
|
|
204
|
+
...this.instantiateMany(this.globalPipes, requestContainer),
|
|
205
|
+
...this.instantiateMany(methodPipes, requestContainer)
|
|
179
206
|
];
|
|
180
207
|
const interceptors = [
|
|
181
|
-
...this.globalInterceptors,
|
|
182
|
-
...
|
|
208
|
+
...this.instantiateMany(this.globalInterceptors, requestContainer),
|
|
209
|
+
...this.instantiateMany(methodInterceptors, requestContainer)
|
|
183
210
|
];
|
|
184
211
|
const filters = [
|
|
185
|
-
...
|
|
186
|
-
...this.globalFilters
|
|
212
|
+
...this.instantiateMany(methodFilters, requestContainer),
|
|
213
|
+
...this.instantiateMany(this.globalFilters, requestContainer)
|
|
187
214
|
];
|
|
188
215
|
const executionContext = this.createExecutionContext(c, controller, route);
|
|
189
216
|
try {
|
|
217
|
+
const instance = requestContainer.resolve(controller);
|
|
190
218
|
// 1. Extract args + run pipes
|
|
191
|
-
const args = await this.extractArguments(c, paramMetadata, pipes);
|
|
219
|
+
const args = await this.extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes);
|
|
192
220
|
// 2. Guards (fail-fast)
|
|
193
221
|
for (const guard of guards){
|
|
194
222
|
const canActivate = await guard.canActivate(executionContext);
|
|
@@ -245,7 +273,7 @@ export class RouteManager {
|
|
|
245
273
|
}
|
|
246
274
|
};
|
|
247
275
|
}
|
|
248
|
-
async extractArguments(c, paramMetadata, pipes) {
|
|
276
|
+
async extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes) {
|
|
249
277
|
if (paramMetadata.length === 0) {
|
|
250
278
|
return [
|
|
251
279
|
c
|
|
@@ -258,7 +286,7 @@ export class RouteManager {
|
|
|
258
286
|
const metadata = {
|
|
259
287
|
type: param.type,
|
|
260
288
|
data: param.name,
|
|
261
|
-
metatype:
|
|
289
|
+
metatype: paramTypes?.[param.index]
|
|
262
290
|
};
|
|
263
291
|
// Run shared pipes (global + controller + method)
|
|
264
292
|
for (const pipe of pipes){
|
|
@@ -267,7 +295,7 @@ export class RouteManager {
|
|
|
267
295
|
// Run param-level pipes
|
|
268
296
|
if (param.pipes && param.pipes.length > 0) {
|
|
269
297
|
for (const paramPipe of param.pipes){
|
|
270
|
-
const pipeInstance = this.instantiate(paramPipe);
|
|
298
|
+
const pipeInstance = this.instantiate(paramPipe, requestContainer);
|
|
271
299
|
value = await pipeInstance.transform(value, metadata);
|
|
272
300
|
}
|
|
273
301
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -34,3 +34,6 @@ export { MetadataRegistry } from './registry/index';
|
|
|
34
34
|
export { RouteManager } from './http/index';
|
|
35
35
|
export { ModuleLoader } from './module/index';
|
|
36
36
|
export { ComponentManager } from './pipeline/index';
|
|
37
|
+
export { createZodDto, ValidationPipe } from './validation/index';
|
|
38
|
+
export { Serialize, SerializerInterceptor, SERIALIZE_METADATA } from './serialization/index';
|
|
39
|
+
export { Test, TestingModule, TestingModuleBuilder } from './testing/index';
|
package/dist/index.js
CHANGED
|
@@ -40,3 +40,9 @@ export { RouteManager } from "./http/index.js";
|
|
|
40
40
|
export { ModuleLoader } from "./module/index.js";
|
|
41
41
|
// Component Manager (for advanced usage)
|
|
42
42
|
export { ComponentManager } from "./pipeline/index.js";
|
|
43
|
+
// Validation
|
|
44
|
+
export { createZodDto, ValidationPipe } from "./validation/index.js";
|
|
45
|
+
// Serialization
|
|
46
|
+
export { Serialize, SerializerInterceptor, SERIALIZE_METADATA } from "./serialization/index.js";
|
|
47
|
+
// Testing
|
|
48
|
+
export { Test, TestingModule, TestingModuleBuilder } from "./testing/index.js";
|
|
@@ -9,12 +9,16 @@ export declare class ModuleLoader {
|
|
|
9
9
|
private collectedControllers;
|
|
10
10
|
private registeredProviders;
|
|
11
11
|
private moduleExportsCache;
|
|
12
|
+
private appProviderCounter;
|
|
13
|
+
private appProviderTokens;
|
|
12
14
|
constructor(container: Container, router: RouteManager);
|
|
13
15
|
load(rootModule: Type): void;
|
|
14
16
|
private processModule;
|
|
15
17
|
private registerProvider;
|
|
18
|
+
private isAppToken;
|
|
16
19
|
private buildExportSet;
|
|
17
20
|
getControllers(): Type[];
|
|
18
21
|
getRegisteredProviders(): Token[];
|
|
22
|
+
getAppProviderTokens(token: Token): Token[];
|
|
19
23
|
resolveAllInstances(): unknown[];
|
|
20
24
|
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Scope } from "../constants.js";
|
|
2
|
+
import { InjectionToken } from "../container/types.js";
|
|
3
|
+
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
1
4
|
import { getModuleMetadata, isModule } from "./decorators.js";
|
|
2
5
|
function isDynamicModule(value) {
|
|
3
6
|
return typeof value === 'object' && value !== null && 'module' in value && typeof value.module === 'function';
|
|
@@ -10,6 +13,29 @@ export class ModuleLoader {
|
|
|
10
13
|
collectedControllers = [];
|
|
11
14
|
registeredProviders = [];
|
|
12
15
|
moduleExportsCache = new Map();
|
|
16
|
+
appProviderCounter = 0;
|
|
17
|
+
appProviderTokens = new Map([
|
|
18
|
+
[
|
|
19
|
+
APP_GUARD,
|
|
20
|
+
[]
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
APP_PIPE,
|
|
24
|
+
[]
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
APP_INTERCEPTOR,
|
|
28
|
+
[]
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
APP_FILTER,
|
|
32
|
+
[]
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
APP_MIDDLEWARE,
|
|
36
|
+
[]
|
|
37
|
+
]
|
|
38
|
+
]);
|
|
13
39
|
constructor(container, router){
|
|
14
40
|
this.container = container;
|
|
15
41
|
this.router = router;
|
|
@@ -98,14 +124,29 @@ export class ModuleLoader {
|
|
|
98
124
|
}
|
|
99
125
|
} else {
|
|
100
126
|
const token = provider.token;
|
|
101
|
-
if (
|
|
127
|
+
if (!token) {
|
|
102
128
|
this.container.register(provider);
|
|
103
|
-
|
|
104
|
-
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (this.isAppToken(token)) {
|
|
132
|
+
const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
|
|
133
|
+
this.container.register({
|
|
134
|
+
...provider,
|
|
135
|
+
token: syntheticToken
|
|
136
|
+
});
|
|
137
|
+
this.registeredProviders.push(syntheticToken);
|
|
138
|
+
this.appProviderTokens.get(token).push(syntheticToken);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!this.container.has(token)) {
|
|
105
142
|
this.container.register(provider);
|
|
143
|
+
this.registeredProviders.push(token);
|
|
106
144
|
}
|
|
107
145
|
}
|
|
108
146
|
}
|
|
147
|
+
isAppToken(token) {
|
|
148
|
+
return token === APP_GUARD || token === APP_PIPE || token === APP_INTERCEPTOR || token === APP_FILTER || token === APP_MIDDLEWARE;
|
|
149
|
+
}
|
|
109
150
|
buildExportSet(exports, providers, importedProviders) {
|
|
110
151
|
const exportSet = new Set();
|
|
111
152
|
for (const exported of exports){
|
|
@@ -134,10 +175,18 @@ export class ModuleLoader {
|
|
|
134
175
|
...this.registeredProviders
|
|
135
176
|
];
|
|
136
177
|
}
|
|
178
|
+
getAppProviderTokens(token) {
|
|
179
|
+
return [
|
|
180
|
+
...this.appProviderTokens.get(token) ?? []
|
|
181
|
+
];
|
|
182
|
+
}
|
|
137
183
|
resolveAllInstances() {
|
|
138
184
|
const instances = [];
|
|
139
185
|
for (const token of this.registeredProviders){
|
|
140
186
|
try {
|
|
187
|
+
if (this.container.getProviderScope(token) === Scope.REQUEST) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
141
190
|
const instance = this.container.resolve(token);
|
|
142
191
|
instances.push(instance);
|
|
143
192
|
} catch {
|
|
@@ -146,6 +195,9 @@ export class ModuleLoader {
|
|
|
146
195
|
}
|
|
147
196
|
for (const controller of this.collectedControllers){
|
|
148
197
|
try {
|
|
198
|
+
if (this.container.getProviderScope(controller) === Scope.REQUEST) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
149
201
|
const instance = this.container.resolve(controller);
|
|
150
202
|
if (!instances.includes(instance)) {
|
|
151
203
|
instances.push(instance);
|
|
@@ -2,10 +2,19 @@ import type { OnApplicationBootstrap, OnModuleDestroy } from '../lifecycle/index
|
|
|
2
2
|
import { ScheduleRegistry } from './schedule.registry';
|
|
3
3
|
export declare class ScheduleExecutor implements OnApplicationBootstrap, OnModuleDestroy {
|
|
4
4
|
private registry;
|
|
5
|
-
private
|
|
5
|
+
private intervalTimers;
|
|
6
|
+
private cronTimers;
|
|
7
|
+
private cronMatcherCache;
|
|
8
|
+
private lastCronMinute;
|
|
6
9
|
private running;
|
|
7
10
|
constructor(registry: ScheduleRegistry);
|
|
8
11
|
onApplicationBootstrap(): void;
|
|
9
|
-
private
|
|
12
|
+
private scheduleInterval;
|
|
13
|
+
private scheduleCron;
|
|
14
|
+
private invoke;
|
|
15
|
+
private getCronMatcher;
|
|
16
|
+
private parseField;
|
|
17
|
+
private parseRange;
|
|
18
|
+
private parseCronNumber;
|
|
10
19
|
onModuleDestroy(): void;
|
|
11
20
|
}
|
|
@@ -11,38 +11,153 @@ import { Injectable } from "../container/index.js";
|
|
|
11
11
|
import { ScheduleRegistry } from "./schedule.registry.js";
|
|
12
12
|
export class ScheduleExecutor {
|
|
13
13
|
registry;
|
|
14
|
-
|
|
14
|
+
intervalTimers = [];
|
|
15
|
+
cronTimers = [];
|
|
16
|
+
cronMatcherCache = new Map();
|
|
17
|
+
lastCronMinute = new Map();
|
|
15
18
|
running = true;
|
|
16
19
|
constructor(registry){
|
|
17
20
|
this.registry = registry;
|
|
18
21
|
}
|
|
19
22
|
onApplicationBootstrap() {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const intervalJobs = this.registry.getIntervalJobs();
|
|
24
|
+
const cronJobs = this.registry.getCronJobs();
|
|
25
|
+
for (const job of intervalJobs){
|
|
26
|
+
this.scheduleInterval(job.instance, job.methodName, job.ms);
|
|
27
|
+
}
|
|
28
|
+
for(let i = 0; i < cronJobs.length; i++){
|
|
29
|
+
const job = cronJobs[i];
|
|
30
|
+
this.scheduleCron(job.instance, job.methodName, job.expression, i);
|
|
23
31
|
}
|
|
24
32
|
}
|
|
25
|
-
|
|
33
|
+
scheduleInterval(instance, methodName, ms) {
|
|
26
34
|
if (!this.running) return;
|
|
27
|
-
const timer =
|
|
28
|
-
|
|
29
|
-
const method = instance[methodName];
|
|
30
|
-
if (typeof method === 'function') {
|
|
31
|
-
await method.call(instance);
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
// Swallow errors to keep the loop alive
|
|
35
|
-
}
|
|
36
|
-
this.scheduleNext(instance, methodName, ms);
|
|
35
|
+
const timer = setInterval(()=>{
|
|
36
|
+
void this.invoke(instance, methodName);
|
|
37
37
|
}, ms);
|
|
38
|
-
this.
|
|
38
|
+
this.intervalTimers.push(timer);
|
|
39
|
+
}
|
|
40
|
+
scheduleCron(instance, methodName, expression, index) {
|
|
41
|
+
if (!this.running) return;
|
|
42
|
+
const matcher = this.getCronMatcher(expression);
|
|
43
|
+
if (!matcher) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const jobKey = `${index}:${methodName}:${expression}`;
|
|
47
|
+
const timer = setInterval(()=>{
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const minuteKey = Math.floor(now.getTime() / 60_000);
|
|
50
|
+
if (this.lastCronMinute.get(jobKey) === minuteKey) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!matcher(now)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.lastCronMinute.set(jobKey, minuteKey);
|
|
57
|
+
void this.invoke(instance, methodName);
|
|
58
|
+
}, 1000);
|
|
59
|
+
this.cronTimers.push(timer);
|
|
60
|
+
}
|
|
61
|
+
async invoke(instance, methodName) {
|
|
62
|
+
try {
|
|
63
|
+
const method = instance[methodName];
|
|
64
|
+
if (typeof method === 'function') {
|
|
65
|
+
await method.call(instance);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Swallow errors to keep the scheduler alive
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
getCronMatcher(expression) {
|
|
72
|
+
if (this.cronMatcherCache.has(expression)) {
|
|
73
|
+
return this.cronMatcherCache.get(expression) ?? null;
|
|
74
|
+
}
|
|
75
|
+
const fields = expression.trim().split(/\s+/);
|
|
76
|
+
if (fields.length !== 5) {
|
|
77
|
+
this.cronMatcherCache.set(expression, null);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const [minuteField, hourField, dayField, monthField, weekdayField] = fields;
|
|
81
|
+
const minuteMatcher = this.parseField(minuteField, 0, 59);
|
|
82
|
+
const hourMatcher = this.parseField(hourField, 0, 23);
|
|
83
|
+
const dayMatcher = this.parseField(dayField, 1, 31);
|
|
84
|
+
const monthMatcher = this.parseField(monthField, 1, 12);
|
|
85
|
+
const weekdayMatcher = this.parseField(weekdayField, 0, 7);
|
|
86
|
+
if (!minuteMatcher || !hourMatcher || !dayMatcher || !monthMatcher || !weekdayMatcher) {
|
|
87
|
+
this.cronMatcherCache.set(expression, null);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const matcher = (date)=>{
|
|
91
|
+
const weekday = date.getDay();
|
|
92
|
+
return minuteMatcher(date.getMinutes()) && hourMatcher(date.getHours()) && dayMatcher(date.getDate()) && monthMatcher(date.getMonth() + 1) && weekdayMatcher(weekday);
|
|
93
|
+
};
|
|
94
|
+
this.cronMatcherCache.set(expression, matcher);
|
|
95
|
+
return matcher;
|
|
96
|
+
}
|
|
97
|
+
parseField(field, min, max) {
|
|
98
|
+
const segments = field.split(',');
|
|
99
|
+
const predicates = [];
|
|
100
|
+
for (const rawSegment of segments){
|
|
101
|
+
const segment = rawSegment.trim();
|
|
102
|
+
if (!segment) return null;
|
|
103
|
+
const stepParts = segment.split('/');
|
|
104
|
+
if (stepParts.length > 2) return null;
|
|
105
|
+
const baseSegment = stepParts[0];
|
|
106
|
+
const step = stepParts.length === 2 ? Number(stepParts[1]) : 1;
|
|
107
|
+
if (!Number.isInteger(step) || step <= 0) return null;
|
|
108
|
+
const range = this.parseRange(baseSegment, min, max);
|
|
109
|
+
if (!range) return null;
|
|
110
|
+
predicates.push((value)=>{
|
|
111
|
+
if (value < range.start || value > range.end) return false;
|
|
112
|
+
return (value - range.start) % step === 0;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return (value)=>predicates.some((predicate)=>predicate(value));
|
|
116
|
+
}
|
|
117
|
+
parseRange(segment, min, max) {
|
|
118
|
+
if (segment === '*') {
|
|
119
|
+
return {
|
|
120
|
+
start: min,
|
|
121
|
+
end: max
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const bounds = segment.split('-');
|
|
125
|
+
if (bounds.length === 1) {
|
|
126
|
+
const value = this.parseCronNumber(bounds[0], min, max);
|
|
127
|
+
if (value === null) return null;
|
|
128
|
+
return {
|
|
129
|
+
start: value,
|
|
130
|
+
end: value
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (bounds.length !== 2) return null;
|
|
134
|
+
const start = this.parseCronNumber(bounds[0], min, max);
|
|
135
|
+
const end = this.parseCronNumber(bounds[1], min, max);
|
|
136
|
+
if (start === null || end === null || start > end) return null;
|
|
137
|
+
return {
|
|
138
|
+
start,
|
|
139
|
+
end
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
parseCronNumber(raw, min, max) {
|
|
143
|
+
const value = Number(raw);
|
|
144
|
+
if (!Number.isInteger(value)) return null;
|
|
145
|
+
// Cron allows both 0 and 7 for Sunday.
|
|
146
|
+
const normalized = max === 7 && value === 7 ? 0 : value;
|
|
147
|
+
if (normalized < min || normalized > max) return null;
|
|
148
|
+
return normalized;
|
|
39
149
|
}
|
|
40
150
|
onModuleDestroy() {
|
|
41
151
|
this.running = false;
|
|
42
|
-
for (const timer of this.
|
|
43
|
-
|
|
152
|
+
for (const timer of this.intervalTimers){
|
|
153
|
+
clearInterval(timer);
|
|
154
|
+
}
|
|
155
|
+
for (const timer of this.cronTimers){
|
|
156
|
+
clearInterval(timer);
|
|
44
157
|
}
|
|
45
|
-
this.
|
|
158
|
+
this.intervalTimers = [];
|
|
159
|
+
this.cronTimers = [];
|
|
160
|
+
this.lastCronMinute.clear();
|
|
46
161
|
}
|
|
47
162
|
}
|
|
48
163
|
ScheduleExecutor = _ts_decorate([
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
2
|
+
export const SERIALIZE_METADATA = 'vela:serialize';
|
|
3
|
+
export function Serialize(dto) {
|
|
4
|
+
return (target, propertyKey)=>{
|
|
5
|
+
MetadataRegistry.setCustomHandlerMeta(target.constructor, propertyKey, SERIALIZE_METADATA, dto);
|
|
6
|
+
};
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
2
|
+
import { SERIALIZE_METADATA } from "./serialize.decorator.js";
|
|
3
|
+
export class SerializerInterceptor {
|
|
4
|
+
async intercept(context, next) {
|
|
5
|
+
const result = await next.handle();
|
|
6
|
+
const controller = context.getClass();
|
|
7
|
+
const handler = context.getHandler();
|
|
8
|
+
const dto = MetadataRegistry.getCustomHandlerMeta(controller, handler, SERIALIZE_METADATA);
|
|
9
|
+
const schema = dto?.schema;
|
|
10
|
+
if (!schema?.parse) return result;
|
|
11
|
+
if (Array.isArray(result)) {
|
|
12
|
+
return result.map((item)=>schema.parse(item));
|
|
13
|
+
}
|
|
14
|
+
return schema.parse(result);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Token, Type } from '../container/types';
|
|
2
|
+
import type { ModuleOptions } from '../module/types';
|
|
3
|
+
import { TestingModule } from './testing.module';
|
|
4
|
+
export declare class OverrideBy {
|
|
5
|
+
private readonly builder;
|
|
6
|
+
private readonly token;
|
|
7
|
+
constructor(builder: TestingModuleBuilder, token: Token);
|
|
8
|
+
useValue(value: unknown): TestingModuleBuilder;
|
|
9
|
+
useClass(cls: Type): TestingModuleBuilder;
|
|
10
|
+
useFactory(options: {
|
|
11
|
+
factory: (...args: unknown[]) => unknown;
|
|
12
|
+
inject?: Token[];
|
|
13
|
+
}): TestingModuleBuilder;
|
|
14
|
+
}
|
|
15
|
+
export declare class TestingModuleBuilder {
|
|
16
|
+
private readonly metadata;
|
|
17
|
+
private overrides;
|
|
18
|
+
constructor(metadata: ModuleOptions);
|
|
19
|
+
overrideProvider(token: Token): OverrideBy;
|
|
20
|
+
overrideGuard(guard: Type): OverrideBy;
|
|
21
|
+
overridePipe(pipe: Type): OverrideBy;
|
|
22
|
+
overrideInterceptor(interceptor: Type): OverrideBy;
|
|
23
|
+
overrideFilter(filter: Type): OverrideBy;
|
|
24
|
+
private addOverride;
|
|
25
|
+
compile(): Promise<TestingModule>;
|
|
26
|
+
}
|
|
27
|
+
export declare class Test {
|
|
28
|
+
static createTestingModule(metadata: ModuleOptions): TestingModuleBuilder;
|
|
29
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { VelaApplication } from "../application.js";
|
|
2
|
+
import { METADATA_KEYS } from "../constants.js";
|
|
3
|
+
import { Container } from "../container/container.js";
|
|
4
|
+
import { RouteManager } from "../http/route.manager.js";
|
|
5
|
+
import { defineMetadata } from "../metadata.js";
|
|
6
|
+
import { ModuleLoader } from "../module/module-loader.js";
|
|
7
|
+
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
8
|
+
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
9
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
10
|
+
import { TestingModule } from "./testing.module.js";
|
|
11
|
+
export class OverrideBy {
|
|
12
|
+
builder;
|
|
13
|
+
token;
|
|
14
|
+
constructor(builder, token){
|
|
15
|
+
this.builder = builder;
|
|
16
|
+
this.token = token;
|
|
17
|
+
}
|
|
18
|
+
useValue(value) {
|
|
19
|
+
this.builder['addOverride']({
|
|
20
|
+
token: this.token,
|
|
21
|
+
provider: {
|
|
22
|
+
token: this.token,
|
|
23
|
+
useValue: value
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return this.builder;
|
|
27
|
+
}
|
|
28
|
+
useClass(cls) {
|
|
29
|
+
this.builder['addOverride']({
|
|
30
|
+
token: this.token,
|
|
31
|
+
provider: {
|
|
32
|
+
token: this.token,
|
|
33
|
+
useClass: cls
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return this.builder;
|
|
37
|
+
}
|
|
38
|
+
useFactory(options) {
|
|
39
|
+
this.builder['addOverride']({
|
|
40
|
+
token: this.token,
|
|
41
|
+
provider: {
|
|
42
|
+
token: this.token,
|
|
43
|
+
useFactory: options.factory,
|
|
44
|
+
inject: options.inject
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return this.builder;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class TestingModuleBuilder {
|
|
51
|
+
metadata;
|
|
52
|
+
overrides = [];
|
|
53
|
+
constructor(metadata){
|
|
54
|
+
this.metadata = metadata;
|
|
55
|
+
}
|
|
56
|
+
overrideProvider(token) {
|
|
57
|
+
return new OverrideBy(this, token);
|
|
58
|
+
}
|
|
59
|
+
overrideGuard(guard) {
|
|
60
|
+
return new OverrideBy(this, guard);
|
|
61
|
+
}
|
|
62
|
+
overridePipe(pipe) {
|
|
63
|
+
return new OverrideBy(this, pipe);
|
|
64
|
+
}
|
|
65
|
+
overrideInterceptor(interceptor) {
|
|
66
|
+
return new OverrideBy(this, interceptor);
|
|
67
|
+
}
|
|
68
|
+
overrideFilter(filter) {
|
|
69
|
+
return new OverrideBy(this, filter);
|
|
70
|
+
}
|
|
71
|
+
addOverride(entry) {
|
|
72
|
+
// Replace existing override for same token if any
|
|
73
|
+
const idx = this.overrides.findIndex((o)=>o.token === entry.token);
|
|
74
|
+
if (idx !== -1) {
|
|
75
|
+
this.overrides[idx] = entry;
|
|
76
|
+
} else {
|
|
77
|
+
this.overrides.push(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async compile() {
|
|
81
|
+
// Create a temporary module class with the provided metadata
|
|
82
|
+
let TestRootModule = class TestRootModule {
|
|
83
|
+
};
|
|
84
|
+
MetadataRegistry.setModuleOptions(TestRootModule, {
|
|
85
|
+
imports: this.metadata.imports,
|
|
86
|
+
providers: this.metadata.providers,
|
|
87
|
+
controllers: this.metadata.controllers,
|
|
88
|
+
exports: this.metadata.exports
|
|
89
|
+
});
|
|
90
|
+
defineMetadata(METADATA_KEYS.MODULE, true, TestRootModule);
|
|
91
|
+
// Bootstrap (mirrors VelaFactory.create)
|
|
92
|
+
const container = new Container();
|
|
93
|
+
container.register({
|
|
94
|
+
token: Container,
|
|
95
|
+
useValue: container
|
|
96
|
+
});
|
|
97
|
+
// Register overrides BEFORE module loading so ModuleLoader skips originals
|
|
98
|
+
for (const override of this.overrides){
|
|
99
|
+
container.register(override.provider);
|
|
100
|
+
}
|
|
101
|
+
const routeManager = new RouteManager(container);
|
|
102
|
+
ComponentManager.init(container);
|
|
103
|
+
const loader = new ModuleLoader(container, routeManager);
|
|
104
|
+
loader.load(TestRootModule);
|
|
105
|
+
const appGuards = loader.getAppProviderTokens(APP_GUARD);
|
|
106
|
+
if (appGuards.length > 0) {
|
|
107
|
+
routeManager.useGlobalGuardTokens(...appGuards);
|
|
108
|
+
} else if (container.has(APP_GUARD)) {
|
|
109
|
+
routeManager.useGlobalGuardTokens(APP_GUARD);
|
|
110
|
+
}
|
|
111
|
+
const appPipes = loader.getAppProviderTokens(APP_PIPE);
|
|
112
|
+
if (appPipes.length > 0) {
|
|
113
|
+
routeManager.useGlobalPipeTokens(...appPipes);
|
|
114
|
+
} else if (container.has(APP_PIPE)) {
|
|
115
|
+
routeManager.useGlobalPipeTokens(APP_PIPE);
|
|
116
|
+
}
|
|
117
|
+
const appInterceptors = loader.getAppProviderTokens(APP_INTERCEPTOR);
|
|
118
|
+
if (appInterceptors.length > 0) {
|
|
119
|
+
routeManager.useGlobalInterceptorTokens(...appInterceptors);
|
|
120
|
+
} else if (container.has(APP_INTERCEPTOR)) {
|
|
121
|
+
routeManager.useGlobalInterceptorTokens(APP_INTERCEPTOR);
|
|
122
|
+
}
|
|
123
|
+
const appFilters = loader.getAppProviderTokens(APP_FILTER);
|
|
124
|
+
if (appFilters.length > 0) {
|
|
125
|
+
routeManager.useGlobalFilterTokens(...appFilters);
|
|
126
|
+
} else if (container.has(APP_FILTER)) {
|
|
127
|
+
routeManager.useGlobalFilterTokens(APP_FILTER);
|
|
128
|
+
}
|
|
129
|
+
const appMiddleware = loader.getAppProviderTokens(APP_MIDDLEWARE);
|
|
130
|
+
if (appMiddleware.length > 0) {
|
|
131
|
+
routeManager.useGlobalMiddlewareTokens(...appMiddleware);
|
|
132
|
+
} else if (container.has(APP_MIDDLEWARE)) {
|
|
133
|
+
routeManager.useGlobalMiddlewareTokens(APP_MIDDLEWARE);
|
|
134
|
+
}
|
|
135
|
+
const app = new VelaApplication(container, routeManager);
|
|
136
|
+
const instances = loader.resolveAllInstances();
|
|
137
|
+
app.setInstances(instances);
|
|
138
|
+
await app.callOnModuleInit();
|
|
139
|
+
await app.callOnApplicationBootstrap();
|
|
140
|
+
await app.initRoutes();
|
|
141
|
+
return new TestingModule(app);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export class Test {
|
|
145
|
+
static createTestingModule(metadata) {
|
|
146
|
+
return new TestingModuleBuilder(metadata);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { VelaApplication } from '../application';
|
|
2
|
+
import type { Token } from '../container/types';
|
|
3
|
+
export declare class TestingModule {
|
|
4
|
+
private readonly app;
|
|
5
|
+
constructor(app: VelaApplication);
|
|
6
|
+
get<T>(token: Token<T>): T;
|
|
7
|
+
close(signal?: string): Promise<void>;
|
|
8
|
+
createNestApplication(): VelaApplication;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BadRequestException } from "../errors/http-exception.js";
|
|
2
|
+
export class ValidationPipe {
|
|
3
|
+
transform(value, metadata) {
|
|
4
|
+
const metatype = metadata.metatype;
|
|
5
|
+
if (!metatype?.schema?.parse) return value;
|
|
6
|
+
try {
|
|
7
|
+
return metatype.schema.parse(value);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error && typeof error === 'object' && 'issues' in error && Array.isArray(error.issues)) {
|
|
10
|
+
throw new BadRequestException('Validation failed', {
|
|
11
|
+
statusCode: 400,
|
|
12
|
+
message: 'Validation failed',
|
|
13
|
+
errors: error.issues
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/vela",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "NestJS-compatible framework for edge runtimes, powered by Hono",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"@types/bun": "latest",
|
|
67
67
|
"typescript": "^5",
|
|
68
68
|
"unplugin-swc": "^1.5.9",
|
|
69
|
-
"vitest": "^4.0.18"
|
|
69
|
+
"vitest": "^4.0.18",
|
|
70
|
+
"zod": "^4.3.6"
|
|
70
71
|
},
|
|
71
72
|
"scripts": {
|
|
72
73
|
"build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|