@velajs/vela 0.4.2 → 0.5.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/dist/cache/cache.module.d.ts +4 -1
- package/dist/cache/cache.module.js +51 -9
- package/dist/config/config.module.d.ts +4 -1
- package/dist/config/config.module.js +40 -3
- package/dist/config/config.types.d.ts +1 -0
- package/dist/constants.d.ts +6 -1
- package/dist/constants.js +5 -0
- package/dist/container/container.d.ts +4 -0
- package/dist/container/container.js +52 -15
- package/dist/container/decorators.d.ts +3 -1
- package/dist/container/decorators.js +27 -6
- package/dist/container/index.d.ts +4 -2
- package/dist/container/index.js +4 -2
- package/dist/container/mixin.d.ts +24 -0
- package/dist/container/mixin.js +26 -0
- package/dist/container/module-ref.d.ts +21 -0
- package/dist/container/module-ref.js +48 -0
- package/dist/container/types.d.ts +10 -3
- package/dist/container/types.js +9 -0
- package/dist/cors/cors.module.js +2 -2
- package/dist/factory.js +36 -13
- package/dist/fetch/fetch.module.d.ts +6 -0
- package/dist/fetch/fetch.module.js +77 -0
- package/dist/fetch/fetch.service.d.ts +24 -0
- package/dist/fetch/fetch.service.js +145 -0
- package/dist/fetch/fetch.types.d.ts +24 -0
- package/dist/fetch/fetch.types.js +1 -0
- package/dist/fetch/index.d.ts +3 -0
- package/dist/fetch/index.js +2 -0
- package/dist/http/decorators.d.ts +64 -0
- package/dist/http/decorators.js +89 -1
- package/dist/http/index.d.ts +3 -1
- package/dist/http/index.js +2 -1
- package/dist/http/middleware-consumer.d.ts +36 -0
- package/dist/http/middleware-consumer.js +71 -0
- package/dist/http/route.manager.d.ts +13 -1
- package/dist/http/route.manager.js +153 -53
- package/dist/index.d.ts +11 -6
- package/dist/index.js +7 -4
- package/dist/metadata.d.ts +1 -1
- package/dist/module/decorators.d.ts +1 -0
- package/dist/module/decorators.js +24 -8
- package/dist/module/index.d.ts +2 -2
- package/dist/module/index.js +1 -1
- package/dist/module/module-loader.d.ts +9 -1
- package/dist/module/module-loader.js +107 -11
- package/dist/module/types.d.ts +13 -3
- package/dist/pipeline/index.d.ts +3 -2
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/pipes.d.ts +22 -0
- package/dist/pipeline/pipes.js +50 -0
- package/dist/pipeline/tokens.d.ts +1 -1
- package/dist/pipeline/tokens.js +1 -1
- package/dist/pipeline/types.d.ts +16 -0
- package/dist/registry/metadata.registry.d.ts +7 -6
- package/dist/registry/metadata.registry.js +13 -0
- package/dist/registry/types.d.ts +1 -0
- package/dist/schedule/schedule.executor.d.ts +11 -2
- package/dist/schedule/schedule.executor.js +134 -19
- package/dist/schedule/schedule.module.d.ts +4 -1
- package/dist/schedule/schedule.module.js +39 -7
- package/dist/testing/testing.builder.js +31 -17
- package/dist/throttler/throttler.module.d.ts +2 -1
- package/dist/throttler/throttler.module.js +47 -9
- package/package.json +1 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { Scope } from "../constants.js";
|
|
2
|
+
import { ForwardRef, InjectionToken } from "../container/types.js";
|
|
3
|
+
import { MiddlewareBuilder } from "../http/middleware-consumer.js";
|
|
4
|
+
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
|
|
5
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
1
6
|
import { getModuleMetadata, isModule } from "./decorators.js";
|
|
2
7
|
function isDynamicModule(value) {
|
|
3
8
|
return typeof value === 'object' && value !== null && 'module' in value && typeof value.module === 'function';
|
|
@@ -10,6 +15,31 @@ export class ModuleLoader {
|
|
|
10
15
|
collectedControllers = [];
|
|
11
16
|
registeredProviders = [];
|
|
12
17
|
moduleExportsCache = new Map();
|
|
18
|
+
globalExports = new Set();
|
|
19
|
+
consumerMiddlewareDefinitions = [];
|
|
20
|
+
appProviderCounter = 0;
|
|
21
|
+
appProviderTokens = new Map([
|
|
22
|
+
[
|
|
23
|
+
APP_GUARD,
|
|
24
|
+
[]
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
APP_PIPE,
|
|
28
|
+
[]
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
APP_INTERCEPTOR,
|
|
32
|
+
[]
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
APP_FILTER,
|
|
36
|
+
[]
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
APP_MIDDLEWARE,
|
|
40
|
+
[]
|
|
41
|
+
]
|
|
42
|
+
]);
|
|
13
43
|
constructor(container, router){
|
|
14
44
|
this.container = container;
|
|
15
45
|
this.router = router;
|
|
@@ -21,12 +51,14 @@ export class ModuleLoader {
|
|
|
21
51
|
}
|
|
22
52
|
}
|
|
23
53
|
processModule(moduleClassOrDynamic) {
|
|
24
|
-
// Handle dynamic modules ({ module, controllers, providers })
|
|
54
|
+
// Handle dynamic modules ({ module, imports, controllers, providers })
|
|
25
55
|
let moduleClass;
|
|
56
|
+
let extraImports = [];
|
|
26
57
|
let extraControllers = [];
|
|
27
58
|
let extraProviders = [];
|
|
28
59
|
if (isDynamicModule(moduleClassOrDynamic)) {
|
|
29
60
|
moduleClass = moduleClassOrDynamic.module;
|
|
61
|
+
extraImports = moduleClassOrDynamic.imports ?? [];
|
|
30
62
|
extraControllers = moduleClassOrDynamic.controllers ?? [];
|
|
31
63
|
extraProviders = moduleClassOrDynamic.providers ?? [];
|
|
32
64
|
} else {
|
|
@@ -57,8 +89,20 @@ export class ModuleLoader {
|
|
|
57
89
|
}
|
|
58
90
|
this.processingStack.add(moduleClass);
|
|
59
91
|
try {
|
|
60
|
-
const importedProviders = new Set();
|
|
61
|
-
for (const
|
|
92
|
+
const importedProviders = new Set(this.globalExports);
|
|
93
|
+
for (const entry of [
|
|
94
|
+
...metadata.imports,
|
|
95
|
+
...extraImports
|
|
96
|
+
]){
|
|
97
|
+
// Unwrap forwardRef(() => Module) — resolves lazy circular references
|
|
98
|
+
const isForwardRef = entry instanceof ForwardRef;
|
|
99
|
+
const importedModule = isForwardRef ? entry.factory() : entry;
|
|
100
|
+
const importedModuleClass = isDynamicModule(importedModule) ? importedModule.module : importedModule;
|
|
101
|
+
// If this forwardRef-wrapped import is currently being processed, skip it to
|
|
102
|
+
// break the circular chain. Non-forwardRef circular imports still throw.
|
|
103
|
+
if (isForwardRef && this.processingStack.has(importedModuleClass)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
62
106
|
const exportedTokens = this.processModule(importedModule);
|
|
63
107
|
for (const token of exportedTokens){
|
|
64
108
|
importedProviders.add(token);
|
|
@@ -82,9 +126,30 @@ export class ModuleLoader {
|
|
|
82
126
|
this.collectedControllers.push(controller);
|
|
83
127
|
}
|
|
84
128
|
}
|
|
129
|
+
// Propagate module-level Use* decorators (@UseGuards, @UseInterceptors, etc.) to each controller
|
|
130
|
+
for (const controller of allControllers){
|
|
131
|
+
MetadataRegistry.propagateControllerComponents(moduleClass, controller);
|
|
132
|
+
}
|
|
85
133
|
this.processedModules.add(moduleClass);
|
|
86
134
|
const exports = this.buildExportSet(metadata.exports, allProviders, importedProviders);
|
|
87
135
|
this.moduleExportsCache.set(moduleClass, exports);
|
|
136
|
+
const isGlobal = metadata.isGlobal || isDynamicModule(moduleClassOrDynamic) && moduleClassOrDynamic.global === true;
|
|
137
|
+
if (isGlobal) {
|
|
138
|
+
for (const token of exports){
|
|
139
|
+
this.globalExports.add(token);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Call configure() if the module implements NestModule
|
|
143
|
+
if (typeof moduleClass.prototype?.configure === 'function') {
|
|
144
|
+
try {
|
|
145
|
+
const instance = new moduleClass();
|
|
146
|
+
const builder = new MiddlewareBuilder();
|
|
147
|
+
instance.configure(builder);
|
|
148
|
+
this.consumerMiddlewareDefinitions.push(...builder.getDefinitions());
|
|
149
|
+
} catch {
|
|
150
|
+
// Module has constructor dependencies — configure() skipped
|
|
151
|
+
}
|
|
152
|
+
}
|
|
88
153
|
return exports;
|
|
89
154
|
} finally{
|
|
90
155
|
this.processingStack.delete(moduleClass);
|
|
@@ -97,15 +162,30 @@ export class ModuleLoader {
|
|
|
97
162
|
this.registeredProviders.push(provider);
|
|
98
163
|
}
|
|
99
164
|
} else {
|
|
100
|
-
const token = provider.
|
|
101
|
-
if (
|
|
165
|
+
const token = provider.provide;
|
|
166
|
+
if (!token) {
|
|
102
167
|
this.container.register(provider);
|
|
103
|
-
|
|
104
|
-
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (this.isAppToken(token)) {
|
|
171
|
+
const syntheticToken = new InjectionToken(`${token.toString()}:${this.appProviderCounter++}`);
|
|
172
|
+
this.container.register({
|
|
173
|
+
...provider,
|
|
174
|
+
provide: syntheticToken
|
|
175
|
+
});
|
|
176
|
+
this.registeredProviders.push(syntheticToken);
|
|
177
|
+
this.appProviderTokens.get(token).push(syntheticToken);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!this.container.has(token)) {
|
|
105
181
|
this.container.register(provider);
|
|
182
|
+
this.registeredProviders.push(token);
|
|
106
183
|
}
|
|
107
184
|
}
|
|
108
185
|
}
|
|
186
|
+
isAppToken(token) {
|
|
187
|
+
return token === APP_GUARD || token === APP_PIPE || token === APP_INTERCEPTOR || token === APP_FILTER || token === APP_MIDDLEWARE;
|
|
188
|
+
}
|
|
109
189
|
buildExportSet(exports, providers, importedProviders) {
|
|
110
190
|
const exportSet = new Set();
|
|
111
191
|
for (const exported of exports){
|
|
@@ -113,7 +193,7 @@ export class ModuleLoader {
|
|
|
113
193
|
if (typeof p === 'function') {
|
|
114
194
|
return p === exported;
|
|
115
195
|
}
|
|
116
|
-
return p.
|
|
196
|
+
return p.provide === exported;
|
|
117
197
|
});
|
|
118
198
|
const isImportedProvider = importedProviders.has(exported);
|
|
119
199
|
if (!isLocalProvider && !isImportedProvider) {
|
|
@@ -134,11 +214,24 @@ export class ModuleLoader {
|
|
|
134
214
|
...this.registeredProviders
|
|
135
215
|
];
|
|
136
216
|
}
|
|
137
|
-
|
|
217
|
+
getAppProviderTokens(token) {
|
|
218
|
+
return [
|
|
219
|
+
...this.appProviderTokens.get(token) ?? []
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
getConsumerMiddlewareDefinitions() {
|
|
223
|
+
return [
|
|
224
|
+
...this.consumerMiddlewareDefinitions
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
async resolveAllInstances() {
|
|
138
228
|
const instances = [];
|
|
139
229
|
for (const token of this.registeredProviders){
|
|
140
230
|
try {
|
|
141
|
-
|
|
231
|
+
if (this.container.getProviderScope(token) === Scope.REQUEST) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const instance = await this.container.resolveAsync(token);
|
|
142
235
|
instances.push(instance);
|
|
143
236
|
} catch {
|
|
144
237
|
// Skip unresolvable tokens
|
|
@@ -146,7 +239,10 @@ export class ModuleLoader {
|
|
|
146
239
|
}
|
|
147
240
|
for (const controller of this.collectedControllers){
|
|
148
241
|
try {
|
|
149
|
-
|
|
242
|
+
if (this.container.getProviderScope(controller) === Scope.REQUEST) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const instance = await this.container.resolveAsync(controller);
|
|
150
246
|
if (!instances.includes(instance)) {
|
|
151
247
|
instances.push(instance);
|
|
152
248
|
}
|
package/dist/module/types.d.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import type { InjectionToken, ProviderOptions, Type } from '../container/types';
|
|
1
|
+
import type { ForwardRef, InjectionToken, ProviderOptions, Token, Type } from '../container/types';
|
|
2
|
+
export type ModuleImport = Type | DynamicModule | ForwardRef;
|
|
3
|
+
export interface AsyncModuleOptions<T = unknown> {
|
|
4
|
+
imports?: ModuleImport[];
|
|
5
|
+
useFactory: (...args: any[]) => T | Promise<T>;
|
|
6
|
+
inject?: Token[];
|
|
7
|
+
}
|
|
2
8
|
export interface DynamicModule {
|
|
3
9
|
module: Type;
|
|
10
|
+
imports?: ModuleImport[];
|
|
4
11
|
providers?: Array<Type | ProviderOptions>;
|
|
5
12
|
controllers?: Type[];
|
|
6
13
|
exports?: Array<Type | InjectionToken>;
|
|
14
|
+
global?: boolean;
|
|
7
15
|
}
|
|
8
16
|
export interface ModuleOptions {
|
|
9
17
|
providers?: Array<Type | ProviderOptions>;
|
|
10
18
|
controllers?: Type[];
|
|
11
|
-
imports?:
|
|
19
|
+
imports?: ModuleImport[];
|
|
12
20
|
exports?: Array<Type | InjectionToken>;
|
|
21
|
+
isGlobal?: boolean;
|
|
13
22
|
}
|
|
14
23
|
export interface ModuleMetadata {
|
|
15
24
|
providers: Array<Type | ProviderOptions>;
|
|
16
25
|
controllers: Type[];
|
|
17
|
-
imports:
|
|
26
|
+
imports: ModuleImport[];
|
|
18
27
|
exports: Array<Type | InjectionToken>;
|
|
28
|
+
isGlobal: boolean;
|
|
19
29
|
}
|
package/dist/pipeline/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch,
|
|
|
3
3
|
export { SetMetadata, Reflector } from './reflector';
|
|
4
4
|
export type { ReflectableDecorator, CreateDecoratorOptions } from './reflector';
|
|
5
5
|
export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from './tokens';
|
|
6
|
-
export { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe, } from './pipes';
|
|
7
|
-
export type {
|
|
6
|
+
export { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe, ParseArrayPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe, } from './pipes';
|
|
7
|
+
export type { ParseUUIDPipeOptions, ParseArrayPipeOptions } from './pipes';
|
|
8
|
+
export type { HttpArgumentsHost, ExecutionContext, CanActivate, CallHandler, NestInterceptor, NestMiddleware, PipeTransform, ExceptionFilter, ArgumentMetadata, } from './types';
|
package/dist/pipeline/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { ComponentManager } from "./component.manager.js";
|
|
|
2
2
|
export { UseMiddleware, UseGuards, UsePipes, UseInterceptors, UseFilters, Catch, getCatchTypes, shouldFilterCatch } from "./decorators.js";
|
|
3
3
|
export { SetMetadata, Reflector } from "./reflector.js";
|
|
4
4
|
export { APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE } from "./tokens.js";
|
|
5
|
-
export { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe } from "./pipes.js";
|
|
5
|
+
export { ParseIntPipe, ParseFloatPipe, ParseBoolPipe, ParseUUIDPipe, ParseEnumPipe, ParseArrayPipe, DefaultValuePipe, RequiredPipe, ZodValidationPipe } from "./pipes.js";
|
package/dist/pipeline/pipes.d.ts
CHANGED
|
@@ -23,3 +23,25 @@ export declare class ZodValidationPipe implements PipeTransform {
|
|
|
23
23
|
});
|
|
24
24
|
transform(value: unknown, _metadata: ArgumentMetadata): unknown;
|
|
25
25
|
}
|
|
26
|
+
export interface ParseUUIDPipeOptions {
|
|
27
|
+
version?: '3' | '4' | '5';
|
|
28
|
+
}
|
|
29
|
+
export declare class ParseUUIDPipe implements PipeTransform<string, string> {
|
|
30
|
+
private readonly options?;
|
|
31
|
+
constructor(options?: ParseUUIDPipeOptions | undefined);
|
|
32
|
+
transform(value: string, metadata: ArgumentMetadata): string;
|
|
33
|
+
}
|
|
34
|
+
export declare class ParseEnumPipe<T extends Record<string, string | number>> implements PipeTransform<string, T[keyof T]> {
|
|
35
|
+
private readonly enumType;
|
|
36
|
+
constructor(enumType: T);
|
|
37
|
+
transform(value: string, metadata: ArgumentMetadata): T[keyof T];
|
|
38
|
+
}
|
|
39
|
+
export interface ParseArrayPipeOptions {
|
|
40
|
+
separator?: string;
|
|
41
|
+
optional?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare class ParseArrayPipe implements PipeTransform {
|
|
44
|
+
private readonly options?;
|
|
45
|
+
constructor(options?: ParseArrayPipeOptions | undefined);
|
|
46
|
+
transform(value: unknown, metadata: ArgumentMetadata): unknown[];
|
|
47
|
+
}
|
package/dist/pipeline/pipes.js
CHANGED
|
@@ -50,3 +50,53 @@ export class ZodValidationPipe {
|
|
|
50
50
|
return this.schema.parse(value);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
54
|
+
const UUID_VERSION_REGEX = {
|
|
55
|
+
'3': /^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
56
|
+
'4': /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
57
|
+
'5': /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
58
|
+
};
|
|
59
|
+
export class ParseUUIDPipe {
|
|
60
|
+
options;
|
|
61
|
+
constructor(options){
|
|
62
|
+
this.options = options;
|
|
63
|
+
}
|
|
64
|
+
transform(value, metadata) {
|
|
65
|
+
const regex = this.options?.version ? UUID_VERSION_REGEX[this.options.version] : UUID_REGEX;
|
|
66
|
+
if (!regex.test(value)) {
|
|
67
|
+
throw new BadRequestException(`Validation failed (uuid${this.options?.version ? ` v${this.options.version}` : ''} expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class ParseEnumPipe {
|
|
73
|
+
enumType;
|
|
74
|
+
constructor(enumType){
|
|
75
|
+
this.enumType = enumType;
|
|
76
|
+
}
|
|
77
|
+
transform(value, metadata) {
|
|
78
|
+
const enumValues = Object.values(this.enumType);
|
|
79
|
+
if (!enumValues.includes(value)) {
|
|
80
|
+
throw new BadRequestException(`Validation failed (${enumValues.join(', ')} expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class ParseArrayPipe {
|
|
86
|
+
options;
|
|
87
|
+
constructor(options){
|
|
88
|
+
this.options = options;
|
|
89
|
+
}
|
|
90
|
+
transform(value, metadata) {
|
|
91
|
+
if (value === undefined || value === null || value === '') {
|
|
92
|
+
if (this.options?.optional) return [];
|
|
93
|
+
throw new BadRequestException(`Validation failed (array expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) return value;
|
|
96
|
+
if (typeof value === 'string') {
|
|
97
|
+
const sep = this.options?.separator ?? ',';
|
|
98
|
+
return value.split(sep).map((v)=>v.trim()).filter((v)=>v.length > 0);
|
|
99
|
+
}
|
|
100
|
+
throw new BadRequestException(`Validation failed (array expected)${metadata.data ? ` for parameter '${metadata.data}'` : ''}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -8,7 +8,7 @@ import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, Pip
|
|
|
8
8
|
* @Module({
|
|
9
9
|
* providers: [
|
|
10
10
|
* AuthGuard,
|
|
11
|
-
* {
|
|
11
|
+
* { provide: APP_GUARD, useClass: AuthGuard },
|
|
12
12
|
* ],
|
|
13
13
|
* })
|
|
14
14
|
* class AppModule {}
|
package/dist/pipeline/tokens.js
CHANGED
package/dist/pipeline/types.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
2
|
import type { Type } from '../container/types';
|
|
3
|
+
/**
|
|
4
|
+
* HTTP-specific arguments host returned by `ExecutionContext.switchToHttp()`.
|
|
5
|
+
* In Vela/Hono, the `Context` object holds both request and response state.
|
|
6
|
+
*
|
|
7
|
+
* - `getRequest()` — the Web `Request` object
|
|
8
|
+
* - `getResponse()` — the Hono `Context` (use `c.header()`, `c.setCookie()`, etc.)
|
|
9
|
+
*/
|
|
10
|
+
export interface HttpArgumentsHost {
|
|
11
|
+
getRequest<T = Request>(): T;
|
|
12
|
+
getResponse<T = Context>(): T;
|
|
13
|
+
}
|
|
3
14
|
export interface ExecutionContext {
|
|
15
|
+
getType<T extends string = 'http'>(): T;
|
|
4
16
|
getClass(): Type;
|
|
5
17
|
getHandler(): string | symbol;
|
|
18
|
+
/** Returns the Hono `Context` directly. */
|
|
6
19
|
getContext<T = Context>(): T;
|
|
20
|
+
/** Shorthand for `switchToHttp().getRequest()` — returns the Web `Request`. */
|
|
7
21
|
getRequest(): Request;
|
|
22
|
+
/** Switch to the HTTP arguments host for NestJS-style `getRequest()` / `getResponse()` access. */
|
|
23
|
+
switchToHttp(): HttpArgumentsHost;
|
|
8
24
|
}
|
|
9
25
|
export interface CanActivate {
|
|
10
26
|
canActivate(context: ExecutionContext): boolean | Promise<boolean>;
|
|
@@ -38,12 +38,12 @@ export declare class MetadataRegistry {
|
|
|
38
38
|
static getController<T extends ComponentType>(type: T, controller: Constructor): ComponentTypeMap[T][];
|
|
39
39
|
static registerHandler<T extends ComponentType>(type: T, handlerKey: string, component: ComponentTypeMap[T]): void;
|
|
40
40
|
static getHandler<T extends ComponentType>(type: T, handlerKey: string): ComponentTypeMap[T][];
|
|
41
|
-
static markInjectable(target:
|
|
42
|
-
static hasInjectable(target:
|
|
43
|
-
static setScope(target:
|
|
44
|
-
static getScope(target:
|
|
45
|
-
static setInjectTokens(target:
|
|
46
|
-
static getInjectTokens(target:
|
|
41
|
+
static markInjectable(target: object): void;
|
|
42
|
+
static hasInjectable(target: object): boolean;
|
|
43
|
+
static setScope(target: object, scope: Scope): void;
|
|
44
|
+
static getScope(target: object): Scope | undefined;
|
|
45
|
+
static setInjectTokens(target: object, tokens: InjectMetadata[]): void;
|
|
46
|
+
static getInjectTokens(target: object): InjectMetadata[] | undefined;
|
|
47
47
|
static setHandlerHttpMeta(controller: Constructor, method: string | symbol, meta: HttpHandlerMeta): void;
|
|
48
48
|
static getHandlerHttpMeta(controller: Constructor, method: string | symbol): HttpHandlerMeta | undefined;
|
|
49
49
|
static setCatchTypes(filter: Constructor, types: Type<Error>[]): void;
|
|
@@ -57,5 +57,6 @@ export declare class MetadataRegistry {
|
|
|
57
57
|
static setCustomHandlerMeta(target: Constructor, handler: string | symbol, key: string, value: unknown): void;
|
|
58
58
|
static getCustomHandlerMeta(target: Constructor, handler: string | symbol, key: string): unknown;
|
|
59
59
|
static getCustomHandlerMetaAll(target: Constructor, handler: string | symbol): Map<string, unknown> | undefined;
|
|
60
|
+
static propagateControllerComponents(from: Constructor, to: Constructor): void;
|
|
60
61
|
static clear(): void;
|
|
61
62
|
}
|
|
@@ -248,6 +248,19 @@ export class MetadataRegistry {
|
|
|
248
248
|
static getCustomHandlerMetaAll(target, handler) {
|
|
249
249
|
return this.customHandlerMeta.get(target)?.get(handler);
|
|
250
250
|
}
|
|
251
|
+
// Propagate all controller-level components from one class to another.
|
|
252
|
+
// Used by ModuleLoader to apply module-level decorators to every controller.
|
|
253
|
+
static propagateControllerComponents(from, to) {
|
|
254
|
+
for (const [, typeMap] of this.controller.entries()){
|
|
255
|
+
const components = typeMap.get(from);
|
|
256
|
+
if (components && components.length > 0) {
|
|
257
|
+
if (!typeMap.has(to)) {
|
|
258
|
+
typeMap.set(to, []);
|
|
259
|
+
}
|
|
260
|
+
typeMap.get(to).push(...components);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
251
264
|
// Clear all (for testing)
|
|
252
265
|
static clear() {
|
|
253
266
|
this.routes.clear();
|
package/dist/registry/types.d.ts
CHANGED
|
@@ -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([
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { DynamicModule } from '../module/types';
|
|
1
|
+
import type { AsyncModuleOptions, DynamicModule } from '../module/types';
|
|
2
2
|
import type { ScheduleModuleOptions } from './schedule.types';
|
|
3
3
|
export declare class ScheduleModule {
|
|
4
4
|
static forRoot(options?: ScheduleModuleOptions): DynamicModule;
|
|
5
|
+
static forRootAsync(options: AsyncModuleOptions<ScheduleModuleOptions> & {
|
|
6
|
+
enableTimers?: boolean;
|
|
7
|
+
}): DynamicModule;
|
|
5
8
|
}
|