@geekmidas/cli 0.10.0 → 0.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.
Files changed (146) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-B1qy9b-j.cjs +112 -0
  3. package/dist/bundler-B1qy9b-j.cjs.map +1 -0
  4. package/dist/bundler-DskIqW2t.mjs +111 -0
  5. package/dist/bundler-DskIqW2t.mjs.map +1 -0
  6. package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +1 -1
  14. package/dist/encryption-C8H-38Yy.mjs +42 -0
  15. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  16. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  17. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  18. package/dist/index.cjs +2123 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2141 -192
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
  23. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  24. package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
  25. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  26. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  27. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  28. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  29. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  30. package/dist/openapi-react-query.cjs +1 -1
  31. package/dist/openapi-react-query.d.cts.map +1 -1
  32. package/dist/openapi-react-query.d.mts.map +1 -1
  33. package/dist/openapi-react-query.mjs +1 -1
  34. package/dist/openapi.cjs +2 -2
  35. package/dist/openapi.d.cts +1 -1
  36. package/dist/openapi.d.cts.map +1 -1
  37. package/dist/openapi.d.mts +1 -1
  38. package/dist/openapi.d.mts.map +1 -1
  39. package/dist/openapi.mjs +2 -2
  40. package/dist/storage-BOOpAF8N.cjs +5 -0
  41. package/dist/storage-Bj1E26lU.cjs +187 -0
  42. package/dist/storage-Bj1E26lU.cjs.map +1 -0
  43. package/dist/storage-kSxTjkNb.mjs +133 -0
  44. package/dist/storage-kSxTjkNb.mjs.map +1 -0
  45. package/dist/storage-tgZSUnKl.mjs +3 -0
  46. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  47. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  48. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  49. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  50. package/examples/cron-example.ts +27 -27
  51. package/examples/env.ts +27 -27
  52. package/examples/function-example.ts +31 -31
  53. package/examples/gkm.config.json +20 -20
  54. package/examples/gkm.config.ts +8 -8
  55. package/examples/gkm.minimal.config.json +5 -5
  56. package/examples/gkm.production.config.json +25 -25
  57. package/examples/logger.ts +2 -2
  58. package/package.json +6 -6
  59. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  60. package/src/__tests__/config.spec.ts +55 -55
  61. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  62. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  63. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  64. package/src/__tests__/openapi.spec.ts +428 -428
  65. package/src/__tests__/test-helpers.ts +76 -76
  66. package/src/auth/__tests__/credentials.spec.ts +204 -0
  67. package/src/auth/__tests__/index.spec.ts +168 -0
  68. package/src/auth/credentials.ts +187 -0
  69. package/src/auth/index.ts +226 -0
  70. package/src/build/__tests__/bundler.spec.ts +444 -0
  71. package/src/build/__tests__/index-new.spec.ts +474 -474
  72. package/src/build/__tests__/manifests.spec.ts +333 -333
  73. package/src/build/bundler.ts +210 -0
  74. package/src/build/endpoint-analyzer.ts +236 -0
  75. package/src/build/handler-templates.ts +1253 -0
  76. package/src/build/index.ts +260 -179
  77. package/src/build/manifests.ts +52 -52
  78. package/src/build/providerResolver.ts +145 -145
  79. package/src/build/types.ts +64 -43
  80. package/src/config.ts +39 -39
  81. package/src/deploy/__tests__/docker.spec.ts +111 -0
  82. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  83. package/src/deploy/__tests__/init.spec.ts +662 -0
  84. package/src/deploy/docker.ts +128 -0
  85. package/src/deploy/dokploy.ts +204 -0
  86. package/src/deploy/index.ts +136 -0
  87. package/src/deploy/init.ts +484 -0
  88. package/src/deploy/types.ts +48 -0
  89. package/src/dev/__tests__/index.spec.ts +266 -266
  90. package/src/dev/index.ts +647 -601
  91. package/src/docker/__tests__/compose.spec.ts +531 -0
  92. package/src/docker/__tests__/templates.spec.ts +280 -0
  93. package/src/docker/compose.ts +273 -0
  94. package/src/docker/index.ts +230 -0
  95. package/src/docker/templates.ts +446 -0
  96. package/src/generators/CronGenerator.ts +72 -72
  97. package/src/generators/EndpointGenerator.ts +699 -398
  98. package/src/generators/FunctionGenerator.ts +84 -84
  99. package/src/generators/Generator.ts +72 -72
  100. package/src/generators/OpenApiTsGenerator.ts +577 -577
  101. package/src/generators/SubscriberGenerator.ts +124 -124
  102. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  103. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  104. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  105. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  106. package/src/generators/index.ts +4 -4
  107. package/src/index.ts +623 -201
  108. package/src/init/__tests__/generators.spec.ts +334 -334
  109. package/src/init/__tests__/init.spec.ts +332 -332
  110. package/src/init/__tests__/utils.spec.ts +89 -89
  111. package/src/init/generators/config.ts +175 -175
  112. package/src/init/generators/docker.ts +41 -41
  113. package/src/init/generators/env.ts +72 -72
  114. package/src/init/generators/index.ts +1 -1
  115. package/src/init/generators/models.ts +64 -64
  116. package/src/init/generators/monorepo.ts +161 -161
  117. package/src/init/generators/package.ts +71 -71
  118. package/src/init/generators/source.ts +6 -6
  119. package/src/init/index.ts +203 -208
  120. package/src/init/templates/api.ts +115 -115
  121. package/src/init/templates/index.ts +75 -75
  122. package/src/init/templates/minimal.ts +98 -98
  123. package/src/init/templates/serverless.ts +89 -89
  124. package/src/init/templates/worker.ts +98 -98
  125. package/src/init/utils.ts +54 -56
  126. package/src/openapi-react-query.ts +194 -194
  127. package/src/openapi.ts +63 -63
  128. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  129. package/src/secrets/__tests__/generator.spec.ts +319 -0
  130. package/src/secrets/__tests__/index.spec.ts +91 -0
  131. package/src/secrets/__tests__/storage.spec.ts +611 -0
  132. package/src/secrets/encryption.ts +91 -0
  133. package/src/secrets/generator.ts +164 -0
  134. package/src/secrets/index.ts +383 -0
  135. package/src/secrets/storage.ts +192 -0
  136. package/src/secrets/types.ts +53 -0
  137. package/src/types.ts +295 -176
  138. package/tsdown.config.ts +11 -8
  139. package/dist/config-BrkUalUh.mjs.map +0 -1
  140. package/dist/config-C9aXOHBe.cjs.map +0 -1
  141. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  142. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  143. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  144. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  145. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  146. package/dist/types-b-vwGpqc.d.cts.map +0 -1
