@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts +1 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +3 -3
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +69 -0
- package/dist/src/codegen/templates/command/decide.specs.ts +73 -61
- package/dist/src/codegen/templates/command/decide.ts.ejs +7 -0
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +386 -20
- package/dist/src/codegen/templates/query/projection.specs.ts +86 -75
- package/dist/src/codegen/templates/query/projection.ts.ejs +19 -0
- package/dist/src/codegen/templates/react/react.specs.specs.ts +127 -1
- package/dist/src/codegen/templates/react/react.specs.ts +4 -0
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/dist/src/codegen/templates/react/react.ts.ejs +16 -0
- package/dist/src/codegen/templates/react/react.ts.specs.ts +194 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/codegen/formatTsValueSimple.specs.ts +48 -0
- package/src/codegen/scaffoldFromSchema.ts +3 -3
- package/src/codegen/templates/command/decide.specs.specs.ts +69 -0
- package/src/codegen/templates/command/decide.specs.ts +73 -61
- package/src/codegen/templates/command/decide.ts.ejs +7 -0
- package/src/codegen/templates/query/projection.specs.specs.ts +386 -20
- package/src/codegen/templates/query/projection.specs.ts +86 -75
- package/src/codegen/templates/query/projection.ts.ejs +19 -0
- package/src/codegen/templates/react/react.specs.specs.ts +127 -1
- package/src/codegen/templates/react/react.specs.ts +4 -0
- package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/src/codegen/templates/react/react.ts.ejs +16 -0
- 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,
|
|
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,
|
|
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
|
+
});
|