@martel/calyx 1.11.0 → 1.13.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 +15 -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/cli/index.ts +7 -1
- package/src/config/config.module.ts +16 -2
- package/src/config/config.service.ts +20 -6
- package/src/core/container.ts +559 -140
- 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 +123 -0
- package/src/cqrs/cqrs.ts +264 -0
- package/src/database/sequelize.module.ts +239 -0
- package/src/event-emitter/decorators.ts +2 -2
- package/src/event-emitter/event-emitter.ts +3 -0
- package/src/graphql/decorators.ts +16 -0
- package/src/graphql/graphql.module.ts +16 -0
- package/src/http/application.ts +261 -21
- package/src/http/decorators.ts +25 -1
- package/src/http/exceptions.ts +97 -0
- package/src/http/factory.ts +3 -0
- package/src/http/router.ts +27 -4
- package/src/index.ts +3 -0
- package/src/microservices/clients.module.ts +47 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +2 -0
- package/src/microservices/microservice.ts +1 -1
- package/src/queue/queue.module.ts +73 -5
- 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 +134 -0
- package/src/validation/compiler.ts +133 -10
- package/src/validation/decorators.ts +164 -2
- package/src/validation/http-pipes.ts +128 -0
- package/src/validation/index.ts +1 -0
- package/src/websockets/decorators.ts +12 -2
- package/src/websockets/exceptions.ts +10 -0
- package/src/websockets/index.ts +1 -0
- package/tests/circular-di.test.ts +151 -0
- package/tests/di.test.ts +10 -2
- package/tests/nestjs-parity.test.ts +527 -0
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Injectable } from './decorators.ts';
|
|
2
|
+
import { CalyxContainer } from './container.ts';
|
|
3
|
+
import { ModuleRef } from './module-ref.ts';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class LazyModuleLoader {
|
|
7
|
+
constructor(private readonly moduleRef: ModuleRef) {}
|
|
8
|
+
|
|
9
|
+
async load(loader: () => Promise<any> | any): Promise<ModuleRef> {
|
|
10
|
+
let moduleClass = await loader();
|
|
11
|
+
if (moduleClass && typeof moduleClass === 'object' && 'default' in moduleClass) {
|
|
12
|
+
moduleClass = moduleClass.default;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const container: CalyxContainer = (this.moduleRef as any).container;
|
|
16
|
+
if (!container) {
|
|
17
|
+
throw new Error('LazyModuleLoader: Container reference not found on moduleRef');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await container.bootstrap(moduleClass);
|
|
21
|
+
|
|
22
|
+
const record = container.getModuleRecord(moduleClass);
|
|
23
|
+
if (!record) {
|
|
24
|
+
throw new Error(`LazyModuleLoader: Module record not found for ${moduleClass.name}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return record.instances.get(ModuleRef);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/core/metadata.ts
CHANGED
|
@@ -44,7 +44,12 @@ export interface FactoryProvider {
|
|
|
44
44
|
scope?: Scope;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export
|
|
47
|
+
export interface ExistingProvider {
|
|
48
|
+
provide: InjectionToken;
|
|
49
|
+
useExisting: InjectionToken;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Provider = Type<any> | ValueProvider | ClassProvider | FactoryProvider | ExistingProvider;
|
|
48
53
|
|
|
49
54
|
export interface ModuleMetadata {
|
|
50
55
|
imports?: any[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CalyxContainer } from './container.ts';
|
|
2
|
+
import { ModuleMetadata, Type, InjectionToken } from './metadata.ts';
|
|
3
|
+
import { Module } from './decorators.ts';
|
|
4
|
+
import { ModuleRef } from './module-ref.ts';
|
|
5
|
+
import { CalyxApplication } from '../http/application.ts';
|
|
6
|
+
|
|
7
|
+
export class TestingModule extends ModuleRef {
|
|
8
|
+
constructor(public readonly container: CalyxContainer, private readonly rootModuleClass: any) {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
createCalyxApplication(): CalyxApplication {
|
|
13
|
+
const app = new CalyxApplication(this.rootModuleClass);
|
|
14
|
+
(app as any).container = this.container;
|
|
15
|
+
return app;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createNestApplication(options?: any): CalyxApplication {
|
|
19
|
+
return this.createCalyxApplication();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get<T>(token: InjectionToken, options?: { strict: boolean }): T {
|
|
23
|
+
const strict = options?.strict ?? false;
|
|
24
|
+
if (strict) {
|
|
25
|
+
return this.container.resolveTokenInModuleContext(this.rootModuleClass, token);
|
|
26
|
+
} else {
|
|
27
|
+
return this.container.getGlobalOrAnyInstance(token);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async resolve<T>(token: InjectionToken, contextId?: any, options?: { strict: boolean }): Promise<T> {
|
|
32
|
+
const requestContext = contextId instanceof Map ? contextId : new Map<any, any>();
|
|
33
|
+
const strict = options?.strict ?? false;
|
|
34
|
+
if (strict) {
|
|
35
|
+
return await this.container.resolveTokenInModuleContextAsync(this.rootModuleClass, token, requestContext);
|
|
36
|
+
} else {
|
|
37
|
+
return await this.container.resolveTokenGloballyAsync(token, requestContext);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async create<T>(type: Type<T>): Promise<T> {
|
|
42
|
+
return await this.container.instantiateClassAsync(type, this.rootModuleClass);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async close() {
|
|
46
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
47
|
+
for (const inst of instances) {
|
|
48
|
+
if (inst && typeof inst.onModuleDestroy === 'function') {
|
|
49
|
+
await inst.onModuleDestroy();
|
|
50
|
+
}
|
|
51
|
+
if (inst && typeof inst.onApplicationShutdown === 'function') {
|
|
52
|
+
await inst.onApplicationShutdown();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class TestingModuleBuilder {
|
|
59
|
+
private overrides = new Map<InjectionToken, { useValue?: any; useClass?: any; useFactory?: any; inject?: any[] }>();
|
|
60
|
+
|
|
61
|
+
constructor(private readonly metadata: ModuleMetadata) {}
|
|
62
|
+
|
|
63
|
+
overrideProvider(token: InjectionToken) {
|
|
64
|
+
const builder = {
|
|
65
|
+
useValue: (value: any) => {
|
|
66
|
+
this.overrides.set(token, { useValue: value });
|
|
67
|
+
return this;
|
|
68
|
+
},
|
|
69
|
+
useClass: (clazz: any) => {
|
|
70
|
+
this.overrides.set(token, { useClass: clazz });
|
|
71
|
+
return this;
|
|
72
|
+
},
|
|
73
|
+
useFactory: (options: { factory: (...args: any[]) => any; inject?: any[] }) => {
|
|
74
|
+
this.overrides.set(token, { useFactory: options.factory, inject: options.inject });
|
|
75
|
+
return this;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
return builder;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
overrideGuard(token: any) { return this.overrideProvider(token); }
|
|
82
|
+
overrideInterceptor(token: any) { return this.overrideProvider(token); }
|
|
83
|
+
overridePipe(token: any) { return this.overrideProvider(token); }
|
|
84
|
+
overrideFilter(token: any) { return this.overrideProvider(token); }
|
|
85
|
+
|
|
86
|
+
async compile(): Promise<TestingModule> {
|
|
87
|
+
@Module({
|
|
88
|
+
imports: this.metadata.imports || [],
|
|
89
|
+
controllers: this.metadata.controllers || [],
|
|
90
|
+
providers: this.metadata.providers || [],
|
|
91
|
+
exports: this.metadata.exports || [],
|
|
92
|
+
})
|
|
93
|
+
class RootTestModule {}
|
|
94
|
+
|
|
95
|
+
const container = new CalyxContainer();
|
|
96
|
+
container.addModule(RootTestModule);
|
|
97
|
+
|
|
98
|
+
// Apply overrides to module records
|
|
99
|
+
for (const [token, override] of this.overrides.entries()) {
|
|
100
|
+
for (const record of (container as any).modules.values()) {
|
|
101
|
+
if (record.providers.has(token)) {
|
|
102
|
+
if ('useValue' in override) {
|
|
103
|
+
record.providers.set(token, { provide: token, useValue: override.useValue });
|
|
104
|
+
} else if ('useClass' in override) {
|
|
105
|
+
record.providers.set(token, { provide: token, useClass: override.useClass });
|
|
106
|
+
} else if ('useFactory' in override) {
|
|
107
|
+
record.providers.set(token, { provide: token, useFactory: override.useFactory, inject: override.inject });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await container.bootstrap(RootTestModule);
|
|
114
|
+
|
|
115
|
+
return new TestingModule(container, RootTestModule);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class Test {
|
|
120
|
+
static createTestingModule(metadata: ModuleMetadata): TestingModuleBuilder {
|
|
121
|
+
return new TestingModuleBuilder(metadata);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/cqrs/cqrs.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Module, Injectable } from '../core/decorators.ts';
|
|
2
|
+
import { Type } from '../core/metadata.ts';
|
|
3
|
+
import { ModuleRef } from '../core/module-ref.ts';
|
|
4
|
+
import { Subject, Observable } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
export interface ICommand {}
|
|
7
|
+
export interface ICommandHandler<TCommand extends ICommand = any, TResult = any> {
|
|
8
|
+
execute(command: TCommand): Promise<TResult>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IQuery {}
|
|
12
|
+
export interface IQueryHandler<TQuery extends IQuery = any, TResult = any> {
|
|
13
|
+
execute(query: TQuery): Promise<TResult>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IEvent {}
|
|
17
|
+
export interface IEventHandler<TEvent extends IEvent = any> {
|
|
18
|
+
handle(event: TEvent): any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import { METADATA_KEYS } from '../core/metadata.ts';
|
|
22
|
+
|
|
23
|
+
export const COMMAND_HANDLER_METADATA = 'cqrs:command_handler';
|
|
24
|
+
export const QUERY_HANDLER_METADATA = 'cqrs:query_handler';
|
|
25
|
+
export const EVENT_HANDLER_METADATA = 'cqrs:event_handler';
|
|
26
|
+
export const SAGA_METADATA = 'cqrs:saga';
|
|
27
|
+
|
|
28
|
+
export const CommandHandler = (command: Type<ICommand>): ClassDecorator => {
|
|
29
|
+
return (target) => {
|
|
30
|
+
Reflect.defineMetadata(COMMAND_HANDLER_METADATA, command, target);
|
|
31
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const QueryHandler = (query: Type<IQuery>): ClassDecorator => {
|
|
36
|
+
return (target) => {
|
|
37
|
+
Reflect.defineMetadata(QUERY_HANDLER_METADATA, query, target);
|
|
38
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const EventsHandler = (...events: Type<IEvent>[]): ClassDecorator => {
|
|
43
|
+
return (target) => {
|
|
44
|
+
Reflect.defineMetadata(EVENT_HANDLER_METADATA, events, target);
|
|
45
|
+
Reflect.defineMetadata(METADATA_KEYS.INJECTABLE, true, target);
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function Saga(): PropertyDecorator {
|
|
50
|
+
return (target: any, propertyKey: string | symbol) => {
|
|
51
|
+
const constructor = target.constructor;
|
|
52
|
+
const existing = Reflect.getOwnMetadata(SAGA_METADATA, constructor) || [];
|
|
53
|
+
existing.push(propertyKey);
|
|
54
|
+
Reflect.defineMetadata(SAGA_METADATA, existing, constructor);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Injectable()
|
|
59
|
+
export class UnhandledExceptionBus extends Subject<any> {}
|
|
60
|
+
|
|
61
|
+
@Injectable()
|
|
62
|
+
export class CommandBus {
|
|
63
|
+
private handlers = new Map<any, ICommandHandler>();
|
|
64
|
+
|
|
65
|
+
constructor(private readonly exceptionBus: UnhandledExceptionBus) {}
|
|
66
|
+
|
|
67
|
+
register(command: any, handler: ICommandHandler) {
|
|
68
|
+
this.handlers.set(command, handler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async execute<T extends ICommand, R = any>(command: T): Promise<R> {
|
|
72
|
+
const commandClass = command.constructor;
|
|
73
|
+
const handler = this.handlers.get(commandClass);
|
|
74
|
+
if (!handler) {
|
|
75
|
+
throw new Error(`CommandHandler not found for command: ${commandClass.name}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return (await handler.execute(command)) as R;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.exceptionBus.next({ error: err, command });
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Injectable()
|
|
87
|
+
export class QueryBus {
|
|
88
|
+
private handlers = new Map<any, IQueryHandler>();
|
|
89
|
+
|
|
90
|
+
constructor(private readonly exceptionBus: UnhandledExceptionBus) {}
|
|
91
|
+
|
|
92
|
+
register(query: any, handler: IQueryHandler) {
|
|
93
|
+
this.handlers.set(query, handler);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async execute<T extends IQuery, R = any>(query: T): Promise<R> {
|
|
97
|
+
const queryClass = query.constructor;
|
|
98
|
+
const handler = this.handlers.get(queryClass);
|
|
99
|
+
if (!handler) {
|
|
100
|
+
throw new Error(`QueryHandler not found for query: ${queryClass.name}`);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return (await handler.execute(query)) as R;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.exceptionBus.next({ error: err, query });
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Injectable()
|
|
112
|
+
export class EventBus {
|
|
113
|
+
public readonly subject$ = new Subject<any>();
|
|
114
|
+
private handlers = new Map<any, IEventHandler[]>();
|
|
115
|
+
|
|
116
|
+
register(event: any, handler: IEventHandler) {
|
|
117
|
+
const existing = this.handlers.get(event) || [];
|
|
118
|
+
existing.push(handler);
|
|
119
|
+
this.handlers.set(event, existing);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
publish<T extends IEvent>(event: T) {
|
|
123
|
+
const eventClass = event.constructor;
|
|
124
|
+
const eventHandlers = this.handlers.get(eventClass) || [];
|
|
125
|
+
for (const handler of eventHandlers) {
|
|
126
|
+
try {
|
|
127
|
+
handler.handle(event);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(`Error in event handler:`, err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.subject$.next(event);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Injectable()
|
|
137
|
+
export class EventPublisher {
|
|
138
|
+
constructor(private readonly eventBus: EventBus) {}
|
|
139
|
+
|
|
140
|
+
mergeClassContext<T extends Type<any>>(metatype: T): T {
|
|
141
|
+
const eventBus = this.eventBus;
|
|
142
|
+
return class extends metatype {
|
|
143
|
+
publish(event: IEvent) {
|
|
144
|
+
eventBus.publish(event);
|
|
145
|
+
}
|
|
146
|
+
publishAll(events: IEvent[]) {
|
|
147
|
+
for (const event of events) {
|
|
148
|
+
eventBus.publish(event);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
mergeObjectContext<T extends Object>(object: T): T {
|
|
155
|
+
const eventBus = this.eventBus;
|
|
156
|
+
return Object.assign(object, {
|
|
157
|
+
publish: (event: IEvent) => eventBus.publish(event),
|
|
158
|
+
publishAll: (events: IEvent[]) => {
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
eventBus.publish(event);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class AggregateRoot<EventBase extends IEvent = IEvent> {
|
|
168
|
+
private readonly _events: EventBase[] = [];
|
|
169
|
+
autoCommit = false;
|
|
170
|
+
|
|
171
|
+
publish(event: EventBase) {}
|
|
172
|
+
publishAll(events: EventBase[]) {}
|
|
173
|
+
|
|
174
|
+
commit() {
|
|
175
|
+
this.publishAll(this._events);
|
|
176
|
+
this._events.length = 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
uncommit() {
|
|
180
|
+
this._events.length = 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getUncommittedEvents(): EventBase[] {
|
|
184
|
+
return this._events;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
loadFromHistory(history: EventBase[]) {
|
|
188
|
+
history.forEach((event) => this.apply(event, true));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
apply(event: EventBase, isFromHistory = false) {
|
|
192
|
+
if (!isFromHistory) {
|
|
193
|
+
this._events.push(event);
|
|
194
|
+
}
|
|
195
|
+
const handlerName = `on${event.constructor.name}`;
|
|
196
|
+
if ((this as any)[handlerName]) {
|
|
197
|
+
(this as any)[handlerName](event);
|
|
198
|
+
}
|
|
199
|
+
if (this.autoCommit) {
|
|
200
|
+
this.commit();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Module({
|
|
206
|
+
providers: [CommandBus, QueryBus, EventBus, EventPublisher, UnhandledExceptionBus],
|
|
207
|
+
exports: [CommandBus, QueryBus, EventBus, EventPublisher, UnhandledExceptionBus],
|
|
208
|
+
})
|
|
209
|
+
export class CqrsModule {
|
|
210
|
+
constructor(
|
|
211
|
+
private readonly moduleRef: ModuleRef,
|
|
212
|
+
private readonly commandBus: CommandBus,
|
|
213
|
+
private readonly queryBus: QueryBus,
|
|
214
|
+
private readonly eventBus: EventBus
|
|
215
|
+
) {}
|
|
216
|
+
|
|
217
|
+
onModuleInit() {
|
|
218
|
+
const container = (this.moduleRef as any).container;
|
|
219
|
+
if (!container) return;
|
|
220
|
+
|
|
221
|
+
const instances = container.getProviderAndControllerInstances();
|
|
222
|
+
for (const inst of instances) {
|
|
223
|
+
if (!inst || !inst.constructor) continue;
|
|
224
|
+
|
|
225
|
+
const command = Reflect.getMetadata(COMMAND_HANDLER_METADATA, inst.constructor);
|
|
226
|
+
if (command) {
|
|
227
|
+
this.commandBus.register(command, inst);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const query = Reflect.getMetadata(QUERY_HANDLER_METADATA, inst.constructor);
|
|
231
|
+
if (query) {
|
|
232
|
+
this.queryBus.register(query, inst);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const events = Reflect.getMetadata(EVENT_HANDLER_METADATA, inst.constructor);
|
|
236
|
+
if (events && Array.isArray(events)) {
|
|
237
|
+
for (const event of events) {
|
|
238
|
+
this.eventBus.register(event, inst);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check Sagas
|
|
243
|
+
const sagas: (string | symbol)[] = Reflect.getMetadata(SAGA_METADATA, inst.constructor) || [];
|
|
244
|
+
for (const sagaKey of sagas) {
|
|
245
|
+
const sagaFn = inst[sagaKey];
|
|
246
|
+
if (typeof sagaFn === 'function') {
|
|
247
|
+
const commands$ = sagaFn(this.eventBus.subject$);
|
|
248
|
+
if (commands$ && typeof commands$.subscribe === 'function') {
|
|
249
|
+
commands$.subscribe({
|
|
250
|
+
next: (cmd: any) => {
|
|
251
|
+
this.commandBus.execute(cmd).catch((err) => {
|
|
252
|
+
console.error(`Saga error executing command:`, err);
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
error: (err: any) => {
|
|
256
|
+
console.error(`Saga stream error:`, err);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { Module, DynamicModule, Inject } from '../core/decorators.ts';
|
|
3
|
+
import { ConnectionManager } from './typeorm.module.ts';
|
|
4
|
+
|
|
5
|
+
let isSequelizeAvailable = false;
|
|
6
|
+
try {
|
|
7
|
+
require.resolve('sequelize');
|
|
8
|
+
isSequelizeAvailable = true;
|
|
9
|
+
} catch {
|
|
10
|
+
// ignore
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Model<T extends Record<string, any> = any> {
|
|
14
|
+
static tableName: string;
|
|
15
|
+
static db: Database;
|
|
16
|
+
static isNative = true;
|
|
17
|
+
static realModel: any = null;
|
|
18
|
+
|
|
19
|
+
constructor(values?: Partial<T>) {
|
|
20
|
+
if (values) {
|
|
21
|
+
Object.assign(this, values);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static async findAll(options?: any): Promise<any[]> {
|
|
26
|
+
if (!this.isNative && this.realModel) {
|
|
27
|
+
const results = await this.realModel.findAll(options);
|
|
28
|
+
return results.map((r: any) => {
|
|
29
|
+
const inst = new this();
|
|
30
|
+
Object.assign(inst, r.toJSON ? r.toJSON() : r);
|
|
31
|
+
return inst;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const tableName = this.tableName || this.name.toLowerCase();
|
|
35
|
+
const db = this.db || ConnectionManager.getOrCreate();
|
|
36
|
+
const rows = db.query(`SELECT id, data FROM ${tableName}`).all() as any[];
|
|
37
|
+
return rows.map((row) => {
|
|
38
|
+
const dataObj = JSON.parse(row.data);
|
|
39
|
+
dataObj.id = row.id;
|
|
40
|
+
const inst = new this();
|
|
41
|
+
Object.assign(inst, dataObj);
|
|
42
|
+
return inst;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async findOne(options?: any): Promise<any | null> {
|
|
47
|
+
const list = await this.findAll(options);
|
|
48
|
+
return list.length > 0 ? list[0] : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static async findByPk(id: number | string): Promise<any | null> {
|
|
52
|
+
if (!this.isNative && this.realModel) {
|
|
53
|
+
const r = await this.realModel.findByPk(id);
|
|
54
|
+
if (!r) return null;
|
|
55
|
+
const inst = new this();
|
|
56
|
+
Object.assign(inst, r.toJSON ? r.toJSON() : r);
|
|
57
|
+
return inst;
|
|
58
|
+
}
|
|
59
|
+
const tableName = this.tableName || this.name.toLowerCase();
|
|
60
|
+
const db = this.db || ConnectionManager.getOrCreate();
|
|
61
|
+
const row = db.query(`SELECT id, data FROM ${tableName} WHERE id = $id`).get({ $id: Number(id) }) as any;
|
|
62
|
+
if (!row) return null;
|
|
63
|
+
const dataObj = JSON.parse(row.data);
|
|
64
|
+
dataObj.id = row.id;
|
|
65
|
+
const inst = new this();
|
|
66
|
+
Object.assign(inst, dataObj);
|
|
67
|
+
return inst;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static async create(values: any): Promise<any> {
|
|
71
|
+
if (!this.isNative && this.realModel) {
|
|
72
|
+
const r = await this.realModel.create(values);
|
|
73
|
+
const inst = new this();
|
|
74
|
+
Object.assign(inst, r.toJSON ? r.toJSON() : r);
|
|
75
|
+
return inst;
|
|
76
|
+
}
|
|
77
|
+
const tableName = this.tableName || this.name.toLowerCase();
|
|
78
|
+
const db = this.db || ConnectionManager.getOrCreate();
|
|
79
|
+
db.run(`
|
|
80
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
data TEXT
|
|
83
|
+
)
|
|
84
|
+
`);
|
|
85
|
+
const dataCopy = { ...values };
|
|
86
|
+
delete dataCopy.id;
|
|
87
|
+
const result = db.query(`INSERT INTO ${tableName} (data) VALUES ($data) RETURNING id`).get({
|
|
88
|
+
$data: JSON.stringify(dataCopy)
|
|
89
|
+
}) as any;
|
|
90
|
+
const inst = new this();
|
|
91
|
+
Object.assign(inst, values, { id: result.id });
|
|
92
|
+
return inst;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async save(): Promise<this> {
|
|
96
|
+
const modelClass = this.constructor as any;
|
|
97
|
+
if (!modelClass.isNative && modelClass.realModel) {
|
|
98
|
+
const realInst = await modelClass.realModel.build(this);
|
|
99
|
+
await realInst.save();
|
|
100
|
+
Object.assign(this, realInst.toJSON());
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
const tableName = modelClass.tableName || modelClass.name.toLowerCase();
|
|
104
|
+
const db = modelClass.db || ConnectionManager.getOrCreate();
|
|
105
|
+
const self = this as any;
|
|
106
|
+
const dataCopy = { ...this };
|
|
107
|
+
const id = self.id;
|
|
108
|
+
delete (dataCopy as any).id;
|
|
109
|
+
|
|
110
|
+
if (id !== undefined && id !== null) {
|
|
111
|
+
db.query(`UPDATE ${tableName} SET data = $data WHERE id = $id`).run({
|
|
112
|
+
$data: JSON.stringify(dataCopy),
|
|
113
|
+
$id: id
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
const result = db.query(`INSERT INTO ${tableName} (data) VALUES ($data) RETURNING id`).get({
|
|
117
|
+
$data: JSON.stringify(dataCopy)
|
|
118
|
+
}) as any;
|
|
119
|
+
self.id = result.id;
|
|
120
|
+
}
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async update(values: any): Promise<this> {
|
|
125
|
+
Object.assign(this, values);
|
|
126
|
+
return await this.save();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async destroy(): Promise<void> {
|
|
130
|
+
const modelClass = this.constructor as any;
|
|
131
|
+
if (!modelClass.isNative && modelClass.realModel) {
|
|
132
|
+
const realInst = await modelClass.realModel.findByPk((this as any).id);
|
|
133
|
+
if (realInst) await realInst.destroy();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const tableName = modelClass.tableName || modelClass.name.toLowerCase();
|
|
137
|
+
const db = modelClass.db || ConnectionManager.getOrCreate();
|
|
138
|
+
const id = (this as any).id;
|
|
139
|
+
if (id !== undefined && id !== null) {
|
|
140
|
+
db.query(`DELETE FROM ${tableName} WHERE id = $id`).run({ $id: id });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function InjectModel(model: any): ParameterDecorator & PropertyDecorator {
|
|
146
|
+
return Inject(`Sequelize_Model_${model.name}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface SequelizeModuleOptions {
|
|
150
|
+
dialect?: string;
|
|
151
|
+
storage?: string;
|
|
152
|
+
host?: string;
|
|
153
|
+
port?: number;
|
|
154
|
+
username?: string;
|
|
155
|
+
password?: string;
|
|
156
|
+
database?: string;
|
|
157
|
+
models?: any[];
|
|
158
|
+
autoLoadModels?: boolean;
|
|
159
|
+
synchronize?: boolean;
|
|
160
|
+
[key: string]: any;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@Module({})
|
|
164
|
+
export class SequelizeModule {
|
|
165
|
+
static forRoot(options: SequelizeModuleOptions = {}): DynamicModule {
|
|
166
|
+
const isUsingSequelize = isSequelizeAvailable && options.dialect !== 'sqlite-native';
|
|
167
|
+
|
|
168
|
+
let seqPromise: Promise<any>;
|
|
169
|
+
if (isUsingSequelize) {
|
|
170
|
+
seqPromise = (async () => {
|
|
171
|
+
const { Sequelize } = await import('sequelize');
|
|
172
|
+
const sequelize = new Sequelize(options as any);
|
|
173
|
+
if (options.models) {
|
|
174
|
+
sequelize.addModels(options.models);
|
|
175
|
+
}
|
|
176
|
+
await sequelize.authenticate();
|
|
177
|
+
if (options.synchronize !== false) {
|
|
178
|
+
await sequelize.sync();
|
|
179
|
+
}
|
|
180
|
+
return sequelize;
|
|
181
|
+
})();
|
|
182
|
+
} else {
|
|
183
|
+
seqPromise = Promise.resolve(ConnectionManager.getOrCreate(options.storage));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
module: SequelizeModule,
|
|
188
|
+
providers: [
|
|
189
|
+
{
|
|
190
|
+
provide: 'Calyx_Sequelize_Instance',
|
|
191
|
+
useValue: seqPromise,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
provide: 'Calyx_Sequelize_IsNative',
|
|
195
|
+
useValue: !isUsingSequelize,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
exports: ['Calyx_Sequelize_Instance', 'Calyx_Sequelize_IsNative'],
|
|
199
|
+
global: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static forFeature(models: any[] = []): DynamicModule {
|
|
204
|
+
const providers = models.map((model) => {
|
|
205
|
+
return {
|
|
206
|
+
provide: `Sequelize_Model_${model.name}`,
|
|
207
|
+
useFactory: (seqInstancePromise: Promise<any>, isNative: boolean) => {
|
|
208
|
+
if (isNative) {
|
|
209
|
+
const db = ConnectionManager.getOrCreate();
|
|
210
|
+
model.db = db;
|
|
211
|
+
model.tableName = model.name.toLowerCase();
|
|
212
|
+
model.isNative = true;
|
|
213
|
+
db.run(`
|
|
214
|
+
CREATE TABLE IF NOT EXISTS ${model.tableName} (
|
|
215
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
216
|
+
data TEXT
|
|
217
|
+
)
|
|
218
|
+
`);
|
|
219
|
+
return model;
|
|
220
|
+
} else {
|
|
221
|
+
return seqInstancePromise.then((seq) => {
|
|
222
|
+
const realModel = seq.models[model.name];
|
|
223
|
+
model.realModel = realModel;
|
|
224
|
+
model.isNative = false;
|
|
225
|
+
return model;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
inject: ['Calyx_Sequelize_Instance', 'Calyx_Sequelize_IsNative'],
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
module: SequelizeModule,
|
|
235
|
+
providers,
|
|
236
|
+
exports: models.map((model) => `Sequelize_Model_${model.name}`),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
|
|
3
|
-
export function OnEvent(event: string): MethodDecorator {
|
|
3
|
+
export function OnEvent(event: string, options?: any): MethodDecorator {
|
|
4
4
|
return (target, propertyKey) => {
|
|
5
5
|
const constructor = target.constructor;
|
|
6
6
|
const existing = Reflect.getOwnMetadata('calyx:on_event', constructor) || [];
|
|
7
|
-
existing.push({ event, propertyKey });
|
|
7
|
+
existing.push({ event, propertyKey, options });
|
|
8
8
|
Reflect.defineMetadata('calyx:on_event', existing, constructor);
|
|
9
9
|
};
|
|
10
10
|
}
|