@@ -1,395 +1,410 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import {
4
- type Subscriber,
5
- SubscriberBuilder,
4
+ type Subscriber,
5
+ SubscriberBuilder,
6
6
  } from '@geekmidas/constructs/subscribers';
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
9
  import {
10
- cleanupDir,
11
- createMockBuildContext,
12
- createTempDir,
10
+ cleanupDir,
11
+ createMockBuildContext,
12
+ createTempDir,
13
13
  } from '../../__tests__/test-helpers';
14
14
  import type { GeneratedConstruct } from '../Generator';
15
15
  import { SubscriberGenerator } from '../SubscriberGenerator';
16
16
 
17
17
  describe('SubscriberGenerator', () => {
18
- let tempDir: string;
19
- let outputDir: string;
20
- let generator: SubscriberGenerator;
21
- let context: ReturnType<typeof createMockBuildContext>;
22
-
23
- beforeEach(async () => {
24
- tempDir = await createTempDir();
25
- outputDir = join(tempDir, 'output');
26
- generator = new SubscriberGenerator();
27
- context = createMockBuildContext();
28
- });
29
-
30
- afterEach(async () => {
31
- await cleanupDir(tempDir);
32
- });
33
-
34
- describe('isConstruct', () => {
35
- it('should identify valid subscribers', async () => {
36
- const testSubscriber = new SubscriberBuilder()
37
- .subscribe(['user.created'] as any)
38
- .handle(async ({ events, logger }) => {
39
- logger.info({ eventCount: events.length }, 'Processing events');
40
- });
41
-
42
- expect(generator.isConstruct(testSubscriber)).toBe(true);
43
- });
44
-
45
- it('should reject invalid constructs', () => {
46
- expect(generator.isConstruct({})).toBe(false);
47
- expect(generator.isConstruct('string')).toBe(false);
48
- expect(generator.isConstruct(null)).toBe(false);
49
- });
50
- });
51
-
52
- describe('build', () => {
53
- const createSubscriberConstruct = (
54
- key: string,
55
- subscribedEvents: string[],
56
- timeout: number = 30000,
57
- ): GeneratedConstruct<Subscriber<any, any, any, any, any, any>> => {
58
- const subscriber = new SubscriberBuilder()
59
- .subscribe(subscribedEvents as any)
60
- .timeout(timeout)
61
- .handle(async ({ events, logger }) => {
62
- logger.info({ eventCount: events.length }, 'Processing events');
63
- });
64
-
65
- return {
66
- key,
67
- name: key.toLowerCase(),
68
- construct: subscriber,
69
- path: {
70
- absolute: join(tempDir, `${key}.ts`),
71
- relative: `${key}.ts`,
72
- },
73
- };
74
- };
75
-
76
- it('should generate subscriber handlers', async () => {
77
- const constructs = [
78
- createSubscriberConstruct('userEventSubscriber', [
79
- 'user.created',
80
- 'user.updated',
81
- ]),
82
- createSubscriberConstruct('orderEventSubscriber', ['order.placed']),
83
- ];
84
-
85
- const subscriberInfos = await generator.build(
86
- context,
87
- constructs,
88
- outputDir,
89
- { provider: 'aws-lambda' },
90
- );
91
-
92
- expect(subscriberInfos).toHaveLength(2);
93
- expect(subscriberInfos[0]).toMatchObject({
94
- name: 'userEventSubscriber',
95
- handler: expect.stringContaining(
96
- 'subscribers/userEventSubscriber.handler',
97
- ),
98
- subscribedEvents: ['user.created', 'user.updated'],
99
- timeout: 30000,
100
- });
101
- expect(subscriberInfos[1]).toMatchObject({
102
- name: 'orderEventSubscriber',
103
- handler: expect.stringContaining(
104
- 'subscribers/orderEventSubscriber.handler',
105
- ),
106
- subscribedEvents: ['order.placed'],
107
- timeout: 30000,
108
- });
109
-
110
- // Check that handler files were created
111
- const userHandlerPath = join(
112
- outputDir,
113
- 'subscribers',
114
- 'userEventSubscriber.ts',
115
- );
116
- const userContent = await readFile(userHandlerPath, 'utf-8');
117
- expect(userContent).toContain('AWSLambdaSubscriber');
118
- expect(userContent).toContain('import { userEventSubscriber }');
119
- expect(userContent).toContain('import envParser');
120
-
121
- const orderHandlerPath = join(
122
- outputDir,
123
- 'subscribers',
124
- 'orderEventSubscriber.ts',
125
- );
126
- const orderContent = await readFile(orderHandlerPath, 'utf-8');
127
- expect(orderContent).toContain('AWSLambdaSubscriber');
128
- expect(orderContent).toContain('import { orderEventSubscriber }');
129
- });
130
-
131
- it('should generate correct relative import paths', async () => {
132
- const subscriber = new SubscriberBuilder()
133
- .subscribe(['event.type'] as any)
134
- .timeout(45000)
135
- .handle(async ({ events, logger }) => {
136
- logger.info({ eventCount: events.length }, 'Processing events');
137
- });
138
-
139
- const construct: GeneratedConstruct<
140
- Subscriber<any, any, any, any, any, any>
141
- > = {
142
- key: 'deepSubscriber',
143
- name: 'deep-subscriber',
144
- construct: subscriber,
145
- path: {
146
- absolute: join(tempDir, 'src/subscribers/deep/processor.ts'),
147
- relative: 'src/subscribers/deep/processor.ts',
148
- },
149
- };
150
-
151
- await generator.build(context, [construct], outputDir);
152
-
153
- const handlerPath = join(outputDir, 'subscribers', 'deepSubscriber.ts');
154
- const handlerContent = await readFile(handlerPath, 'utf-8');
155
-
156
- // Check relative imports are correct
157
- expect(handlerContent).toMatch(
158
- /from ['"].*src\/subscribers\/deep\/processor\.js['"]/,
159
- );
160
- expect(handlerContent).toMatch(/from ['"].*\/env['"]/);
161
- });
162
-
163
- it('should handle subscribers with different timeout values', async () => {
164
- const constructs = [
165
- createSubscriberConstruct('quickSubscriber', ['fast.event'], 15000),
166
- createSubscriberConstruct('slowSubscriber', ['slow.event'], 300000),
167
- ];
168
-
169
- const subscriberInfos = await generator.build(
170
- context,
171
- constructs,
172
- outputDir,
173
- );
174
-
175
- expect(subscriberInfos[0].timeout).toBe(15000);
176
- expect(subscriberInfos[1].timeout).toBe(300000);
177
- });
178
-
179
- it('should handle subscribers with no subscribed events', async () => {
180
- const subscriber = new SubscriberBuilder()
181
- .timeout(30000)
182
- .handle(async ({ events, logger }) => {
183
- logger.info({ eventCount: events.length }, 'Processing all events');
184
- });
185
-
186
- const construct: GeneratedConstruct<
187
- Subscriber<any, any, any, any, any, any>
188
- > = {
189
- key: 'catchAllSubscriber',
190
- name: 'catch-all-subscriber',
191
- construct: subscriber,
192
- path: {
193
- absolute: join(tempDir, 'catchAllSubscriber.ts'),
194
- relative: 'catchAllSubscriber.ts',
195
- },
196
- };
197
-
198
- const subscriberInfos = await generator.build(
199
- context,
200
- [construct],
201
- outputDir,
202
- );
203
-
204
- expect(subscriberInfos[0].subscribedEvents).toEqual([]);
205
- });
206
-
207
- it('should handle subscribers with multiple event types', async () => {
208
- const constructs = [
209
- createSubscriberConstruct('multiEventSubscriber', [
210
- 'user.created',
211
- 'user.updated',
212
- 'user.deleted',
213
- 'order.placed',
214
- ]),
215
- ];
216
-
217
- const subscriberInfos = await generator.build(
218
- context,
219
- constructs,
220
- outputDir,
221
- );
222
-
223
- expect(subscriberInfos[0].subscribedEvents).toEqual([
224
- 'user.created',
225
- 'user.updated',
226
- 'user.deleted',
227
- 'order.placed',
228
- ]);
229
- });
230
-
231
- it('should log generation progress', async () => {
232
- const logSpy = vi.spyOn(console, 'log');
233
-
234
- const constructs = [
235
- createSubscriberConstruct('testSubscriber', ['test.event']),
236
- ];
237
-
238
- await generator.build(context, constructs, outputDir);
239
-
240
- expect(logSpy).toHaveBeenCalledWith(
241
- 'Generated subscriber handler: testSubscriber',
242
- );
243
-
244
- logSpy.mockRestore();
245
- });
246
-
247
- it('should return empty array for empty constructs', async () => {
248
- const subscriberInfos = await generator.build(context, [], outputDir);
249
- expect(subscriberInfos).toEqual([]);
250
- });
251
-
252
- it('should generate subscribers.ts file for server provider even with no subscribers', async () => {
253
- const subscriberInfos = await generator.build(context, [], outputDir, {
254
- provider: 'server',
255
- });
256
-
257
- expect(subscriberInfos).toEqual([]);
258
-
259
- // Check that subscribers.ts was generated
260
- const subscribersPath = join(outputDir, 'subscribers.ts');
261
- const subscribersContent = await readFile(subscribersPath, 'utf-8');
262
-
263
- expect(subscribersContent).toContain(
264
- 'export async function setupSubscribers',
265
- );
266
- expect(subscribersContent).toContain('const subscribers = [');
267
- expect(subscribersContent).toContain('import type { EnvironmentParser }');
268
- expect(subscribersContent).toContain('import type { Logger }');
269
- });
270
-
271
- it('should generate subscribers.ts file for server provider with subscribers', async () => {
272
- const constructs = [
273
- createSubscriberConstruct('userEventSubscriber', [
274
- 'user.created',
275
- 'user.updated',
276
- ]),
277
- createSubscriberConstruct('orderEventSubscriber', ['order.placed']),
278
- ];
279
-
280
- const subscriberInfos = await generator.build(
281
- context,
282
- constructs,
283
- outputDir,
284
- { provider: 'server' },
285
- );
286
-
287
- expect(subscriberInfos).toEqual([]);
288
-
289
- // Check that subscribers.ts was generated
290
- const subscribersPath = join(outputDir, 'subscribers.ts');
291
- const subscribersContent = await readFile(subscribersPath, 'utf-8');
292
-
293
- expect(subscribersContent).toContain(
294
- 'export async function setupSubscribers',
295
- );
296
- expect(subscribersContent).toContain('import { userEventSubscriber }');
297
- expect(subscribersContent).toContain('import { orderEventSubscriber }');
298
- expect(subscribersContent).toContain('const subscribers = [');
299
- expect(subscribersContent).toContain('userEventSubscriber');
300
- expect(subscribersContent).toContain('orderEventSubscriber');
301
- expect(subscribersContent).toContain(
302
- 'Setting up subscribers in polling mode',
303
- );
304
- });
305
-
306
- it('should handle subscribers with custom environment parser patterns', async () => {
307
- const customContext = {
308
- ...context,
309
- envParserImportPattern: '{ customParser as envParser }',
310
- };
311
-
312
- const constructs = [
313
- createSubscriberConstruct('customSubscriber', ['custom.event']),
314
- ];
315
-
316
- await generator.build(customContext, constructs, outputDir);
317
-
318
- const handlerPath = join(outputDir, 'subscribers', 'customSubscriber.ts');
319
- const handlerContent = await readFile(handlerPath, 'utf-8');
320
-
321
- expect(handlerContent).toContain('import { customParser as envParser }');
322
- });
323
-
324
- it('should create subscribers directory if it does not exist', async () => {
325
- const constructs = [
326
- createSubscriberConstruct('firstSubscriber', ['first.event']),
327
- ];
328
-
329
- // outputDir does not exist yet
330
- await generator.build(context, constructs, outputDir);
331
-
332
- const subscribersDir = join(outputDir, 'subscribers');
333
- const handlerPath = join(subscribersDir, 'firstSubscriber.ts');
334
-
335
- // Should be able to read the file, meaning the directory was created
336
- const content = await readFile(handlerPath, 'utf-8');
337
- expect(content).toContain('AWSLambdaSubscriber');
338
- });
339
-
340
- it('should handle exported subscriber with custom name', async () => {
341
- const subscriber = new SubscriberBuilder()
342
- .subscribe(['custom.event'] as any)
343
- .handle(async ({ events, logger }) => {
344
- logger.info({ eventCount: events.length }, 'Processing events');
345
- });
346
-
347
- const construct: GeneratedConstruct<
348
- Subscriber<any, any, any, any, any, any>
349
- > = {
350
- key: 'myCustomSubscriberName',
351
- name: 'custom-name',
352
- construct: subscriber,
353
- path: {
354
- absolute: join(tempDir, 'subscriber.ts'),
355
- relative: 'subscriber.ts',
356
- },
357
- };
358
-
359
- await generator.build(context, [construct], outputDir);
360
-
361
- const handlerPath = join(
362
- outputDir,
363
- 'subscribers',
364
- 'myCustomSubscriberName.ts',
365
- );
366
- const handlerContent = await readFile(handlerPath, 'utf-8');
367
-
368
- expect(handlerContent).toContain(
369
- 'import { myCustomSubscriberName } from',
370
- );
371
- expect(handlerContent).toContain(
372
- 'new AWSLambdaSubscriber(envParser, myCustomSubscriberName)',
373
- );
374
- });
375
-
376
- it('should generate handler files that can be imported', async () => {
377
- const constructs = [
378
- createSubscriberConstruct('validSubscriber', ['valid.event']),
379
- ];
380
-
381
- await generator.build(context, constructs, outputDir);
382
-
383
- const handlerPath = join(outputDir, 'subscribers', 'validSubscriber.ts');
384
- const handlerContent = await readFile(handlerPath, 'utf-8');
385
-
386
- // Check that the generated file has proper structure
387
- expect(handlerContent).toContain(
388
- "import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws'",
389
- );
390
- expect(handlerContent).toContain(
391
- 'export const handler = adapter.handler',
392
- );
393
- });
394
- });
18
+ let tempDir: string;
19
+ let outputDir: string;
20
+ let generator: SubscriberGenerator;
21
+ let context: ReturnType<typeof createMockBuildContext>;
22
+
23
+ beforeEach(async () => {
24
+ tempDir = await createTempDir();
25
+ outputDir = join(tempDir, 'output');
26
+ generator = new SubscriberGenerator();
27
+ context = createMockBuildContext();
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await cleanupDir(tempDir);
32
+ });
33
+
34
+ describe('isConstruct', () => {
35
+ it('should identify valid subscribers', async () => {
36
+ const testSubscriber = new SubscriberBuilder()
37
+ .subscribe(['user.created'] as any)
38
+ .handle(async ({ events, logger }) => {
39
+ logger.info({ eventCount: events.length }, 'Processing events');
40
+ });
41
+
42
+ expect(generator.isConstruct(testSubscriber)).toBe(true);
43
+ });
44
+
45
+ it('should reject invalid constructs', () => {
46
+ expect(generator.isConstruct({})).toBe(false);
47
+ expect(generator.isConstruct('string')).toBe(false);
48
+ expect(generator.isConstruct(null)).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe('build', () => {
53
+ const createSubscriberConstruct = (
54
+ key: string,
55
+ subscribedEvents: string[],
56
+ timeout: number = 30000,
57
+ ): GeneratedConstruct<Subscriber<any, any, any, any, any, any>> => {
58
+ const subscriber = new SubscriberBuilder()
59
+ .subscribe(subscribedEvents as any)
60
+ .timeout(timeout)
61
+ .handle(async ({ events, logger }) => {
62
+ logger.info({ eventCount: events.length }, 'Processing events');
63
+ });
64
+
65
+ return {
66
+ key,
67
+ name: key.toLowerCase(),
68
+ construct: subscriber,
69
+ path: {
70
+ absolute: join(tempDir, `${key}.ts`),
71
+ relative: `${key}.ts`,
72
+ },
73
+ };
74
+ };
75
+
76
+ it('should generate subscriber handlers', async () => {
77
+ const constructs = [
78
+ createSubscriberConstruct('userEventSubscriber', [
79
+ 'user.created',
80
+ 'user.updated',
81
+ ]),
82
+ createSubscriberConstruct('orderEventSubscriber', ['order.placed']),
83
+ ];
84
+
85
+ const subscriberInfos = await generator.build(
86
+ context,
87
+ constructs,
88
+ outputDir,
89
+ { provider: 'aws-lambda' },
90
+ );
91
+
92
+ expect(subscriberInfos).toHaveLength(2);
93
+ expect(subscriberInfos[0]).toMatchObject({
94
+ name: 'userEventSubscriber',
95
+ handler: expect.stringContaining(
96
+ 'subscribers/userEventSubscriber.handler',
97
+ ),
98
+ subscribedEvents: ['user.created', 'user.updated'],
99
+ timeout: 30000,
100
+ });
101
+ expect(subscriberInfos[1]).toMatchObject({
102
+ name: 'orderEventSubscriber',
103
+ handler: expect.stringContaining(
104
+ 'subscribers/orderEventSubscriber.handler',
105
+ ),
106
+ subscribedEvents: ['order.placed'],
107
+ timeout: 30000,
108
+ });
109
+
110
+ // Check that handler files were created
111
+ const userHandlerPath = join(
112
+ outputDir,
113
+ 'subscribers',
114
+ 'userEventSubscriber.ts',
115
+ );
116
+ const userContent = await readFile(userHandlerPath, 'utf-8');
117
+ expect(userContent).toContain('AWSLambdaSubscriber');
118
+ expect(userContent).toContain('import { userEventSubscriber }');
119
+ expect(userContent).toContain('import envParser');
120
+
121
+ const orderHandlerPath = join(
122
+ outputDir,
123
+ 'subscribers',
124
+ 'orderEventSubscriber.ts',
125
+ );
126
+ const orderContent = await readFile(orderHandlerPath, 'utf-8');
127
+ expect(orderContent).toContain('AWSLambdaSubscriber');
128
+ expect(orderContent).toContain('import { orderEventSubscriber }');
129
+ });
130
+
131
+ it('should generate correct relative import paths', async () => {
132
+ const subscriber = new SubscriberBuilder()
133
+ .subscribe(['event.type'] as any)
134
+ .timeout(45000)
135
+ .handle(async ({ events, logger }) => {
136
+ logger.info({ eventCount: events.length }, 'Processing events');
137
+ });
138
+
139
+ const construct: GeneratedConstruct<
140
+ Subscriber<any, any, any, any, any, any>
141
+ > = {
142
+ key: 'deepSubscriber',
143
+ name: 'deep-subscriber',
144
+ construct: subscriber,
145
+ path: {
146
+ absolute: join(tempDir, 'src/subscribers/deep/processor.ts'),
147
+ relative: 'src/subscribers/deep/processor.ts',
148
+ },
149
+ };
150
+
151
+ await generator.build(context, [construct], outputDir);
152
+
153
+ const handlerPath = join(outputDir, 'subscribers', 'deepSubscriber.ts');
154
+ const handlerContent = await readFile(handlerPath, 'utf-8');
155
+
156
+ // Check relative imports are correct
157
+ expect(handlerContent).toMatch(
158
+ /from ['"].*src\/subscribers\/deep\/processor\.js['"]/,
159
+ );
160
+ expect(handlerContent).toMatch(/from ['"].*\/env['"]/);
161
+ });
162
+
163
+ it('should handle subscribers with different timeout values', async () => {
164
+ const constructs = [
165
+ createSubscriberConstruct('quickSubscriber', ['fast.event'], 15000),
166
+ createSubscriberConstruct('slowSubscriber', ['slow.event'], 300000),
167
+ ];
168
+
169
+ const subscriberInfos = await generator.build(
170
+ context,
171
+ constructs,
172
+ outputDir,
173
+ );
174
+
175
+ expect(subscriberInfos[0].timeout).toBe(15000);
176
+ expect(subscriberInfos[1].timeout).toBe(300000);
177
+ });
178
+
179
+ it('should handle subscribers with no subscribed events', async () => {
180
+ const subscriber = new SubscriberBuilder()
181
+ .timeout(30000)
182
+ .handle(async ({ events, logger }) => {
183
+ logger.info({ eventCount: events.length }, 'Processing all events');
184
+ });
185
+
186
+ const construct: GeneratedConstruct<
187
+ Subscriber<any, any, any, any, any, any>
188
+ > = {
189
+ key: 'catchAllSubscriber',
190
+ name: 'catch-all-subscriber',
191
+ construct: subscriber,
192
+ path: {
193
+ absolute: join(tempDir, 'catchAllSubscriber.ts'),
194
+ relative: 'catchAllSubscriber.ts',
195
+ },
196
+ };
197
+
198
+ const subscriberInfos = await generator.build(
199
+ context,
200
+ [construct],
201
+ outputDir,
202
+ );
203
+
204
+ expect(subscriberInfos[0].subscribedEvents).toEqual([]);
205
+ });
206
+
207
+ it('should handle subscribers with multiple event types', async () => {
208
+ const constructs = [
209
+ createSubscriberConstruct('multiEventSubscriber', [
210
+ 'user.created',
211
+ 'user.updated',
212
+ 'user.deleted',
213
+ 'order.placed',
214
+ ]),
215
+ ];
216
+
217
+ const subscriberInfos = await generator.build(
218
+ context,
219
+ constructs,
220
+ outputDir,
221
+ );
222
+
223
+ expect(subscriberInfos[0].subscribedEvents).toEqual([
224
+ 'user.created',
225
+ 'user.updated',
226
+ 'user.deleted',
227
+ 'order.placed',
228
+ ]);
229
+ });
230
+
231
+ it('should log generation progress', async () => {
232
+ const logSpy = vi.spyOn(console, 'log');
233
+
234
+ const constructs = [
235
+ createSubscriberConstruct('testSubscriber', ['test.event']),
236
+ ];
237
+
238
+ await generator.build(context, constructs, outputDir);
239
+
240
+ expect(logSpy).toHaveBeenCalledWith(
241
+ 'Generated subscriber handler: testSubscriber',
242
+ );
243
+
244
+ logSpy.mockRestore();
245
+ });
246
+
247
+ it('should return empty array for empty constructs', async () => {
248
+ const subscriberInfos = await generator.build(context, [], outputDir);
249
+ expect(subscriberInfos).toEqual([]);
250
+ });
251
+
252
+ it('should generate subscribers.ts file for server provider even with no subscribers', async () => {
253
+ const subscriberInfos = await generator.build(context, [], outputDir, {
254
+ provider: 'server',
255
+ });
256
+
257
+ expect(subscriberInfos).toEqual([]);
258
+
259
+ // Check that subscribers.ts was generated
260
+ const subscribersPath = join(outputDir, 'subscribers.ts');
261
+ const subscribersContent = await readFile(subscribersPath, 'utf-8');
262
+
263
+ expect(subscribersContent).toContain(
264
+ 'export async function setupSubscribers',
265
+ );
266
+ expect(subscribersContent).toContain('const subscribers = [');
267
+ expect(subscribersContent).toContain('import type { EnvironmentParser }');
268
+ expect(subscribersContent).toContain('import type { Logger }');
269
+ });
270
+
271
+ it('should generate subscribers.ts file for server provider with subscribers', async () => {
272
+ const constructs = [
273
+ createSubscriberConstruct('userEventSubscriber', [
274
+ 'user.created',
275
+ 'user.updated',
276
+ ]),
277
+ createSubscriberConstruct('orderEventSubscriber', ['order.placed']),
278
+ ];
279
+
280
+ const subscriberInfos = await generator.build(
281
+ context,
282
+ constructs,
283
+ outputDir,
284
+ { provider: 'server' },
285
+ );
286
+
287
+ expect(subscriberInfos).toEqual([]);
288
+
289
+ // Check that subscribers.ts was generated
290
+ const subscribersPath = join(outputDir, 'subscribers.ts');
291
+ const subscribersContent = await readFile(subscribersPath, 'utf-8');
292
+
293
+ expect(subscribersContent).toContain(
294
+ 'export async function setupSubscribers',
295
+ );
296
+ expect(subscribersContent).toContain('import { userEventSubscriber }');
297
+ expect(subscribersContent).toContain('import { orderEventSubscriber }');
298
+ expect(subscribersContent).toContain('const subscribers = [');
299
+ expect(subscribersContent).toContain('userEventSubscriber');
300
+ expect(subscribersContent).toContain('orderEventSubscriber');
301
+ expect(subscribersContent).toContain(
302
+ 'Setting up subscribers in polling mode',
303
+ );
304
+ });
305
+
306
+ it('should handle subscribers with custom environment parser patterns', async () => {
307
+ const customContext = {
308
+ ...context,
309
+ envParserImportPattern: '{ customParser as envParser }',
310
+ };
311
+
312
+ const constructs = [
313
+ createSubscriberConstruct('customSubscriber', ['custom.event']),
314
+ ];
315
+
316
+ await generator.build(customContext, constructs, outputDir);
317
+
318
+ const handlerPath = join(outputDir, 'subscribers', 'customSubscriber.ts');
319
+ const handlerContent = await readFile(handlerPath, 'utf-8');
320
+
321
+ expect(handlerContent).toContain('import { customParser as envParser }');
322
+ });
323
+
324
+ it('should create subscribers directory if it does not exist', async () => {
325
+ const constructs = [
326
+ createSubscriberConstruct('firstSubscriber', ['first.event']),
327
+ ];
328
+
329
+ // outputDir does not exist yet
330
+ await generator.build(context, constructs, outputDir);
331
+
332
+ const subscribersDir = join(outputDir, 'subscribers');
333
+ const handlerPath = join(subscribersDir, 'firstSubscriber.ts');
334
+
335
+ // Should be able to read the file, meaning the directory was created
336
+ const content = await readFile(handlerPath, 'utf-8');
337
+ expect(content).toContain('AWSLambdaSubscriber');
338
+ });
339
+
340
+ it('should handle exported subscriber with custom name', async () => {
341
+ const subscriber = new SubscriberBuilder()
342
+ .subscribe(['custom.event'] as any)
343
+ .handle(async ({ events, logger }) => {
344
+ logger.info({ eventCount: events.length }, 'Processing events');
345
+ });
346
+
347
+ const construct: GeneratedConstruct<
348
+ Subscriber<any, any, any, any, any, any>
349
+ > = {
350
+ key: 'myCustomSubscriberName',
351
+ name: 'custom-name',
352
+ construct: subscriber,
353
+ path: {
354
+ absolute: join(tempDir, 'subscriber.ts'),
355
+ relative: 'subscriber.ts',
356
+ },
357
+ };
358
+
359
+ await generator.build(context, [construct], outputDir);
360
+
361
+ const handlerPath = join(
362
+ outputDir,
363
+ 'subscribers',
364
+ 'myCustomSubscriberName.ts',
365
+ );
366
+ const handlerContent = await readFile(handlerPath, 'utf-8');
367
+
368
+ expect(handlerContent).toContain(
369
+ 'import { myCustomSubscriberName } from',
370
+ );
371
+ expect(handlerContent).toContain(
372
+ 'new AWSLambdaSubscriber(envParser, myCustomSubscriberName)',
373
+ );
374
+ });
375
+
376
+ it('should return empty array for unsupported provider', async () => {
377
+ const constructs = [
378
+ createSubscriberConstruct('testSubscriber', ['test.event']),
379
+ ];
380
+
381
+ const subscriberInfos = await generator.build(
382
+ context,
383
+ constructs,
384
+ outputDir,
385
+ { provider: 'unsupported' as any },
386
+ );
387
+
388
+ expect(subscriberInfos).toEqual([]);
389
+ });
390
+
391
+ it('should generate handler files that can be imported', async () => {
392
+ const constructs = [
393
+ createSubscriberConstruct('validSubscriber', ['valid.event']),
394
+ ];
395
+
396
+ await generator.build(context, constructs, outputDir);
397
+
398
+ const handlerPath = join(outputDir, 'subscribers', 'validSubscriber.ts');
399
+ const handlerContent = await readFile(handlerPath, 'utf-8');
400
+
401
+ // Check that the generated file has proper structure
402
+ expect(handlerContent).toContain(
403
+ "import { AWSLambdaSubscriber } from '@geekmidas/constructs/aws'",
404
+ );
405
+ expect(handlerContent).toContain(
406
+ 'export const handler = adapter.handler',
407
+ );
408
+ });
409
+ });
395
410
  });