@auto-engineer/server-generator-apollo-emmett 1.26.0 → 1.26.1

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 (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +14 -0
  5. package/dist/src/codegen/scaffoldFromSchema.d.ts +1 -0
  6. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  7. package/dist/src/codegen/scaffoldFromSchema.js +3 -3
  8. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  9. package/dist/src/codegen/templates/command/decide.specs.specs.ts +69 -0
  10. package/dist/src/codegen/templates/command/decide.specs.ts +73 -61
  11. package/dist/src/codegen/templates/command/decide.ts.ejs +7 -0
  12. package/dist/src/codegen/templates/query/projection.specs.specs.ts +386 -20
  13. package/dist/src/codegen/templates/query/projection.specs.ts +86 -75
  14. package/dist/src/codegen/templates/query/projection.ts.ejs +19 -0
  15. package/dist/src/codegen/templates/react/react.specs.specs.ts +127 -1
  16. package/dist/src/codegen/templates/react/react.specs.ts +4 -0
  17. package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
  18. package/dist/src/codegen/templates/react/react.ts.ejs +16 -0
  19. package/dist/src/codegen/templates/react/react.ts.specs.ts +194 -0
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/package.json +5 -5
  22. package/src/codegen/formatTsValueSimple.specs.ts +48 -0
  23. package/src/codegen/scaffoldFromSchema.ts +3 -3
  24. package/src/codegen/templates/command/decide.specs.specs.ts +69 -0
  25. package/src/codegen/templates/command/decide.specs.ts +73 -61
  26. package/src/codegen/templates/command/decide.ts.ejs +7 -0
  27. package/src/codegen/templates/query/projection.specs.specs.ts +386 -20
  28. package/src/codegen/templates/query/projection.specs.ts +86 -75
  29. package/src/codegen/templates/query/projection.ts.ejs +19 -0
  30. package/src/codegen/templates/react/react.specs.specs.ts +127 -1
  31. package/src/codegen/templates/react/react.specs.ts +4 -0
  32. package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
  33. package/src/codegen/templates/react/react.ts.ejs +16 -0
  34. package/src/codegen/templates/react/react.ts.specs.ts +194 -0
@@ -217,7 +217,7 @@ describe('react.specs.ts.ejs (react slice)', () => {
217
217
  guestId: 'guest_456',
218
218
  checkIn: '2025-07-15',
219
219
  checkOut: '2025-07-18',
220
- guests: 2,
220
+ guests: '2',
221
221
  message: 'Hey',
222
222
  status: 'pending_host_approval',
223
223
  requestedAt: '2025-06-10T16:30:00.000Z',
@@ -242,4 +242,130 @@ describe('react.specs.ts.ejs (react slice)', () => {
242
242
  "
243
243
  `);
244
244
  });
245
+
246
+ it('should use correct field types from commands array (number fields not strings)', async () => {
247
+ const spec: SpecsSchema = {
248
+ variant: 'specs',
249
+ narratives: [
250
+ {
251
+ name: 'tipping flow',
252
+ slices: [
253
+ {
254
+ type: 'command',
255
+ name: 'complete appointment',
256
+ client: { specs: [] },
257
+ server: {
258
+ description: '',
259
+ specs: [
260
+ {
261
+ type: 'gherkin',
262
+ feature: 'Complete appointment command',
263
+ rules: [
264
+ {
265
+ name: 'Should complete appointment',
266
+ examples: [
267
+ {
268
+ name: 'Appointment completed',
269
+ steps: [
270
+ {
271
+ keyword: 'When',
272
+ text: 'CompleteAppointment',
273
+ docString: { appointmentId: 'apt_001' },
274
+ },
275
+ {
276
+ keyword: 'Then',
277
+ text: 'AppointmentCompleted',
278
+ docString: { appointmentId: 'apt_001' },
279
+ },
280
+ ],
281
+ },
282
+ ],
283
+ },
284
+ ],
285
+ },
286
+ ],
287
+ },
288
+ },
289
+ {
290
+ type: 'react',
291
+ name: 'Process tip after completion',
292
+ server: {
293
+ description: 'Processes tip command after appointment completion',
294
+ data: {
295
+ items: [
296
+ {
297
+ target: { type: 'Command', name: 'ProcessTip' },
298
+ destination: { type: 'stream', pattern: 'tips-${appointmentId}' },
299
+ },
300
+ ],
301
+ },
302
+ specs: [
303
+ {
304
+ type: 'gherkin',
305
+ feature: 'Process tip reaction',
306
+ rules: [
307
+ {
308
+ name: 'Should process tip on appointment completion',
309
+ examples: [
310
+ {
311
+ name: 'Tip processed after completion',
312
+ steps: [
313
+ {
314
+ keyword: 'When',
315
+ text: 'AppointmentCompleted',
316
+ docString: { appointmentId: 'apt_001' },
317
+ },
318
+ {
319
+ keyword: 'Then',
320
+ text: 'ProcessTip',
321
+ docString: {
322
+ appointmentId: 'apt_001',
323
+ tipAmount: '5.00',
324
+ },
325
+ },
326
+ ],
327
+ },
328
+ ],
329
+ },
330
+ ],
331
+ },
332
+ ],
333
+ },
334
+ },
335
+ ],
336
+ },
337
+ ],
338
+ messages: [
339
+ {
340
+ type: 'command',
341
+ name: 'CompleteAppointment',
342
+ fields: [{ name: 'appointmentId', type: 'string', required: true }],
343
+ },
344
+ {
345
+ type: 'command',
346
+ name: 'ProcessTip',
347
+ fields: [
348
+ { name: 'appointmentId', type: 'string', required: true },
349
+ { name: 'tipAmount', type: 'number', required: true },
350
+ ],
351
+ },
352
+ {
353
+ type: 'event',
354
+ name: 'AppointmentCompleted',
355
+ source: 'internal',
356
+ fields: [{ name: 'appointmentId', type: 'string', required: true }],
357
+ },
358
+ ],
359
+ };
360
+
361
+ const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
362
+
363
+ const specFile = plans.find((p) => p.outputPath.endsWith('react.specs.ts'));
364
+ expect(specFile?.contents).toBeDefined();
365
+
366
+ // tipAmount should be a number (5), not a string ("5.00")
367
+ expect(specFile?.contents).toContain('tipAmount: 5');
368
+ expect(specFile?.contents).not.toContain("tipAmount: '5.00'");
369
+ expect(specFile?.contents).not.toContain('tipAmount: "5.00"');
370
+ });
245
371
  });
@@ -254,6 +254,10 @@ describe('handle.ts.ejs (react slice)', () => {
254
254
  * - Optionally return a MessageHandlerResult for SKIP or error cases.
255
255
  */
256
256
 
257
+ // Event (BookingRequested) fields: bookingId: string, hostId: string, message: string
258
+
259
+ // Command (NotifyHost) fields: hostId: string, notificationType: string, priority: string, channels: string[], message: string, actionRequired: boolean
260
+
257
261
  throw new IllegalStateError('Not yet implemented: react in response to BookingRequested');
258
262
 
259
263
  // Example:
@@ -126,7 +126,7 @@ describe('<%= ruleDescription %>', () => {
126
126
  .then({
127
127
  type: '<%= commandSchema.commandRef %>',
128
128
  kind: 'Command',
129
- data: <%- formatDataObject(commandSchema.exampleData, messages.find(m => m.name === commandSchema.commandRef && m.type === 'command')) %>
129
+ data: <%- formatDataObject(commandSchema.exampleData, commands.find(c => c.type === commandSchema.commandRef)) %>
130
130
  });
131
131
  <% } else { %>
132
132
  .then([
@@ -134,7 +134,7 @@ describe('<%= ruleDescription %>', () => {
134
134
  {
135
135
  type: '<%= cmd.commandRef %>',
136
136
  kind: 'Command',
137
- data: <%- formatDataObject(cmd.exampleData, messages.find(m => m.name === cmd.commandRef && m.type === 'command')) %>
137
+ data: <%- formatDataObject(cmd.exampleData, commands.find(c => c.type === cmd.commandRef)) %>
138
138
  },
139
139
  <% } %>
140
140
  ]);
@@ -11,6 +11,12 @@ const then = Array.isArray(gwt?.then) ? gwt.then[0] : gwt?.then;
11
11
 
12
12
  const eventType = when?.eventRef;
13
13
  const commandType = then?.commandRef;
14
+ if (!eventType) throw new Error(
15
+ `react.ts.ejs: slice "${slice.name}" has no event in .when() — check specs`
16
+ );
17
+ if (!commandType) throw new Error(
18
+ `react.ts.ejs: slice "${slice.name}" has no command in .then() — check specs`
19
+ );
14
20
  const event = events.find(e => e.type === eventType);
15
21
  const isCrossFlow = event?.sourceFlowName && event.sourceFlowName !== flowName;
16
22
  const eventImportBase = isCrossFlow
@@ -44,6 +50,16 @@ eachMessage: async (event, context): Promise<MessageHandlerResult> => {
44
50
  * - Context state available: <%= states.map(s => pascalCase(s.type)).join(', ') %> (readable from database)
45
51
  <% } -%>
46
52
  */
53
+ <%
54
+ const eventDef = messages.find(m => m.name === eventType);
55
+ const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
56
+ -%>
57
+ <% if (eventDef?.fields?.length) { %>
58
+ // Event (<%= eventType %>) fields: <%= eventDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
59
+ <% } -%>
60
+ <% if (commandDef?.fields?.length) { %>
61
+ // Command (<%= commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
62
+ <% } -%>
47
63
 
48
64
  throw new IllegalStateError('Not yet implemented: react in response to <%= eventType %>');
49
65
 
@@ -0,0 +1,194 @@
1
+ import type { Model as SpecsSchema } from '@auto-engineer/narrative';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
4
+
5
+ describe('react.ts.ejs', () => {
6
+ it('should throw when react slice has no When event', async () => {
7
+ const spec: SpecsSchema = {
8
+ variant: 'specs',
9
+ narratives: [
10
+ {
11
+ name: 'broken flow',
12
+ slices: [
13
+ {
14
+ type: 'react',
15
+ name: 'missing-when-slice',
16
+ server: {
17
+ description: 'A react slice with no When step',
18
+ data: {
19
+ items: [
20
+ {
21
+ target: { type: 'Command', name: 'DoSomething' },
22
+ destination: { type: 'stream', pattern: 'things-${id}' },
23
+ },
24
+ ],
25
+ },
26
+ specs: [
27
+ {
28
+ type: 'gherkin',
29
+ feature: 'Broken reaction',
30
+ rules: [
31
+ {
32
+ name: 'Should fail gracefully',
33
+ examples: [
34
+ {
35
+ name: 'No when step',
36
+ steps: [
37
+ {
38
+ keyword: 'Then',
39
+ text: 'DoSomething',
40
+ docString: { id: '123' },
41
+ },
42
+ ],
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ },
48
+ ],
49
+ },
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ messages: [
55
+ {
56
+ type: 'command',
57
+ name: 'DoSomething',
58
+ fields: [{ name: 'id', type: 'string', required: true }],
59
+ },
60
+ ],
61
+ };
62
+
63
+ await expect(
64
+ generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows'),
65
+ ).rejects.toThrow('missing-when-slice');
66
+ });
67
+
68
+ it('should include field-type hint comments for event and command', async () => {
69
+ const spec: SpecsSchema = {
70
+ variant: 'specs',
71
+ narratives: [
72
+ {
73
+ name: 'order flow',
74
+ slices: [
75
+ {
76
+ type: 'command',
77
+ name: 'place order',
78
+ client: { specs: [] },
79
+ server: {
80
+ description: '',
81
+ specs: [
82
+ {
83
+ type: 'gherkin',
84
+ feature: 'Place order command',
85
+ rules: [
86
+ {
87
+ name: 'Should place order',
88
+ examples: [
89
+ {
90
+ name: 'Order placed',
91
+ steps: [
92
+ {
93
+ keyword: 'When',
94
+ text: 'PlaceOrder',
95
+ docString: { orderId: 'o1', total: 100 },
96
+ },
97
+ {
98
+ keyword: 'Then',
99
+ text: 'OrderPlaced',
100
+ docString: { orderId: 'o1', total: 100 },
101
+ },
102
+ ],
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ },
110
+ },
111
+ {
112
+ type: 'react',
113
+ name: 'Send order confirmation',
114
+ server: {
115
+ description: 'Sends confirmation after order placed',
116
+ data: {
117
+ items: [
118
+ {
119
+ target: { type: 'Command', name: 'SendConfirmation' },
120
+ destination: { type: 'stream', pattern: 'confirmations-${orderId}' },
121
+ },
122
+ ],
123
+ },
124
+ specs: [
125
+ {
126
+ type: 'gherkin',
127
+ feature: 'Send confirmation reaction',
128
+ rules: [
129
+ {
130
+ name: 'Should send confirmation',
131
+ examples: [
132
+ {
133
+ name: 'Confirmation sent',
134
+ steps: [
135
+ {
136
+ keyword: 'When',
137
+ text: 'OrderPlaced',
138
+ docString: { orderId: 'o1', total: 100 },
139
+ },
140
+ {
141
+ keyword: 'Then',
142
+ text: 'SendConfirmation',
143
+ docString: { orderId: 'o1', email: 'test@test.com' },
144
+ },
145
+ ],
146
+ },
147
+ ],
148
+ },
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ },
154
+ ],
155
+ },
156
+ ],
157
+ messages: [
158
+ {
159
+ type: 'command',
160
+ name: 'PlaceOrder',
161
+ fields: [
162
+ { name: 'orderId', type: 'string', required: true },
163
+ { name: 'total', type: 'number', required: true },
164
+ ],
165
+ },
166
+ {
167
+ type: 'command',
168
+ name: 'SendConfirmation',
169
+ fields: [
170
+ { name: 'orderId', type: 'string', required: true },
171
+ { name: 'email', type: 'string', required: true },
172
+ ],
173
+ },
174
+ {
175
+ type: 'event',
176
+ name: 'OrderPlaced',
177
+ source: 'internal',
178
+ fields: [
179
+ { name: 'orderId', type: 'string', required: true },
180
+ { name: 'total', type: 'number', required: true },
181
+ ],
182
+ },
183
+ ],
184
+ };
185
+
186
+ const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
187
+ const reactFile = plans.find((p) => p.outputPath.endsWith('send-order-confirmation/react.ts'));
188
+ expect(reactFile?.contents).toBeDefined();
189
+
190
+ // Should contain field-type hints for event and command
191
+ expect(reactFile?.contents).toContain('// Event (OrderPlaced) fields:');
192
+ expect(reactFile?.contents).toContain('// Command (SendConfirmation) fields:');
193
+ });
194
+ });