@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.
@@ -0,0 +1,272 @@
1
+ import { expect, test, describe } from 'bun:test';
2
+ import {
3
+ Test,
4
+ LazyModuleLoader,
5
+ Injectable,
6
+ Module,
7
+ ParseIntPipe,
8
+ ParseBoolPipe,
9
+ CalyxResponse,
10
+ SchedulerRegistry,
11
+ ScheduleModule,
12
+ Cron,
13
+ Interval,
14
+ CacheKey,
15
+ CacheTTL,
16
+ Client,
17
+ ClientsModule,
18
+ HealthCheckService,
19
+ TerminusModule,
20
+ CqrsModule,
21
+ CommandBus,
22
+ QueryBus,
23
+ EventBus,
24
+ CommandHandler,
25
+ QueryHandler,
26
+ EventsHandler,
27
+ ICommand,
28
+ IQuery,
29
+ IEvent,
30
+ ICommandHandler,
31
+ IQueryHandler,
32
+ IEventHandler,
33
+ } from '../src/index.ts';
34
+
35
+ @Injectable()
36
+ class DummyService {
37
+ getValue() { return 'dummy'; }
38
+ }
39
+
40
+ @Injectable()
41
+ class AliasService {
42
+ constructor(public readonly dummy: DummyService) {}
43
+ }
44
+
45
+ @Module({
46
+ providers: [
47
+ DummyService,
48
+ {
49
+ provide: 'ALIAS_TOKEN',
50
+ useExisting: DummyService,
51
+ },
52
+ AliasService,
53
+ ],
54
+ })
55
+ class TestParityModule {}
56
+
57
+ describe('NestJS Parity Extensions', () => {
58
+
59
+ test('DI: useExisting alias provider resolution', async () => {
60
+ const moduleRef = await Test.createTestingModule({
61
+ imports: [TestParityModule],
62
+ }).compile();
63
+
64
+ const dummy = moduleRef.get(DummyService);
65
+ const alias = moduleRef.get('ALIAS_TOKEN');
66
+ expect(dummy).toBe(alias);
67
+ expect(dummy.getValue()).toBe('dummy');
68
+ await moduleRef.close();
69
+ });
70
+
71
+ test('DI: Test/TestingModule overrides', async () => {
72
+ const moduleRef = await Test.createTestingModule({
73
+ imports: [TestParityModule],
74
+ })
75
+ .overrideProvider(DummyService)
76
+ .useValue({ getValue: () => 'mocked' })
77
+ .compile();
78
+
79
+ const dummy = moduleRef.get(DummyService);
80
+ expect(dummy.getValue()).toBe('mocked');
81
+ await moduleRef.close();
82
+ });
83
+
84
+ test('DI: LazyModuleLoader', async () => {
85
+ const moduleRef = await Test.createTestingModule({
86
+ imports: [],
87
+ providers: [LazyModuleLoader],
88
+ }).compile();
89
+
90
+ const loader = moduleRef.get(LazyModuleLoader);
91
+ const lazyModuleRef = await loader.load(() => Promise.resolve(TestParityModule));
92
+ expect(lazyModuleRef).toBeDefined();
93
+
94
+ const dummy = lazyModuleRef.get(DummyService);
95
+ expect(dummy.getValue()).toBe('dummy');
96
+ await moduleRef.close();
97
+ });
98
+
99
+ test('Pipes: built-in parsing pipes', () => {
100
+ const intPipe = new ParseIntPipe();
101
+ expect(intPipe.transform('42', { type: 'query', data: 'id' })).toBe(42);
102
+ expect(() => intPipe.transform('abc', { type: 'query', data: 'id' })).toThrow();
103
+
104
+ const boolPipe = new ParseBoolPipe();
105
+ expect(boolPipe.transform('true', { type: 'query', data: 'flag' })).toBe(true);
106
+ expect(boolPipe.transform('false', { type: 'query', data: 'flag' })).toBe(false);
107
+ expect(() => boolPipe.transform('abc', { type: 'query', data: 'flag' })).toThrow();
108
+ });
109
+
110
+ test('CalyxResponse compatibility methods', () => {
111
+ const res = new CalyxResponse();
112
+ res.header('X-Test', 'value')
113
+ .type('text/html')
114
+ .cookie('cookie_name', 'cookie_val')
115
+ .append('X-Test', 'another');
116
+
117
+ expect(res.get('x-test')).toBe('value, another');
118
+ expect(res.get('content-type')).toBe('text/html');
119
+ });
120
+
121
+ test('SchedulerRegistry named dynamic tasks', async () => {
122
+ @Injectable()
123
+ class ScheduledTasks {
124
+ @Cron('* * * * * *', { name: 'my-cron' })
125
+ runCron() {}
126
+
127
+ @Interval('my-interval', 1000)
128
+ runInterval() {}
129
+ }
130
+
131
+ @Module({
132
+ imports: [ScheduleModule.forRoot()],
133
+ providers: [ScheduledTasks],
134
+ })
135
+ class RootScheduleModule {}
136
+
137
+ const moduleRef = await Test.createTestingModule({
138
+ imports: [RootScheduleModule],
139
+ }).compile();
140
+
141
+ const app = moduleRef.createCalyxApplication();
142
+ await app.init();
143
+
144
+ const registry = moduleRef.get(SchedulerRegistry);
145
+ expect(registry.getCronJob('my-cron')).toBeDefined();
146
+ expect(registry.getInterval('my-interval')).toBeDefined();
147
+
148
+ registry.deleteCronJob('my-cron');
149
+ registry.deleteInterval('my-interval');
150
+
151
+ expect(() => registry.getCronJob('my-cron')).toThrow();
152
+ expect(() => registry.getInterval('my-interval')).toThrow();
153
+
154
+ await app.close();
155
+ await moduleRef.close();
156
+ });
157
+
158
+ test('CacheInterceptor key/ttl retrieval', () => {
159
+ class Target {
160
+ @CacheKey('custom_key')
161
+ @CacheTTL(100)
162
+ handler() {}
163
+ }
164
+
165
+ const t = new Target();
166
+ const key = Reflect.getMetadata('cache_metadata_key', t.handler);
167
+ const ttl = Reflect.getMetadata('cache_metadata_ttl', t.handler);
168
+
169
+ expect(key).toBe('custom_key');
170
+ expect(ttl).toBe(100);
171
+ });
172
+
173
+ test('Microservice clients: @Client and ClientsModule', async () => {
174
+ class Target {
175
+ @Client({ options: { host: '127.0.0.1', port: 1234 } })
176
+ client: any;
177
+ }
178
+
179
+ const target = new Target();
180
+ expect(target.client).toBeDefined();
181
+
182
+ const moduleRef = await Test.createTestingModule({
183
+ imports: [
184
+ ClientsModule.register([
185
+ { name: 'TEST_SERVICE', options: { host: 'localhost', port: 5000 } },
186
+ ]),
187
+ ],
188
+ }).compile();
189
+
190
+ const service = moduleRef.get('TEST_SERVICE');
191
+ expect(service).toBeDefined();
192
+ await moduleRef.close();
193
+ });
194
+
195
+ test('CQRS command/query/event buses', async () => {
196
+ class MyCommand implements ICommand {}
197
+ class MyQuery implements IQuery {}
198
+ class MyEvent implements IEvent {}
199
+
200
+ let commandHandled = false;
201
+ let queryHandled = false;
202
+ let eventHandledCount = 0;
203
+
204
+ @CommandHandler(MyCommand)
205
+ class MyCommandHandler implements ICommandHandler<MyCommand> {
206
+ async execute(command: MyCommand) {
207
+ commandHandled = true;
208
+ return 'command-result';
209
+ }
210
+ }
211
+
212
+ @QueryHandler(MyQuery)
213
+ class MyQueryHandler implements IQueryHandler<MyQuery> {
214
+ async execute(query: MyQuery) {
215
+ queryHandled = true;
216
+ return 'query-result';
217
+ }
218
+ }
219
+
220
+ @EventsHandler(MyEvent)
221
+ class MyEventHandler implements IEventHandler<MyEvent> {
222
+ handle(event: MyEvent) {
223
+ eventHandledCount++;
224
+ }
225
+ }
226
+
227
+ const moduleRef = await Test.createTestingModule({
228
+ imports: [CqrsModule],
229
+ providers: [MyCommandHandler, MyQueryHandler, MyEventHandler],
230
+ }).compile();
231
+
232
+ const commandBus = moduleRef.get(CommandBus);
233
+ const queryBus = moduleRef.get(QueryBus);
234
+ const eventBus = moduleRef.get(EventBus);
235
+
236
+ const cqrsModule = moduleRef.get(CqrsModule);
237
+ (cqrsModule as any).onModuleInit();
238
+
239
+ const cmdRes = await commandBus.execute(new MyCommand());
240
+ expect(cmdRes).toBe('command-result');
241
+ expect(commandHandled).toBe(true);
242
+
243
+ const qryRes = await queryBus.execute(new MyQuery());
244
+ expect(qryRes).toBe('query-result');
245
+ expect(queryHandled).toBe(true);
246
+
247
+ eventBus.publish(new MyEvent());
248
+ expect(eventHandledCount).toBe(1);
249
+
250
+ await moduleRef.close();
251
+ });
252
+
253
+ test('Terminus health check service', async () => {
254
+ const moduleRef = await Test.createTestingModule({
255
+ imports: [TerminusModule],
256
+ }).compile();
257
+
258
+ const health = moduleRef.get(HealthCheckService);
259
+ const res = await health.check([
260
+ () => ({ db: { status: 'up' } }),
261
+ ]);
262
+
263
+ expect(res.status).toBe('ok');
264
+ expect(res.details.db.status).toBe('up');
265
+
266
+ expect(health.check([
267
+ () => { throw new Error('DB connection lost'); }
268
+ ])).rejects.toThrow();
269
+
270
+ await moduleRef.close();
271
+ });
272
+ });
@@ -19,6 +19,10 @@ import {
19
19
  DocumentBuilder,
20
20
  SwaggerModule,
21
21
  PartialType,
22
+ IsString,
23
+ IsNumber,
24
+ IsOptional,
25
+ IsEmail,
22
26
  } from '../src/index.ts';
