@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.
@@ -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
  }
@@ -22,6 +22,7 @@ export interface ProviderOptions<T = unknown> {
22
22
  scope?: Scope;
23
23
  useValue?: T;
24
24
  useFactory?: (...args: any[]) => T | Promise<T>;
25
+ useClass?: Type<T>;
25
26
  inject?: Token[];
26
27
  useExisting?: Token<T>;
27
28
  }
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
- // Resolve APP_* tokens registered via module providers
19
- if (container.has(APP_GUARD)) {
20
- routeManager.useGlobalGuards(container.resolve(APP_GUARD));
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
- if (container.has(APP_PIPE)) {
23
- routeManager.useGlobalPipes(container.resolve(APP_PIPE));
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
- if (container.has(APP_INTERCEPTOR)) {
26
- routeManager.useGlobalInterceptors(container.resolve(APP_INTERCEPTOR));
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
- if (container.has(APP_FILTER)) {
29
- routeManager.useGlobalFilters(container.resolve(APP_FILTER));
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
- if (container.has(APP_MIDDLEWARE)) {
32
- routeManager.useGlobalMiddleware(container.resolve(APP_MIDDLEWARE));
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
- for (const mw of middleware){
39
- this.globalMiddleware.push(this.instantiate(mw));
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
- for (const pipe of pipes){
45
- this.globalPipes.push(this.instantiate(pipe));
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
- for (const guard of guards){
51
- this.globalGuards.push(this.instantiate(guard));
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
- for (const interceptor of interceptors){
57
- this.globalInterceptors.push(this.instantiate(interceptor));
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
- for (const filter of filters){
63
- this.globalFilters.push(this.instantiate(filter));
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 !== 'function') {
69
- return classOrInstance;
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
- if (this.container.has(classOrInstance)) {
72
- return this.container.resolve(classOrInstance);
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
- return new classOrInstance();
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)=>mw.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 resolvedMw = ComponentManager.resolveMiddleware(middlewareItems);
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 mw of resolvedMw){
115
- app.use(fullPath, (c, next)=>mw.use(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(instance, route, controller, allParamMetadata) {
180
+ createHandler(route, controller, allParamMetadata) {
150
181
  const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
151
- // Pre-resolve controller + method level components at build time
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 resolvedMethodFilters = ComponentManager.resolveFilters([
161
- ...methodFilters
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
- ...resolvedMethodGuards
200
+ ...this.instantiateMany(this.globalGuards, requestContainer),
201
+ ...this.instantiateMany(methodGuards, requestContainer)
175
202
  ];
176
203
  const pipes = [
177
- ...this.globalPipes,
178
- ...resolvedMethodPipes
204
+ ...this.instantiateMany(this.globalPipes, requestContainer),
205
+ ...this.instantiateMany(methodPipes, requestContainer)
179
206
  ];
180
207
  const interceptors = [
181
- ...this.globalInterceptors,
182
- ...resolvedMethodInterceptors
208
+ ...this.instantiateMany(this.globalInterceptors, requestContainer),
209
+ ...this.instantiateMany(methodInterceptors, requestContainer)
183
210
  ];
184
211
  const filters = [
185
- ...resolvedMethodFilters,
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: undefined
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 (token && !this.container.has(token)) {
127
+ if (!token) {
102
128
  this.container.register(provider);
103
- this.registeredProviders.push(token);
104
- } else if (!token) {
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 timers;
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 scheduleNext;
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
- timers = [];
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 jobs = this.registry.getIntervalJobs();
21
- for (const job of jobs){
22
- this.scheduleNext(job.instance, job.methodName, job.ms);
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
- scheduleNext(instance, methodName, ms) {
33
+ scheduleInterval(instance, methodName, ms) {
26
34
  if (!this.running) return;
27
- const timer = setTimeout(async ()=>{
28
- try {
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.timers.push(timer);
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.timers){
43
- clearTimeout(timer);
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.timers = [];
158
+ this.intervalTimers = [];
159
+ this.cronTimers = [];
160
+ this.lastCronMinute.clear();
46
161
  }
47
162
  }
48
163
  ScheduleExecutor = _ts_decorate([
@@ -0,0 +1,2 @@
1
+ export { Serialize, SERIALIZE_METADATA } from './serialize.decorator';
2
+ export { SerializerInterceptor } from './serializer.interceptor';
@@ -0,0 +1,2 @@
1
+ export { Serialize, SERIALIZE_METADATA } from "./serialize.decorator.js";
2
+ export { SerializerInterceptor } from "./serializer.interceptor.js";
@@ -0,0 +1,6 @@
1
+ export declare const SERIALIZE_METADATA = "vela:serialize";
2
+ export declare function Serialize(dto: {
3
+ schema: {
4
+ parse(data: unknown): unknown;
5
+ };
6
+ }): MethodDecorator;
@@ -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,4 @@
1
+ import type { CallHandler, ExecutionContext, NestInterceptor } from '../pipeline/types';
2
+ export declare class SerializerInterceptor implements NestInterceptor {
3
+ intercept(context: ExecutionContext, next: CallHandler): Promise<unknown>;
4
+ }
@@ -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,2 @@
1
+ export { Test, TestingModuleBuilder, OverrideBy } from './testing.builder';
2
+ export { TestingModule } from './testing.module';
@@ -0,0 +1,2 @@
1
+ export { Test, TestingModuleBuilder, OverrideBy } from "./testing.builder.js";
2
+ export { TestingModule } from "./testing.module.js";
@@ -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,15 @@
1
+ export class TestingModule {
2
+ app;
3
+ constructor(app){
4
+ this.app = app;
5
+ }
6
+ get(token) {
7
+ return this.app.get(token);
8
+ }
9
+ async close(signal) {
10
+ await this.app.close(signal);
11
+ }
12
+ createNestApplication() {
13
+ return this.app;
14
+ }
15
+ }
@@ -0,0 +1,8 @@
1
+ interface ZodSchema {
2
+ parse(data: unknown): unknown;
3
+ }
4
+ export declare function createZodDto<T extends ZodSchema>(schema: T): {
5
+ new (): unknown;
6
+ schema: T;
7
+ };
8
+ export {};
@@ -0,0 +1,6 @@
1
+ export function createZodDto(schema) {
2
+ let ZodDto = class ZodDto {
3
+ static schema = schema;
4
+ };
5
+ return ZodDto;
6
+ }
@@ -0,0 +1,2 @@
1
+ export { createZodDto } from './create-zod-dto';
2
+ export { ValidationPipe } from './validation.pipe';
@@ -0,0 +1,2 @@
1
+ export { createZodDto } from "./create-zod-dto.js";
2
+ export { ValidationPipe } from "./validation.pipe.js";
@@ -0,0 +1,4 @@
1
+ import type { ArgumentMetadata, PipeTransform } from '../pipeline/types';
2
+ export declare class ValidationPipe implements PipeTransform {
3
+ transform(value: unknown, metadata: ArgumentMetadata): unknown;
4
+ }
@@ -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.1",
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",