@auto-engineer/server-generator-apollo-emmett 0.10.4 → 0.11.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 (104) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-format.log +5 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +22 -0
  5. package/.turbo/turbo-type-check.log +5 -0
  6. package/CHANGELOG.md +20 -0
  7. package/dist/src/codegen/extract/events.d.ts +2 -2
  8. package/dist/src/codegen/extract/events.d.ts.map +1 -1
  9. package/dist/src/codegen/extract/events.js +16 -6
  10. package/dist/src/codegen/extract/events.js.map +1 -1
  11. package/dist/src/codegen/extract/gwt.js +7 -22
  12. package/dist/src/codegen/extract/gwt.js.map +1 -1
  13. package/dist/src/codegen/extract/imports.d.ts +29 -0
  14. package/dist/src/codegen/extract/imports.d.ts.map +1 -0
  15. package/dist/src/codegen/extract/imports.js +55 -0
  16. package/dist/src/codegen/extract/imports.js.map +1 -0
  17. package/dist/src/codegen/extract/index.d.ts +1 -0
  18. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  19. package/dist/src/codegen/extract/index.js +1 -0
  20. package/dist/src/codegen/extract/index.js.map +1 -1
  21. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  22. package/dist/src/codegen/extract/messages.js +33 -7
  23. package/dist/src/codegen/extract/messages.js.map +1 -1
  24. package/dist/src/codegen/extract/query.d.ts +3 -1
  25. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  26. package/dist/src/codegen/extract/query.js +12 -12
  27. package/dist/src/codegen/extract/query.js.map +1 -1
  28. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  29. package/dist/src/codegen/scaffoldFromSchema.js +9 -1
  30. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  31. package/dist/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  32. package/dist/src/codegen/templates/command/decide.specs.ts +8 -8
  33. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  34. package/dist/src/codegen/templates/command/decide.ts.ejs +2 -2
  35. package/dist/src/codegen/templates/command/events.ts.ejs +2 -2
  36. package/dist/src/codegen/templates/command/evolve.ts.ejs +3 -3
  37. package/dist/src/codegen/templates/command/handle.specs.ts +6 -6
  38. package/dist/src/codegen/templates/command/handle.ts.ejs +3 -3
  39. package/dist/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  40. package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
  41. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  42. package/dist/src/codegen/templates/query/projection.ts.ejs +30 -29
  43. package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  44. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  45. package/dist/src/codegen/templates/react/react.specs.specs.ts +8 -5
  46. package/dist/src/codegen/templates/react/react.specs.ts +4 -4
  47. package/dist/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  48. package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
  49. package/dist/src/codegen/templates/react/register.specs.ts +2 -2
  50. package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
  51. package/dist/src/codegen/types.d.ts +2 -0
  52. package/dist/src/codegen/types.d.ts.map +1 -1
  53. package/dist/src/commands/generate-server.d.ts.map +1 -1
  54. package/dist/src/commands/generate-server.js +3 -0
  55. package/dist/src/commands/generate-server.js.map +1 -1
  56. package/dist/src/domain/shared/ReadModel.d.ts +2 -2
  57. package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
  58. package/dist/src/domain/shared/ReadModel.js +2 -2
  59. package/dist/src/domain/shared/ReadModel.js.map +1 -1
  60. package/dist/src/domain/shared/ReadModel.ts +3 -3
  61. package/dist/src/domain/shared/types.d.ts +5 -3
  62. package/dist/src/domain/shared/types.d.ts.map +1 -1
  63. package/dist/src/domain/shared/types.js.map +1 -1
  64. package/dist/src/domain/shared/types.ts +5 -3
  65. package/dist/src/server.js +54 -7
  66. package/dist/src/server.js.map +1 -1
  67. package/dist/src/server.ts +53 -15
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +8 -5
  70. package/src/codegen/extract/events.ts +20 -3
  71. package/src/codegen/extract/gwt.ts +10 -26
  72. package/src/codegen/extract/imports.ts +71 -0
  73. package/src/codegen/extract/index.ts +1 -0
  74. package/src/codegen/extract/messages.ts +34 -7
  75. package/src/codegen/extract/query.ts +17 -19
  76. package/src/codegen/scaffoldFromSchema.ts +13 -0
  77. package/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  78. package/src/codegen/templates/command/decide.specs.ts +8 -8
  79. package/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  80. package/src/codegen/templates/command/decide.ts.ejs +2 -2
  81. package/src/codegen/templates/command/events.ts.ejs +2 -2
  82. package/src/codegen/templates/command/evolve.ts.ejs +3 -3
  83. package/src/codegen/templates/command/handle.specs.ts +6 -6
  84. package/src/codegen/templates/command/handle.ts.ejs +3 -3
  85. package/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  86. package/src/codegen/templates/query/projection.specs.ts +1 -1
  87. package/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  88. package/src/codegen/templates/query/projection.ts.ejs +30 -29
  89. package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  90. package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  91. package/src/codegen/templates/react/react.specs.specs.ts +8 -5
  92. package/src/codegen/templates/react/react.specs.ts +4 -4
  93. package/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  94. package/src/codegen/templates/react/react.ts.ejs +4 -4
  95. package/src/codegen/templates/react/register.specs.ts +2 -2
  96. package/src/codegen/templates/react/register.ts.ejs +2 -2
  97. package/src/codegen/types.ts +2 -0
  98. package/src/commands/generate-server.ts +3 -0
  99. package/src/domain/shared/ReadModel.ts +3 -3
  100. package/src/domain/shared/types.ts +5 -3
  101. package/src/server.ts +53 -15
  102. package/dist/src/codegen/templates/query/projection.specs.specs..ts +0 -296
  103. package/src/codegen/scaffoldFromSchema.query-slice-register.specs.ts +0 -179
  104. package/src/codegen/templates/query/projection.specs.specs..ts +0 -296
