@martel/calyx 1.10.1 → 1.12.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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/cache/cache.interceptor.ts +4 -2
- package/src/cache/decorators.ts +4 -0
- package/src/cache/index.ts +1 -0
- package/src/core/container.ts +242 -9
- package/src/core/index.ts +2 -0
- package/src/core/lazy-module-loader.ts +29 -0
- package/src/core/metadata.ts +6 -1
- package/src/core/testing-module.ts +119 -0
- package/src/cqrs/cqrs.ts +175 -0
- package/src/graphql/decorators.ts +16 -0
- package/src/graphql/graphql.module.ts +103 -3
- package/src/http/application.ts +160 -19
- package/src/http/decorators.ts +4 -0
- package/src/index.ts +2 -0
- package/src/microservices/clients.module.ts +47 -0
- package/src/microservices/index.ts +1 -0
- package/src/microservices/microservice.ts +1 -1
- package/src/openapi/swagger.module.ts +29 -8
- package/src/schedule/decorators.ts +10 -6
- package/src/schedule/index.ts +1 -0
- package/src/schedule/schedule.module.ts +3 -2
- package/src/schedule/scheduler-registry.ts +50 -0
- package/src/security/index.ts +1 -0
- package/src/security/throttler.module.ts +108 -0
- package/src/terminus/terminus.ts +61 -0
- package/src/validation/http-pipes.ts +128 -0
- package/src/validation/index.ts +1 -0
- package/src/websockets/decorators.ts +12 -2
- package/tests/graphql.test.ts +101 -0
- package/tests/nestjs-parity.test.ts +272 -0
- package/tests/openapi.test.ts +41 -0
package/src/cqrs/cqrs.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Module, Injectable } from '../core/decorators.ts';
|
|
2
|
+
import { Type } from '../core/metadata.ts';
|
|
3
|
+
import { ModuleRef } from '../core/module-ref.ts';
|
|
4
|
+
|
|
5
|
+
export interface ICommand {}
|
|
6
|
+
export interface ICommandHandler<TCommand extends ICommand = any, TResult = any> {
|
|
7
|
+
execute(command: TCommand): Promise<TResult>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IQuery {}
|
|
11
|
+
export interface IQueryHandler<TQuery extends IQuery = any, TResult = any> {
|
|
12
|
+
execute(query: TQuery): Promise<TResult>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IEvent {}
|
|
16
|
+
export interface IEventHandler<TEvent extends IEvent = any> {
|
|
17
|
+
handle(event: TEvent): any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
21
|
+
|
|
22
|
+
export const COMMAND_HANDLER_METADATA = 'cqrs:command_handler';
|
|
23
|
+
export const QUERY_HANDLER_METADATA = 'cqrs:query_handler';
|
|
24
|
+
export const EVENT_HANDLER_METADATA = 'cqrs:event_handler';
|
|
25
|
+
|
|
26
|
+
export const CommandHandler = (command: Type<ICommand>): ClassDecorator => {
|
|
27
|
+
return (target) => {
|
|
28
|
+
Reflect.defineMetadata(COMMAND_HANDLER_METADATA, command, target);
|
|
29
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const QueryHandler = (query: Type<IQuery>): ClassDecorator => {
|
|
34
|
+
return (target) => {
|
|
35
|
+
Reflect.defineMetadata(QUERY_HANDLER_METADATA, query, target);
|
|
36
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const EventsHandler = (...events: Type<IEvent>[]): ClassDecorator => {
|
|
41
|
+
return (target) => {
|
|
42
|
+
Reflect.defineMetadata(EVENT_HANDLER_METADATA, events, target);
|
|
43
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
@Injectable()
|
|
48
|
+
export class CommandBus {
|
|
49
|
+
private handlers = new Map<any, ICommandHandler>();
|
|
50
|
+
|
|
51
|
+
register(command: any, handler: ICommandHandler) {
|
|
52
|
+
this.handlers.set(command, handler);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async execute<T extends ICommand, R = any>(command: T): Promise<R> {
|
|
56
|
+
const commandClass = command.constructor;
|
|
57
|
+
const handler = this.handlers.get(commandClass);
|
|
58
|
+
if (!handler) {
|
|
59
|
+
throw new Error(`CommandHandler not found for command: ${commandClass.name}`);
|
|
60
|
+
}
|
|
61
|
+
return (await handler.execute(command)) as R;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Injectable()
|
|
66
|
+
export class QueryBus {
|
|
67
|
+
private handlers = new Map<any, IQueryHandler>();
|
|
68
|
+
|
|
69
|
+
register(query: any, handler: IQueryHandler) {
|
|
70
|
+
this.handlers.set(query, handler);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async execute<T extends IQuery, R = any>(query: T): Promise<R> {
|
|
74
|
+
const queryClass = query.constructor;
|
|
75
|
+
const handler = this.handlers.get(queryClass);
|
|
76
|
+
if (!handler) {
|
|
77
|
+
throw new Error(`QueryHandler not found for query: ${queryClass.name}`);
|
|
78
|
+
}
|
|
79
|
+
return (await handler.execute(query)) as R;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Injectable()
|
|
84
|
+
export class EventBus {
|
|
85
|
+
private handlers = new Map<any, IEventHandler[]>();
|
|
86
|
+
|
|
87
|
+
register(event: any, handler: IEventHandler) {
|
|
88
|
+
const existing = this.handlers.get(event) || [];
|
|
89
|
+
existing.push(handler);
|
|
90
|
+
this.handlers.set(event, existing);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
publish<T extends IEvent>(event: T) {
|
|
94
|
+
const eventClass = event.constructor;
|
|
95
|
+
const eventHandlers = this.handlers.get(eventClass) || [];
|
|
96
|
+
for (const handler of eventHandlers) {
|
|
97
|
+
try {
|
|
98
|
+
handler.handle(event);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`Error in event handler:`, err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Injectable()
|
|
107
|
+
export class EventPublisher {
|
|
108
|
+
constructor(private readonly eventBus: EventBus) {}
|
|
109
|
+
|
|
110
|
+
mergeClassContext<T extends Type<any>>(metatype: T): T {
|
|
111
|
+
const eventBus = this.eventBus;
|
|
112
|
+
return class extends metatype {
|
|
113
|
+
publish(event: IEvent) {
|
|
114
|
+
eventBus.publish(event);
|
|
115
|
+
}
|
|
116
|
+
publishAll(events: IEvent[]) {
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
eventBus.publish(event);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
mergeObjectContext<T extends Object>(object: T): T {
|
|
125
|
+
const eventBus = this.eventBus;
|
|
126
|
+
return Object.assign(object, {
|
|
127
|
+
publish: (event: IEvent) => eventBus.publish(event),
|
|
128
|
+
publishAll: (events: IEvent[]) => {
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
eventBus.publish(event);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Module({
|
|
138
|
+
providers: [CommandBus, QueryBus, EventBus, EventPublisher],
|
|
139
|
+
exports: [CommandBus, QueryBus, EventBus, EventPublisher],
|
|
140
|
+
})
|
|
141
|
+
export class CqrsModule {
|
|
142
|
+
constructor(
|
|
143
|
+
private readonly moduleRef: ModuleRef,
|
|
144
|
+
private readonly commandBus: CommandBus,
|
|
145
|
+
private readonly queryBus: QueryBus,
|
|
146
|
+
private readonly eventBus: EventBus
|
|
147
|
+
) {}
|
|
148
|
+
|
|
149
|
+
onModuleInit() {
|
|
150
|
+
const container = (this.moduleRef as any).container;
|
|
151
|
+
if (!container) return;
|
|
152
|
+
|
|
153
|
+
const instances = container.getProviderAndControllerInstances();
|
|
154
|
+
for (const inst of instances) {
|
|
155
|
+
if (!inst || !inst.constructor) continue;
|
|
156
|
+
|
|
157
|
+
const command = Reflect.getMetadata(COMMAND_HANDLER_METADATA, inst.constructor);
|
|
158
|
+
if (command) {
|
|
159
|
+
this.commandBus.register(command, inst);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const query = Reflect.getMetadata(QUERY_HANDLER_METADATA, inst.constructor);
|
|
163
|
+
if (query) {
|
|
164
|
+
this.queryBus.register(query, inst);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const events = Reflect.getMetadata(EVENT_HANDLER_METADATA, inst.constructor);
|
|
168
|
+
if (events && Array.isArray(events)) {
|
|
169
|
+
for (const event of events) {
|
|
170
|
+
this.eventBus.register(event, inst);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -47,6 +47,22 @@ export function Parent(): ParameterDecorator {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function Context(name?: string): ParameterDecorator {
|
|
51
|
+
return (target, propertyKey, parameterIndex) => {
|
|
52
|
+
const contextParams = Reflect.getOwnMetadata('calyx:context', target, propertyKey!) || [];
|
|
53
|
+
contextParams.push({ parameterIndex, name });
|
|
54
|
+
Reflect.defineMetadata('calyx:context', contextParams, target, propertyKey!);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function Info(): ParameterDecorator {
|
|
59
|
+
return (target, propertyKey, parameterIndex) => {
|
|
60
|
+
const infoParams = Reflect.getOwnMetadata('calyx:info', target, propertyKey!) || [];
|
|
61
|
+
infoParams.push(parameterIndex);
|
|
62
|
+
Reflect.defineMetadata('calyx:info', infoParams, target, propertyKey!);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
export function ObjectType(): ClassDecorator {
|
|
51
67
|
return (target) => {
|
|
52
68
|
Reflect.defineMetadata('calyx:object_type', true, target);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CalyxContainer } from '../core/container.ts';
|
|
1
|
+
import { CalyxContainer, DynamicModule } from '../core/container.ts';
|
|
2
2
|
import { Module } from '../core/decorators.ts';
|
|
3
3
|
import {
|
|
4
4
|
GraphQLSchema,
|
|
@@ -13,17 +13,42 @@ import {
|
|
|
13
13
|
GraphQLBoolean,
|
|
14
14
|
GraphQLList,
|
|
15
15
|
GraphQLNonNull,
|
|
16
|
+
buildSchema as buildGqlSchema,
|
|
16
17
|
} from 'graphql';
|
|
17
18
|
|
|
19
|
+
export interface GraphQLOptions {
|
|
20
|
+
typeDefs?: string;
|
|
21
|
+
context?: (ctx: { req: any }) => any | Promise<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
@Module({})
|
|
19
25
|
export class GraphQLModule {
|
|
26
|
+
static forRoot(options: GraphQLOptions): DynamicModule {
|
|
27
|
+
return {
|
|
28
|
+
module: GraphQLModule,
|
|
29
|
+
providers: [
|
|
30
|
+
{
|
|
31
|
+
provide: 'calyx:graphql_options',
|
|
32
|
+
useValue: options,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
static buildSchema(container: CalyxContainer): GraphQLSchema | null {
|
|
21
39
|
const instances = container.getProviderAndControllerInstances();
|
|
22
40
|
const resolverInstances = instances.filter(
|
|
23
41
|
(inst) => inst && inst.constructor && Reflect.hasMetadata('calyx:resolver', inst.constructor)
|
|
24
42
|
);
|
|
25
43
|
|
|
26
|
-
|
|
44
|
+
let options: GraphQLOptions | undefined;
|
|
45
|
+
try {
|
|
46
|
+
options = container.getGlobalOrAnyInstance('calyx:graphql_options');
|
|
47
|
+
} catch {
|
|
48
|
+
// Options provider not bound, fallback to default code-first
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (resolverInstances.length === 0 && !options?.typeDefs) {
|
|
27
52
|
return null;
|
|
28
53
|
}
|
|
29
54
|
|
|
@@ -98,7 +123,12 @@ export class GraphQLModule {
|
|
|
98
123
|
// Custom Scalar
|
|
99
124
|
if (typeof typeClass === 'function' && Reflect.hasMetadata('calyx:scalar', typeClass)) {
|
|
100
125
|
const scalarMeta = Reflect.getMetadata('calyx:scalar', typeClass);
|
|
101
|
-
|
|
126
|
+
let inst: any;
|
|
127
|
+
try {
|
|
128
|
+
inst = container.getGlobalOrAnyInstance(typeClass);
|
|
129
|
+
} catch {
|
|
130
|
+
inst = new typeClass();
|
|
131
|
+
}
|
|
102
132
|
const gqlScalar = new GraphQLScalarType({
|
|
103
133
|
name: scalarMeta.name,
|
|
104
134
|
description: Reflect.getMetadata('calyx:description', typeClass),
|
|
@@ -283,6 +313,14 @@ export class GraphQLModule {
|
|
|
283
313
|
for (const idx of parentParams) {
|
|
284
314
|
params[idx] = parent;
|
|
285
315
|
}
|
|
316
|
+
const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldMeta.propertyKey) || [];
|
|
317
|
+
for (const item of contextParams) {
|
|
318
|
+
params[item.parameterIndex] = item.name ? context?.[item.name] : context;
|
|
319
|
+
}
|
|
320
|
+
const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldMeta.propertyKey) || [];
|
|
321
|
+
for (const idx of infoParams) {
|
|
322
|
+
params[idx] = info;
|
|
323
|
+
}
|
|
286
324
|
return resolverInstance[fieldMeta.propertyKey](...params);
|
|
287
325
|
};
|
|
288
326
|
|
|
@@ -443,6 +481,14 @@ export class GraphQLModule {
|
|
|
443
481
|
for (const idx of parentParams) {
|
|
444
482
|
params[idx] = parent;
|
|
445
483
|
}
|
|
484
|
+
const contextParams = Reflect.getMetadata('calyx:context', resolverInstance, fieldRes.propertyKey) || [];
|
|
485
|
+
for (const item of contextParams) {
|
|
486
|
+
params[item.parameterIndex] = item.name ? context?.[item.name] : context;
|
|
487
|
+
}
|
|
488
|
+
const infoParams = Reflect.getMetadata('calyx:info', resolverInstance, fieldRes.propertyKey) || [];
|
|
489
|
+
for (const idx of infoParams) {
|
|
490
|
+
params[idx] = info;
|
|
491
|
+
}
|
|
446
492
|
return resolverInstance[fieldRes.propertyKey](...params);
|
|
447
493
|
};
|
|
448
494
|
|
|
@@ -480,6 +526,60 @@ export class GraphQLModule {
|
|
|
480
526
|
}
|
|
481
527
|
}
|
|
482
528
|
|
|
529
|
+
// Support Schema-First Approach if typeDefs is specified
|
|
530
|
+
if (options?.typeDefs) {
|
|
531
|
+
const schema = buildGqlSchema(options.typeDefs);
|
|
532
|
+
|
|
533
|
+
// Attach code-first resolver actions onto schema-first AST definitions
|
|
534
|
+
const queryType = schema.getQueryType();
|
|
535
|
+
if (queryType) {
|
|
536
|
+
const fields = queryType.getFields();
|
|
537
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
538
|
+
if (queryFields[fieldName]) {
|
|
539
|
+
field.resolve = queryFields[fieldName].resolve;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const mutationType = schema.getMutationType();
|
|
544
|
+
if (mutationType) {
|
|
545
|
+
const fields = mutationType.getFields();
|
|
546
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
547
|
+
if (mutationFields[fieldName]) {
|
|
548
|
+
field.resolve = mutationFields[fieldName].resolve;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const subscriptionType = schema.getSubscriptionType();
|
|
553
|
+
if (subscriptionType) {
|
|
554
|
+
const fields = subscriptionType.getFields();
|
|
555
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
556
|
+
if (subscriptionFields[fieldName]) {
|
|
557
|
+
field.subscribe = subscriptionFields[fieldName].subscribe;
|
|
558
|
+
field.resolve = subscriptionFields[fieldName].resolve;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Map Custom ObjectType ResolveFields from code-first typeMap
|
|
564
|
+
for (const [typeClass, codeFirstType] of typeMap.entries()) {
|
|
565
|
+
if (codeFirstType instanceof GraphQLObjectType) {
|
|
566
|
+
const typeName = codeFirstType.name;
|
|
567
|
+
const schemaFirstType = schema.getType(typeName);
|
|
568
|
+
if (schemaFirstType instanceof GraphQLObjectType) {
|
|
569
|
+
const codeFirstFields = codeFirstType.getFields();
|
|
570
|
+
const schemaFirstFields = schemaFirstType.getFields();
|
|
571
|
+
for (const [fieldName, cfField] of Object.entries(codeFirstFields)) {
|
|
572
|
+
if (cfField.resolve && schemaFirstFields[fieldName]) {
|
|
573
|
+
schemaFirstFields[fieldName].resolve = cfField.resolve;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return schema;
|
|
581
|
+
}
|
|
582
|
+
|
|
483
583
|
if (Object.keys(queryFields).length === 0) {
|
|
484
584
|
return null;
|
|
485
585
|
}
|
package/src/http/application.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { EventEmitter } from '../event-emitter/event-emitter.ts';
|
|
|
10
10
|
import { cors, CorsOptions } from '../security/cors.middleware.ts';
|
|
11
11
|
import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
|
|
12
12
|
import { CronMatcher } from '../schedule/cron.matcher.ts';
|
|
13
|
+
import { SchedulerRegistry } from '../schedule/scheduler-registry.ts';
|
|
13
14
|
import { SerializationCompiler } from '../validation/compiler.ts';
|
|
14
15
|
import { VersioningOptions, VersioningType, VersionExtractor, VERSION_METADATA_KEY } from '../versioning/versioning.ts';
|
|
15
16
|
import { QueueManager, PROCESSOR_METADATA_KEY } from '../queue/queue.module.ts';
|
|
@@ -66,6 +67,41 @@ export class CalyxResponse {
|
|
|
66
67
|
this.cookiesList.push(formatCookie(name, value, options));
|
|
67
68
|
return this;
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
header(name: string, value: string) {
|
|
72
|
+
return this.set(name, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type(contentType: string) {
|
|
76
|
+
return this.set('content-type', contentType);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
redirect(url: string, status?: number) {
|
|
80
|
+
this.statusCode = status ?? 302;
|
|
81
|
+
this.set('location', url);
|
|
82
|
+
this.sent = true;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
end() {
|
|
87
|
+
this.sent = true;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get(name: string): string | undefined {
|
|
92
|
+
return this.headers[name.toLowerCase()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
append(name: string, value: string) {
|
|
96
|
+
const key = name.toLowerCase();
|
|
97
|
+
const existing = this.headers[key];
|
|
98
|
+
this.headers[key] = existing ? `${existing}, ${value}` : value;
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
clearCookie(name: string, options?: any) {
|
|
103
|
+
return this.cookie(name, '', { ...options, expires: new Date(0) });
|
|
104
|
+
}
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
interface CompiledLifecycleItem {
|
|
@@ -186,7 +222,7 @@ export class CalyxApplication {
|
|
|
186
222
|
this.isInitialized = true;
|
|
187
223
|
|
|
188
224
|
// Bootstrap the dependency injection container
|
|
189
|
-
this.container.bootstrap(this.rootModule);
|
|
225
|
+
await this.container.bootstrap(this.rootModule);
|
|
190
226
|
|
|
191
227
|
// Resolve registered modules middlewares
|
|
192
228
|
this.resolveMiddleware();
|
|
@@ -685,6 +721,10 @@ export class CalyxApplication {
|
|
|
685
721
|
case 'query': val = config.name ? query?.[config.name] : query; break;
|
|
686
722
|
case 'body': val = config.name ? body?.[config.name] : body; break;
|
|
687
723
|
case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
|
|
724
|
+
case 'session': val = (req as any).session; break;
|
|
725
|
+
case 'ip': val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? ''); break;
|
|
726
|
+
case 'hostparam': val = config.name ? req.headers.get('host') : req.headers.get('host'); break;
|
|
727
|
+
case 'next': val = next; break;
|
|
688
728
|
case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
|
|
689
729
|
}
|
|
690
730
|
|
|
@@ -939,6 +979,18 @@ export class CalyxApplication {
|
|
|
939
979
|
case 'headers':
|
|
940
980
|
val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries());
|
|
941
981
|
break;
|
|
982
|
+
case 'session':
|
|
983
|
+
val = (req as any).session;
|
|
984
|
+
break;
|
|
985
|
+
case 'ip':
|
|
986
|
+
val = this.server ? (this.server.requestIP(req)?.address ?? req.headers.get('x-forwarded-for') ?? '') : (req.headers.get('x-forwarded-for') ?? '');
|
|
987
|
+
break;
|
|
988
|
+
case 'hostparam':
|
|
989
|
+
val = config.name ? req.headers.get('host') : req.headers.get('host');
|
|
990
|
+
break;
|
|
991
|
+
case 'next':
|
|
992
|
+
val = () => {};
|
|
993
|
+
break;
|
|
942
994
|
case 'custom':
|
|
943
995
|
val = config.factory ? config.factory(config.name, context!) : undefined;
|
|
944
996
|
break;
|
|
@@ -1231,10 +1283,25 @@ export class CalyxApplication {
|
|
|
1231
1283
|
this.graphqlQueryCache.set(query, document);
|
|
1232
1284
|
}
|
|
1233
1285
|
|
|
1286
|
+
let options: any;
|
|
1287
|
+
try {
|
|
1288
|
+
options = this.container.get('calyx:graphql_options');
|
|
1289
|
+
} catch {}
|
|
1290
|
+
|
|
1291
|
+
let contextValue: any = { req };
|
|
1292
|
+
if (options?.context) {
|
|
1293
|
+
contextValue = await options.context({ req });
|
|
1294
|
+
if (contextValue && typeof contextValue === 'object') {
|
|
1295
|
+
contextValue.req = req;
|
|
1296
|
+
} else {
|
|
1297
|
+
contextValue = { req, ...contextValue };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1234
1301
|
const result = await execute({
|
|
1235
1302
|
schema: this.graphqlSchema,
|
|
1236
1303
|
document,
|
|
1237
|
-
contextValue
|
|
1304
|
+
contextValue,
|
|
1238
1305
|
variableValues: variables,
|
|
1239
1306
|
});
|
|
1240
1307
|
|
|
@@ -1349,11 +1416,26 @@ export class CalyxApplication {
|
|
|
1349
1416
|
this.graphqlQueryCache.set(query, document);
|
|
1350
1417
|
}
|
|
1351
1418
|
|
|
1419
|
+
let options: any;
|
|
1420
|
+
try {
|
|
1421
|
+
options = this.container.get('calyx:graphql_options');
|
|
1422
|
+
} catch {}
|
|
1423
|
+
|
|
1424
|
+
let contextValue: any = { req: ws.data?.req };
|
|
1425
|
+
if (options?.context) {
|
|
1426
|
+
contextValue = await options.context({ req: ws.data?.req });
|
|
1427
|
+
if (contextValue && typeof contextValue === 'object') {
|
|
1428
|
+
contextValue.req = ws.data?.req;
|
|
1429
|
+
} else {
|
|
1430
|
+
contextValue = { req: ws.data?.req, ...contextValue };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1352
1434
|
const subResult = await subscribe({
|
|
1353
1435
|
schema: this.graphqlSchema,
|
|
1354
1436
|
document,
|
|
1355
1437
|
variableValues: variables,
|
|
1356
|
-
contextValue
|
|
1438
|
+
contextValue,
|
|
1357
1439
|
});
|
|
1358
1440
|
|
|
1359
1441
|
if (subResult && Symbol.asyncIterator in subResult) {
|
|
@@ -1524,11 +1606,18 @@ export class CalyxApplication {
|
|
|
1524
1606
|
return;
|
|
1525
1607
|
}
|
|
1526
1608
|
|
|
1609
|
+
let registry: SchedulerRegistry | null = null;
|
|
1610
|
+
try {
|
|
1611
|
+
registry = this.container.getGlobalOrAnyInstance(SchedulerRegistry);
|
|
1612
|
+
} catch {
|
|
1613
|
+
// ignore
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1527
1616
|
const instances = this.container.getProviderAndControllerInstances();
|
|
1528
1617
|
for (const instance of instances) {
|
|
1529
1618
|
if (!instance || !instance.constructor) continue;
|
|
1530
1619
|
|
|
1531
|
-
const crons: { expression: string; propertyKey: string | symbol }[] =
|
|
1620
|
+
const crons: { expression: string; propertyKey: string | symbol; name?: string }[] =
|
|
1532
1621
|
Reflect.getMetadata('calyx:cron', instance.constructor) || [];
|
|
1533
1622
|
for (const cron of crons) {
|
|
1534
1623
|
const parts = cron.expression.split(' ');
|
|
@@ -1554,9 +1643,16 @@ export class CalyxApplication {
|
|
|
1554
1643
|
const intervalMs = isSecondLevel ? 1000 : 20000;
|
|
1555
1644
|
const timer = setInterval(tick, intervalMs);
|
|
1556
1645
|
this.cleanupListeners.push(() => clearInterval(timer));
|
|
1646
|
+
|
|
1647
|
+
if (registry && cron.name) {
|
|
1648
|
+
registry.addCronJob(cron.name, {
|
|
1649
|
+
start: () => {},
|
|
1650
|
+
stop: () => clearInterval(timer),
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1557
1653
|
}
|
|
1558
1654
|
|
|
1559
|
-
const intervals: { ms: number; propertyKey: string | symbol }[] =
|
|
1655
|
+
const intervals: { ms: number; propertyKey: string | symbol; name?: string }[] =
|
|
1560
1656
|
Reflect.getMetadata('calyx:interval', instance.constructor) || [];
|
|
1561
1657
|
for (const interval of intervals) {
|
|
1562
1658
|
const timer = setInterval(() => {
|
|
@@ -1567,9 +1663,13 @@ export class CalyxApplication {
|
|
|
1567
1663
|
}
|
|
1568
1664
|
}, interval.ms);
|
|
1569
1665
|
this.cleanupListeners.push(() => clearInterval(timer));
|
|
1666
|
+
|
|
1667
|
+
if (registry && interval.name) {
|
|
1668
|
+
registry.addInterval(interval.name, timer);
|
|
1669
|
+
}
|
|
1570
1670
|
}
|
|
1571
1671
|
|
|
1572
|
-
const timeouts: { ms: number; propertyKey: string | symbol }[] =
|
|
1672
|
+
const timeouts: { ms: number; propertyKey: string | symbol; name?: string }[] =
|
|
1573
1673
|
Reflect.getMetadata('calyx:timeout', instance.constructor) || [];
|
|
1574
1674
|
for (const timeout of timeouts) {
|
|
1575
1675
|
const timer = setTimeout(() => {
|
|
@@ -1580,6 +1680,10 @@ export class CalyxApplication {
|
|
|
1580
1680
|
}
|
|
1581
1681
|
}, timeout.ms);
|
|
1582
1682
|
this.cleanupListeners.push(() => clearTimeout(timer));
|
|
1683
|
+
|
|
1684
|
+
if (registry && timeout.name) {
|
|
1685
|
+
registry.addTimeout(timeout.name, timer);
|
|
1686
|
+
}
|
|
1583
1687
|
}
|
|
1584
1688
|
}
|
|
1585
1689
|
}
|
|
@@ -1605,14 +1709,24 @@ export class CalyxApplication {
|
|
|
1605
1709
|
for (const sub of subMessages) {
|
|
1606
1710
|
const paramMapping: any[] = [];
|
|
1607
1711
|
|
|
1712
|
+
const classPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor) || [];
|
|
1713
|
+
const methodPipes = Reflect.getMetadata(METADATA_KEYS.PIPES, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1714
|
+
const methodPipesCompiled = this.compileLifecycleItems(this.rootModule, [...this.globalPipes, ...classPipes, ...methodPipes]);
|
|
1715
|
+
|
|
1608
1716
|
for (const bp of bodyParams) {
|
|
1609
1717
|
if (bp.propertyKey === sub.propertyKey) {
|
|
1610
|
-
paramMapping[bp.parameterIndex] =
|
|
1718
|
+
paramMapping[bp.parameterIndex] = {
|
|
1719
|
+
type: 'body',
|
|
1720
|
+
name: bp.name,
|
|
1721
|
+
pipes: [...methodPipesCompiled, ...this.compileLifecycleItems(this.rootModule, bp.pipes || [])],
|
|
1722
|
+
};
|
|
1611
1723
|
}
|
|
1612
1724
|
}
|
|
1613
1725
|
for (const sp of socketParams) {
|
|
1614
1726
|
if (sp.propertyKey === sub.propertyKey) {
|
|
1615
|
-
paramMapping[sp.parameterIndex] =
|
|
1727
|
+
paramMapping[sp.parameterIndex] = {
|
|
1728
|
+
type: 'socket',
|
|
1729
|
+
};
|
|
1616
1730
|
}
|
|
1617
1731
|
}
|
|
1618
1732
|
|
|
@@ -1624,11 +1738,16 @@ export class CalyxApplication {
|
|
|
1624
1738
|
const methodInterceptors = Reflect.getMetadata(METADATA_KEYS.INTERCEPTORS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1625
1739
|
const interceptors = this.compileLifecycleItems(this.rootModule, [...this.globalInterceptors, ...classInterceptors, ...methodInterceptors]);
|
|
1626
1740
|
|
|
1741
|
+
const classFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor) || [];
|
|
1742
|
+
const methodFilters = Reflect.getMetadata(METADATA_KEYS.FILTERS, instance.constructor.prototype, sub.propertyKey) || [];
|
|
1743
|
+
const filters = this.compileLifecycleItems(this.rootModule, [...this.globalFilters, ...classFilters, ...methodFilters]);
|
|
1744
|
+
|
|
1627
1745
|
handlers.set(sub.event, {
|
|
1628
1746
|
propertyKey: sub.propertyKey,
|
|
1629
1747
|
paramMapping,
|
|
1630
1748
|
guards,
|
|
1631
1749
|
interceptors,
|
|
1750
|
+
filters,
|
|
1632
1751
|
gatewayClass: instance.constructor,
|
|
1633
1752
|
});
|
|
1634
1753
|
}
|
|
@@ -1708,16 +1827,6 @@ export class CalyxApplication {
|
|
|
1708
1827
|
|
|
1709
1828
|
const handlerInfo = gateway.handlers.get(event);
|
|
1710
1829
|
if (handlerInfo) {
|
|
1711
|
-
const args: any[] = [];
|
|
1712
|
-
for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
|
|
1713
|
-
const type = handlerInfo.paramMapping[i];
|
|
1714
|
-
if (type === 'body') {
|
|
1715
|
-
args[i] = data;
|
|
1716
|
-
} else if (type === 'socket') {
|
|
1717
|
-
args[i] = ws;
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
1830
|
const context = this.contextPool.acquire();
|
|
1722
1831
|
context.resetContextWs(ws, data, handlerInfo.gatewayClass, gateway.instance[handlerInfo.propertyKey]);
|
|
1723
1832
|
|
|
@@ -1729,6 +1838,26 @@ export class CalyxApplication {
|
|
|
1729
1838
|
}
|
|
1730
1839
|
}
|
|
1731
1840
|
|
|
1841
|
+
const args: any[] = [];
|
|
1842
|
+
for (let i = 0; i < handlerInfo.paramMapping.length; i++) {
|
|
1843
|
+
const mapping = handlerInfo.paramMapping[i];
|
|
1844
|
+
if (!mapping) continue;
|
|
1845
|
+
if (mapping.type === 'body') {
|
|
1846
|
+
let val = mapping.name ? data?.[mapping.name] : data;
|
|
1847
|
+
for (const pipe of mapping.pipes || []) {
|
|
1848
|
+
const transformed = pipe.instance.transform(val, {
|
|
1849
|
+
type: 'body',
|
|
1850
|
+
metatype: undefined,
|
|
1851
|
+
data: mapping.name,
|
|
1852
|
+
});
|
|
1853
|
+
val = transformed instanceof Promise ? await transformed : transformed;
|
|
1854
|
+
}
|
|
1855
|
+
args[i] = val;
|
|
1856
|
+
} else if (mapping.type === 'socket') {
|
|
1857
|
+
args[i] = ws;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1732
1861
|
const nextCall = {
|
|
1733
1862
|
handle: async () => {
|
|
1734
1863
|
return gateway.instance[handlerInfo.propertyKey](...args);
|
|
@@ -1752,7 +1881,19 @@ export class CalyxApplication {
|
|
|
1752
1881
|
ws.send(JSON.stringify(result));
|
|
1753
1882
|
}
|
|
1754
1883
|
} catch (err: any) {
|
|
1755
|
-
|
|
1884
|
+
if (handlerInfo.filters && handlerInfo.filters.length > 0) {
|
|
1885
|
+
for (const filter of handlerInfo.filters) {
|
|
1886
|
+
const catchException = Reflect.getMetadata('calyx:catch', filter.token) || [];
|
|
1887
|
+
if (catchException.length === 0 || catchException.some((exc: any) => err instanceof exc)) {
|
|
1888
|
+
const filterResult = filter.instance.catch(err, context);
|
|
1889
|
+
if (filterResult !== undefined) {
|
|
1890
|
+
ws.send(JSON.stringify(filterResult));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
ws.send(JSON.stringify({ event: 'error', data: err.message || 'Internal error' }));
|
|
1756
1897
|
} finally {
|
|
1757
1898
|
context.clearContext();
|
|
1758
1899
|
this.contextPool.release(context);
|
package/src/http/decorators.ts
CHANGED
|
@@ -82,6 +82,10 @@ export const Headers = (first?: any, ...pipes: any[]) => {
|
|
|
82
82
|
return createHttpParamDecorator('headers', name, parsedPipes);
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
export const Ip = () => createHttpParamDecorator('ip');
|
|
86
|
+
export const HostParam = (name?: string) => createHttpParamDecorator('hostparam', name);
|
|
87
|
+
export const Next = () => createHttpParamDecorator('next');
|
|
88
|
+
|
|
85
89
|
export function HttpCode(code: number): MethodDecorator {
|
|
86
90
|
return (target, propertyKey) => {
|
|
87
91
|
Reflect.defineMetadata(METADATA_KEYS.HTTP_CODE, code, target, propertyKey);
|