23
27
 
24
28
  // 1. DTO Model
@@ -42,6 +46,22 @@ class CreateItemDto {
42
46
  // 3. Partial DTO using mapped type
43
47
  class UpdateItemDto extends PartialType(CreateItemDto) {}
44
48
 
49
+ // 4. Validation-rich DTO (without explicit @ApiProperty)
50
+ class ValidationRichDto {
51
+ @IsString()
52
+ title!: string;
53
+
54
+ @IsNumber()
55
+ amount!: number;
56
+
57
+ @IsOptional()
58
+ @IsString()
59
+ note?: string;
60
+
61
+ @IsEmail()
62
+ contactEmail!: string;
63
+ }
64
+
45
65
  @ApiTags('Items')
46
66
  @ApiBearerAuth('jwt')
47
67
  @Controller('items')
@@ -66,6 +86,12 @@ class ItemsController {
66
86
  updateItem(@Body() body: UpdateItemDto) {
67
87
  return { id: 3, ...body };
68
88
  }
89
+
90
+ @Post('validate')
91
+ @ApiOperation({ summary: 'Validation endpoint' })
92
+ validateDto(@Body() body: ValidationRichDto) {
93
+ return body;
94
+ }
69
95
  }
70
96
 
71
97
  @Module({
@@ -150,6 +176,21 @@ describe('OpenAPI (Swagger) Generation', () => {
150
176
  expect(spec.components.schemas.UpdateItemDto).toBeDefined();
151
177
  expect(spec.components.schemas.UpdateItemDto.required).toBeUndefined(); // all optional
152
178
  expect(spec.components.schemas.UpdateItemDto.properties.name.type).toBe('string');
179
+
180
+ // 6. Validation Decorator Schema Auto-enrichment
181
+ expect(spec.components.schemas.ValidationRichDto).toBeDefined();
182
+ const richSchema = spec.components.schemas.ValidationRichDto;
183
+ expect(richSchema.properties.title.type).toBe('string');
184
+ expect(richSchema.properties.amount.type).toBe('number');
185
+ expect(richSchema.properties.note.type).toBe('string');
186
+ expect(richSchema.properties.contactEmail.type).toBe('string');
187
+ expect(richSchema.properties.contactEmail.format).toBe('email');
188
+
189
+ // Ensure note is optional, but others are required
190
+ expect(richSchema.required).toContain('title');
191
+ expect(richSchema.required).toContain('amount');
192
+ expect(richSchema.required).toContain('contactEmail');
193
+ expect(richSchema.required).not.toContain('note');
153
194
  });
154
195
 
155
196
  test('should serve Swagger UI html wrapper', async () => {