@@ -0,0 +1,623 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
3
+ import { Model as SpecsSchema } from '@auto-engineer/flow';
4
+
5
+ describe('projection.specs.ts.ejs', () => {
6
+ it('should generate a valid test spec for a query slice projection', async () => {
7
+ const spec: SpecsSchema = {
8
+ variant: 'specs',
9
+ flows: [
10
+ {
11
+ name: 'listing-flow',
12
+ slices: [
13
+ {
14
+ type: 'command',
15
+ name: 'CreateListing',
16
+ stream: 'listing-${propertyId}',
17
+ client: { description: '' },
18
+ server: {
19
+ description: '',
20
+ specs: {
21
+ name: 'CreateListing command',
22
+ rules: [
23
+ {
24
+ description: 'Should handle listing operations',
25
+ examples: [
26
+ {
27
+ description: 'User creates listing successfully',
28
+ when: {
29
+ commandRef: 'CreateListing',
30
+ exampleData: {
31
+ propertyId: 'listing_123',
32
+ title: 'Sea View Flat',
33
+ pricePerNight: 120,
34
+ location: 'Brighton',
35
+ maxGuests: 4,
36
+ },
37
+ },
38
+ then: [
39
+ {
40
+ eventRef: 'ListingCreated',
41
+ exampleData: {
42
+ propertyId: 'listing_123',
43
+ title: 'Sea View Flat',
44
+ pricePerNight: 120,
45
+ location: 'Brighton',
46
+ maxGuests: 4,
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ description: 'User removes listing successfully',
53
+ when: {
54
+ commandRef: 'RemoveListing',
55
+ exampleData: {
56
+ propertyId: 'listing_123',
57
+ },
58
+ },
59
+ then: [
60
+ {
61
+ eventRef: 'ListingRemoved',
62
+ exampleData: {},
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ },
70
+ },
71
+ },
72
+ {
73
+ type: 'query',
74
+ name: 'search-listings',
75
+ stream: 'listings',
76
+ client: { description: '' },
77
+ server: {
78
+ description: '',
79
+ data: [
80
+ {
81
+ origin: {
82
+ type: 'projection',
83
+ idField: 'propertyId',
84
+ name: 'AvailablePropertiesProjection',
85
+ },
86
+ target: {
87
+ type: 'State',
88
+ name: 'AvailableListings',
89
+ },
90
+ },
91
+ ],
92
+ specs: {
93
+ name: 'Search listings query',
94
+ rules: [
95
+ {
96
+ description: 'Should project listings correctly',
97
+ examples: [
98
+ {
99
+ description: 'Listing created shows in search results',
100
+ when: [
101
+ {
102
+ eventRef: 'ListingCreated',
103
+ exampleData: {
104
+ propertyId: 'listing_123',
105
+ title: 'Sea View Flat',
106
+ pricePerNight: 120,
107
+ location: 'Brighton',
108
+ maxGuests: 4,
109
+ },
110
+ },
111
+ ],
112
+ then: [
113
+ {
114
+ stateRef: 'AvailableListings',
115
+ exampleData: {
116
+ propertyId: 'listing_123',
117
+ title: 'Sea View Flat',
118
+ pricePerNight: 120,
119
+ location: 'Brighton',
120
+ maxGuests: 4,
121
+ },
122
+ },
123
+ ],
124
+ },
125
+ ],
126
+ },
127
+ ],
128
+ },
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ ],
134
+ messages: [
135
+ {
136
+ type: 'command',
137
+ name: 'CreateListing',
138
+ fields: [
139
+ { name: 'propertyId', type: 'string', required: true },
140
+ { name: 'title', type: 'string', required: true },
141
+ { name: 'pricePerNight', type: 'number', required: true },
142
+ { name: 'location', type: 'string', required: true },
143
+ { name: 'maxGuests', type: 'number', required: true },
144
+ ],
145
+ },
146
+ {
147
+ type: 'command',
148
+ name: 'RemoveListing',
149
+ fields: [{ name: 'propertyId', type: 'string', required: true }],
150
+ },
151
+ {
152
+ type: 'event',
153
+ name: 'ListingCreated',
154
+ source: 'internal',
155
+ fields: [
156
+ { name: 'propertyId', type: 'string', required: true },
157
+ { name: 'title', type: 'string', required: true },
158
+ { name: 'pricePerNight', type: 'number', required: true },
159
+ { name: 'location', type: 'string', required: true },
160
+ { name: 'maxGuests', type: 'number', required: true },
161
+ ],
162
+ },
163
+ {
164
+ type: 'event',
165
+ name: 'ListingRemoved',
166
+ source: 'internal',
167
+ fields: [{ name: 'propertyId', type: 'string', required: true }],
168
+ },
169
+ {
170
+ type: 'state',
171
+ name: 'AvailableListings',
172
+ fields: [
173
+ { name: 'propertyId', type: 'string', required: true },
174
+ { name: 'title', type: 'string', required: true },
175
+ { name: 'pricePerNight', type: 'number', required: true },
176
+ { name: 'location', type: 'string', required: true },
177
+ { name: 'maxGuests', type: 'number', required: true },
178
+ ],
179
+ },
180
+ ],
181
+ } as SpecsSchema;
182
+
183
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
184
+ const specFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
185
+
186
+ expect(specFile?.contents).toMatchInlineSnapshot(`
187
+ "import { describe, it, beforeEach, expect } from 'vitest';
188
+ import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
189
+ import { projection } from './projection';
190
+ import type { ListingCreated } from '../create-listing/events';
191
+ import { AvailableListings } from './state';
192
+
193
+ type ProjectionEvent = ListingCreated;
194
+
195
+ describe('Should project listings correctly', () => {
196
+ let given: InMemoryProjectionSpec<ProjectionEvent>;
197
+
198
+ beforeEach(() => {
199
+ given = InMemoryProjectionSpec.for({ projection });
200
+ });
201
+
202
+ it('Listing created shows in search results', () =>
203
+ given([])
204
+ .when([
205
+ {
206
+ type: 'ListingCreated',
207
+ data: {
208
+ propertyId: 'listing_123',
209
+ title: 'Sea View Flat',
210
+ pricePerNight: 120,
211
+ location: 'Brighton',
212
+ maxGuests: 4,
213
+ },
214
+ metadata: {
215
+ streamName: 'listings',
216
+ streamPosition: 1n,
217
+ globalPosition: 1n,
218
+ },
219
+ },
220
+ ])
221
+ .then(async (state) => {
222
+ const document = await state.database
223
+ .collection<AvailableListings>('AvailablePropertiesProjection')
224
+ .findOne((doc) => doc.propertyId === 'listing_123');
225
+
226
+ const expected: AvailableListings = {
227
+ propertyId: 'listing_123',
228
+ title: 'Sea View Flat',
229
+ pricePerNight: 120,
230
+ location: 'Brighton',
231
+ maxGuests: 4,
232
+ };
233
+
234
+ expect(document).toMatchObject(expected);
235
+ }));
236
+ });
237
+ "
238
+ `);
239
+ });
240
+
241
+ it('should generate a valid test spec for a model with given/when/then pattern', async () => {
242
+ const questionnaireSpec: SpecsSchema = {
243
+ variant: 'specs',
244
+ flows: [
245
+ {
246
+ name: 'Questionnaires',
247
+ slices: [
248
+ {
249
+ name: 'views the questionnaire',
250
+ type: 'query',
251
+ client: { description: '' },
252
+ server: {
253
+ description: '',
254
+ data: [
255
+ {
256
+ target: {
257
+ type: 'State',
258
+ name: 'QuestionnaireProgress',
259
+ },
260
+ origin: {
261
+ type: 'projection',
262
+ name: 'Questionnaires',
263
+ idField: 'questionnaire-participantId',
264
+ },
265
+ },
266
+ ],
267
+ specs: {
268
+ name: '',
269
+ rules: [
270
+ {
271
+ description: 'questionnaires show current progress',
272
+ examples: [
273
+ {
274
+ description: 'a question has already been answered',
275
+ given: [
276
+ {
277
+ eventRef: 'QuestionnaireLinkSent',
278
+ exampleData: {
279
+ questionnaireId: 'q-001',
280
+ participantId: 'participant-abc',
281
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
282
+ sentAt: '2030-01-01T09:00:00.000Z',
283
+ },
284
+ },
285
+ ],
286
+ when: [
287
+ {
288
+ eventRef: 'QuestionAnswered',
289
+ exampleData: {
290
+ questionnaireId: 'q-001',
291
+ participantId: 'participant-abc',
292
+ questionId: 'q1',
293
+ answer: 'Yes',
294
+ savedAt: '2030-01-01T09:05:00.000Z',
295
+ },
296
+ },
297
+ ],
298
+ then: [
299
+ {
300
+ stateRef: 'QuestionnaireProgress',
301
+ exampleData: {
302
+ questionnaireId: 'q-001',
303
+ participantId: 'participant-abc',
304
+ status: 'in_progress',
305
+ currentQuestionId: 'q2',
306
+ remainingQuestions: ['q2', 'q3'],
307
+ answers: [
308
+ {
309
+ questionId: 'q1',
310
+ value: 'Yes',
311
+ },
312
+ ],
313
+ },
314
+ },
315
+ ],
316
+ },
317
+ ],
318
+ },
319
+ ],
320
+ },
321
+ },
322
+ },
323
+ ],
324
+ },
325
+ ],
326
+ messages: [
327
+ {
328
+ type: 'event',
329
+ name: 'QuestionnaireLinkSent',
330
+ fields: [
331
+ { name: 'questionnaireId', type: 'string', required: true },
332
+ { name: 'participantId', type: 'string', required: true },
333
+ { name: 'link', type: 'string', required: true },
334
+ { name: 'sentAt', type: 'Date', required: true },
335
+ ],
336
+ source: 'internal',
337
+ },
338
+ {
339
+ type: 'event',
340
+ name: 'QuestionAnswered',
341
+ fields: [
342
+ { name: 'questionnaireId', type: 'string', required: true },
343
+ { name: 'participantId', type: 'string', required: true },
344
+ { name: 'questionId', type: 'string', required: true },
345
+ { name: 'answer', type: 'unknown', required: true },
346
+ { name: 'savedAt', type: 'Date', required: true },
347
+ ],
348
+ source: 'internal',
349
+ },
350
+ {
351
+ type: 'state',
352
+ name: 'QuestionnaireProgress',
353
+ fields: [
354
+ { name: 'questionnaireId', type: 'string', required: true },
355
+ { name: 'participantId', type: 'string', required: true },
356
+ { name: 'status', type: '"in_progress" | "ready_to_submit" | "submitted"', required: true },
357
+ { name: 'currentQuestionId', type: 'string | null', required: true },
358
+ { name: 'remainingQuestions', type: 'Array<string>', required: true },
359
+ { name: 'answers', type: 'Array<{ questionId: string; value: unknown }>', required: true },
360
+ ],
361
+ },
362
+ ],
363
+ } as SpecsSchema;
364
+
365
+ const plans = await generateScaffoldFilePlans(
366
+ questionnaireSpec.flows,
367
+ questionnaireSpec.messages,
368
+ undefined,
369
+ 'src/domain/flows',
370
+ );
371
+ const specFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
372
+
373
+ expect(specFile?.contents).toContain('a question has already been answered');
374
+ expect(specFile?.contents).toContain('QuestionnaireLinkSent');
375
+ expect(specFile?.contents).toContain('QuestionAnswered');
376
+ expect(specFile?.contents).toContain('given([');
377
+ expect(specFile?.contents).toContain('.when([');
378
+ });
379
+
380
+ it('should include all events from both given and when clauses in projection imports and types', async () => {
381
+ const spec: SpecsSchema = {
382
+ variant: 'specs',
383
+ flows: [
384
+ {
385
+ name: 'questionnaires',
386
+ slices: [
387
+ {
388
+ type: 'command',
389
+ name: 'sends-the-questionnaire-link',
390
+ client: { description: '' },
391
+ server: {
392
+ description: '',
393
+ specs: {
394
+ name: 'Sends questionnaire link',
395
+ rules: [
396
+ {
397
+ description: 'sends questionnaire link to participant',
398
+ examples: [
399
+ {
400
+ description: 'sends link successfully',
401
+ when: {
402
+ commandRef: 'SendQuestionnaireLink',
403
+ exampleData: {
404
+ questionnaireId: 'q-001',
405
+ participantId: 'participant-abc',
406
+ },
407
+ },
408
+ then: [
409
+ {
410
+ eventRef: 'QuestionnaireLinkSent', // This event is produced here
411
+ exampleData: {
412
+ questionnaireId: 'q-001',
413
+ participantId: 'participant-abc',
414
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
415
+ sentAt: new Date('2030-01-01T09:00:00Z'),
416
+ },
417
+ },
418
+ ],
419
+ },
420
+ ],
421
+ },
422
+ ],
423
+ },
424
+ },
425
+ },
426
+ {
427
+ type: 'command',
428
+ name: 'submits-a-questionnaire-answer',
429
+ client: { description: '' },
430
+ server: {
431
+ description: '',
432
+ specs: {
433
+ name: 'Submits questionnaire answer',
434
+ rules: [
435
+ {
436
+ description: 'submits answer successfully',
437
+ examples: [
438
+ {
439
+ description: 'answers question',
440
+ when: {
441
+ commandRef: 'AnswerQuestion',
442
+ exampleData: {
443
+ questionnaireId: 'q-001',
444
+ participantId: 'participant-abc',
445
+ questionId: 'q1',
446
+ answer: 'Yes',
447
+ },
448
+ },
449
+ then: [
450
+ {
451
+ eventRef: 'QuestionAnswered', // This event is produced here
452
+ exampleData: {
453
+ questionnaireId: 'q-001',
454
+ participantId: 'participant-abc',
455
+ questionId: 'q1',
456
+ answer: 'Yes',
457
+ savedAt: new Date('2030-01-01T09:05:00Z'),
458
+ },
459
+ },
460
+ ],
461
+ },
462
+ ],
463
+ },
464
+ ],
465
+ },
466
+ },
467
+ },
468
+ {
469
+ type: 'query',
470
+ name: 'views-the-questionnaire',
471
+ client: { description: '' },
472
+ server: {
473
+ description: '',
474
+ specs: {
475
+ name: 'Views the questionnaire',
476
+ rules: [
477
+ {
478
+ description: 'questionnaires show current progress',
479
+ examples: [
480
+ {
481
+ description: 'a question has already been answered',
482
+ given: [
483
+ {
484
+ eventRef: 'QuestionnaireLinkSent',
485
+ exampleData: {
486
+ questionnaireId: 'q-001',
487
+ participantId: 'participant-abc',
488
+ link: 'https://app.example.com/q/q-001?participant=participant-abc',
489
+ sentAt: new Date('2030-01-01T09:00:00Z'),
490
+ },
491
+ },
492
+ ],
493
+ when: [
494
+ {
495
+ eventRef: 'QuestionAnswered', // This should be included in imports!
496
+ exampleData: {
497
+ questionnaireId: 'q-001',
498
+ participantId: 'participant-abc',
499
+ questionId: 'q1',
500
+ answer: 'Yes',
501
+ savedAt: new Date('2030-01-01T09:05:00Z'),
502
+ },
503
+ },
504
+ ],
505
+ then: [
506
+ {
507
+ stateRef: 'QuestionnaireProgress',
508
+ exampleData: {
509
+ questionnaireId: 'q-001',
510
+ participantId: 'participant-abc',
511
+ status: 'in_progress',
512
+ currentQuestionId: 'q2',
513
+ remainingQuestions: ['q2'],
514
+ answers: [{ questionId: 'q1', value: 'Yes' }],
515
+ },
516
+ },
517
+ ],
518
+ },
519
+ ],
520
+ },
521
+ ],
522
+ },
523
+ data: [
524
+ {
525
+ origin: { name: 'Questionnaires', idField: 'questionnaireId-participantId' },
526
+ target: { name: 'QuestionnaireProgress' },
527
+ },
528
+ ],
529
+ },
530
+ },
531
+ ],
532
+ },
533
+ ],
534
+ messages: [
535
+ {
536
+ type: 'event',
537
+ name: 'QuestionnaireLinkSent',
538
+ fields: [
539
+ { name: 'questionnaireId', type: 'string', required: true },
540
+ { name: 'participantId', type: 'string', required: true },
541
+ { name: 'link', type: 'string', required: true },
542
+ { name: 'sentAt', type: 'Date', required: true },
543
+ ],
544
+ },
545
+ {
546
+ type: 'event',
547
+ name: 'QuestionAnswered',
548
+ fields: [
549
+ { name: 'questionnaireId', type: 'string', required: true },
550
+ { name: 'participantId', type: 'string', required: true },
551
+ { name: 'questionId', type: 'string', required: true },
552
+ { name: 'answer', type: 'unknown', required: true },
553
+ { name: 'savedAt', type: 'Date', required: true },
554
+ ],
555
+ },
556
+ {
557
+ type: 'state',
558
+ name: 'QuestionnaireProgress',
559
+ fields: [
560
+ { name: 'questionnaireId', type: 'string', required: true },
561
+ { name: 'participantId', type: 'string', required: true },
562
+ { name: 'status', type: '"in_progress" | "ready_to_submit" | "submitted"', required: true },
563
+ { name: 'currentQuestionId', type: 'string | null', required: true },
564
+ { name: 'remainingQuestions', type: 'Array<string>', required: true },
565
+ { name: 'answers', type: 'Array<{ questionId: string; value: unknown }>', required: true },
566
+ ],
567
+ },
568
+ ],
569
+ } as SpecsSchema;
570
+
571
+ const plans = await generateScaffoldFilePlans(
572
+ spec.flows,
573
+ [
574
+ {
575
+ type: 'command',
576
+ name: 'SendQuestionnaireLink',
577
+ fields: [
578
+ { name: 'questionnaireId', type: 'string', required: true },
579
+ { name: 'participantId', type: 'string', required: true },
580
+ ],
581
+ },
582
+ {
583
+ type: 'command',
584
+ name: 'AnswerQuestion',
585
+ fields: [
586
+ { name: 'questionnaireId', type: 'string', required: true },
587
+ { name: 'participantId', type: 'string', required: true },
588
+ { name: 'questionId', type: 'string', required: true },
589
+ { name: 'answer', type: 'unknown', required: true },
590
+ ],
591
+ },
592
+ ...spec.messages,
593
+ ],
594
+ undefined,
595
+ 'src/domain/flows',
596
+ );
597
+
598
+ // Check projection.specs.ts file
599
+ const specsFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
600
+ expect(specsFile?.contents).toBeDefined();
601
+
602
+ // Must import BOTH event types
603
+ expect(specsFile?.contents).toContain('import type { QuestionnaireLinkSent }');
604
+ expect(specsFile?.contents).toContain('import type { QuestionAnswered }');
605
+
606
+ // Union type must include BOTH events (order may vary due to sorting)
607
+ expect(specsFile?.contents).toContain('type ProjectionEvent = QuestionAnswered | QuestionnaireLinkSent');
608
+
609
+ // Check projection.ts file
610
+ const projectionFile = plans.find((p) => p.outputPath.endsWith('projection.ts'));
611
+ expect(projectionFile?.contents).toBeDefined();
612
+
613
+ // Must import BOTH event types
614
+ expect(projectionFile?.contents).toContain('import type { QuestionnaireLinkSent }');
615
+ expect(projectionFile?.contents).toContain('import type { QuestionAnswered }');
616
+
617
+ // AllEvents type must include BOTH events (order may vary due to sorting)
618
+ expect(projectionFile?.contents).toContain('type AllEvents = QuestionAnswered | QuestionnaireLinkSent');
619
+
620
+ // canHandle must include BOTH events
621
+ expect(projectionFile?.contents).toContain("canHandle: ['QuestionnaireLinkSent', 'QuestionAnswered']");
622
+ });
623
+ });
@@ -339,7 +339,7 @@ describe('projection.ts.ejs', () => {
339
339
  @Ctx() ctx: GraphQLContext,
340
340
  @Arg('sessionId', () => ID, { nullable: true }) sessionId?: string,
341
341
  ): Promise<Wishlist[]> {
342
- const model = new ReadModel<Wishlist>(ctx.eventStore, 'WishlistProjection');
342
+ const model = new ReadModel<Wishlist>(ctx.database, 'WishlistProjection');
343
343
 
344
344
  // ## IMPLEMENTATION INSTRUCTIONS ##
345
345
  // You can query the projection using the ReadModel API: