@auto-engineer/server-generator-apollo-emmett 1.110.1 → 1.110.3
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 +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +42 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +3 -1
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/templates/command/handle.specs.ts +102 -0
- package/dist/src/codegen/templates/command/handle.ts.ejs +20 -9
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +4 -4
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +22 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/messages.specs.ts +96 -0
- package/src/codegen/extract/messages.ts +15 -13
- package/src/codegen/templates/command/handle.specs.ts +102 -0
- package/src/codegen/templates/command/handle.ts.ejs +20 -9
- package/src/codegen/templates/query/query.resolver.specs.ts +4 -4
- package/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +22 -1
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^13.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/
|
|
36
|
-
"@auto-engineer/
|
|
35
|
+
"@auto-engineer/narrative": "1.110.3",
|
|
36
|
+
"@auto-engineer/message-bus": "1.110.3"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.8.3",
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
|
-
"@auto-engineer/cli": "1.110.
|
|
47
|
+
"@auto-engineer/cli": "1.110.3"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.110.
|
|
49
|
+
"version": "1.110.3",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"generate:server": "tsx src/cli/index.ts",
|
|
52
52
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts && rm -rf dist/src/codegen/templates && mkdir -p dist/src/codegen && cp -r src/codegen/templates dist/src/codegen/templates && cp src/server.ts dist/src && cp -r src/utils dist/src && cp -r src/domain dist/src",
|
|
@@ -374,6 +374,102 @@ describe('extractMessagesFromSpecs (command slice)', () => {
|
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
376
|
|
|
377
|
+
describe('extractMessagesFromSpecs (query slice)', () => {
|
|
378
|
+
it('should not include non-event When steps in the events list', () => {
|
|
379
|
+
const slice: Slice = {
|
|
380
|
+
type: 'query',
|
|
381
|
+
name: 'views workout details',
|
|
382
|
+
server: {
|
|
383
|
+
description: 'Shows workout details',
|
|
384
|
+
data: {
|
|
385
|
+
items: [
|
|
386
|
+
{
|
|
387
|
+
origin: {
|
|
388
|
+
type: 'projection',
|
|
389
|
+
idField: 'workoutId',
|
|
390
|
+
name: 'WorkoutDetailsProjection',
|
|
391
|
+
},
|
|
392
|
+
target: {
|
|
393
|
+
type: 'State',
|
|
394
|
+
name: 'WorkoutDetails',
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
specs: [
|
|
400
|
+
{
|
|
401
|
+
type: 'gherkin',
|
|
402
|
+
feature: 'Workout details query',
|
|
403
|
+
rules: [
|
|
404
|
+
{
|
|
405
|
+
name: 'Should show workout details',
|
|
406
|
+
examples: [
|
|
407
|
+
{
|
|
408
|
+
name: 'Workout details shown',
|
|
409
|
+
steps: [
|
|
410
|
+
{
|
|
411
|
+
keyword: 'Given',
|
|
412
|
+
text: 'WorkoutLogged',
|
|
413
|
+
docString: { workoutId: 'wkt_001', exercise: 'squat' },
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
keyword: 'When',
|
|
417
|
+
text: 'GetWorkoutDetails',
|
|
418
|
+
docString: {},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
keyword: 'Then',
|
|
422
|
+
text: 'WorkoutDetails',
|
|
423
|
+
docString: { workoutId: 'wkt_001', exercise: 'squat' },
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const allMessages: MessageDefinition[] = [
|
|
436
|
+
{
|
|
437
|
+
type: 'event',
|
|
438
|
+
name: 'WorkoutLogged',
|
|
439
|
+
fields: [
|
|
440
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
441
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
442
|
+
],
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: 'state',
|
|
446
|
+
name: 'WorkoutDetails',
|
|
447
|
+
fields: [
|
|
448
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
449
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const result = extractMessagesFromSpecs(slice, allMessages);
|
|
455
|
+
|
|
456
|
+
expect(result.events).toEqual([
|
|
457
|
+
{
|
|
458
|
+
type: 'WorkoutLogged',
|
|
459
|
+
fields: [
|
|
460
|
+
{ name: 'workoutId', tsType: 'string', required: true },
|
|
461
|
+
{ name: 'exercise', tsType: 'string', required: true },
|
|
462
|
+
],
|
|
463
|
+
source: 'given',
|
|
464
|
+
sourceFlowName: undefined,
|
|
465
|
+
sourceSliceName: undefined,
|
|
466
|
+
},
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
expect(result.events.some((e) => e.type === 'GetWorkoutDetails')).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
377
473
|
describe('extractMessagesFromSpecs (react slice with data target events)', () => {
|
|
378
474
|
it('should extract data target events with source then', () => {
|
|
379
475
|
const slice: Slice = {
|
|
@@ -132,19 +132,21 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
|
|
|
132
132
|
const events: Message[] = gwtSpecs.flatMap((gwt) => {
|
|
133
133
|
const eventsFromWhen: EventRef[] = Array.isArray(gwt.when) ? gwt.when : [];
|
|
134
134
|
const givenEvents = extractEventsFromGiven(gwt.given, allMessages);
|
|
135
|
-
const whenEvents: Message[] = eventsFromWhen
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
135
|
+
const whenEvents: Message[] = eventsFromWhen
|
|
136
|
+
.filter((eventExample) => allMessages.some((m) => m.type === 'event' && m.name === eventExample.eventRef))
|
|
137
|
+
.map((eventExample) => {
|
|
138
|
+
const fields = extractFieldsFromMessage(eventExample.eventRef, 'event', allMessages);
|
|
139
|
+
const messageDef = allMessages.find((m) => m.type === 'event' && m.name === eventExample.eventRef);
|
|
140
|
+
const metadata = messageDef?.metadata as { sourceFlowName?: string; sourceSliceName?: string } | undefined;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
type: eventExample.eventRef,
|
|
144
|
+
fields,
|
|
145
|
+
source: 'when',
|
|
146
|
+
sourceFlowName: metadata?.sourceFlowName,
|
|
147
|
+
sourceSliceName: metadata?.sourceSliceName,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
148
150
|
|
|
149
151
|
return [...givenEvents, ...whenEvents];
|
|
150
152
|
});
|
|
@@ -477,4 +477,106 @@ describe('generateScaffoldFilePlans', () => {
|
|
|
477
477
|
"
|
|
478
478
|
`);
|
|
479
479
|
});
|
|
480
|
+
it('should generate randomUUID for stream vars not in any command', async () => {
|
|
481
|
+
const spec: SpecsSchema = {
|
|
482
|
+
variant: 'specs',
|
|
483
|
+
narratives: [
|
|
484
|
+
{
|
|
485
|
+
name: 'Fitness tracker',
|
|
486
|
+
slices: [
|
|
487
|
+
{
|
|
488
|
+
type: 'command',
|
|
489
|
+
name: 'Log workout',
|
|
490
|
+
stream: 'workouts-${workoutId}',
|
|
491
|
+
client: { specs: [] },
|
|
492
|
+
server: {
|
|
493
|
+
description: 'test',
|
|
494
|
+
specs: [
|
|
495
|
+
{
|
|
496
|
+
type: 'gherkin',
|
|
497
|
+
feature: 'Log workout',
|
|
498
|
+
rules: [
|
|
499
|
+
{
|
|
500
|
+
name: 'Should log workout',
|
|
501
|
+
examples: [
|
|
502
|
+
{
|
|
503
|
+
name: 'Workout logged',
|
|
504
|
+
steps: [
|
|
505
|
+
{
|
|
506
|
+
keyword: 'When',
|
|
507
|
+
text: 'LogWorkout',
|
|
508
|
+
docString: { exercise: 'squat', reps: 10 },
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
keyword: 'Then',
|
|
512
|
+
text: 'WorkoutLogged',
|
|
513
|
+
docString: { exercise: 'squat', reps: 10 },
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
data: {
|
|
523
|
+
items: [
|
|
524
|
+
{
|
|
525
|
+
target: { type: 'Event', name: 'WorkoutLogged' },
|
|
526
|
+
destination: { type: 'stream', pattern: 'workouts-${workoutId}' },
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
messages: [
|
|
536
|
+
{
|
|
537
|
+
type: 'command',
|
|
538
|
+
name: 'LogWorkout',
|
|
539
|
+
fields: [
|
|
540
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
541
|
+
{ name: 'reps', type: 'number', required: true },
|
|
542
|
+
],
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
type: 'event',
|
|
546
|
+
name: 'WorkoutLogged',
|
|
547
|
+
source: 'internal',
|
|
548
|
+
fields: [
|
|
549
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
550
|
+
{ name: 'reps', type: 'number', required: true },
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
557
|
+
const handleFile = plans.find((p) => p.outputPath.endsWith('handle.ts'));
|
|
558
|
+
|
|
559
|
+
expect(handleFile?.contents).toMatchInlineSnapshot(`
|
|
560
|
+
"import { randomUUID } from 'crypto';
|
|
561
|
+
|
|
562
|
+
import { CommandHandler, type EventStore, type MessageHandlerResult } from '@event-driven-io/emmett';
|
|
563
|
+
import { evolve } from './evolve';
|
|
564
|
+
import { initialState } from './state';
|
|
565
|
+
import { decide } from './decide';
|
|
566
|
+
import type { LogWorkout } from './commands';
|
|
567
|
+
|
|
568
|
+
const handler = CommandHandler({
|
|
569
|
+
evolve,
|
|
570
|
+
initialState,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
export const handle = async (eventStore: EventStore, command: LogWorkout): Promise<MessageHandlerResult> => {
|
|
574
|
+
const streamId = \`workouts-\${randomUUID()}\`;
|
|
575
|
+
|
|
576
|
+
await handler(eventStore, streamId, (state) => decide(command, state));
|
|
577
|
+
return undefined;
|
|
578
|
+
};
|
|
579
|
+
"
|
|
580
|
+
`);
|
|
581
|
+
});
|
|
480
582
|
});
|
|
@@ -63,8 +63,14 @@ while ((svMatch = svRegex.exec(streamPatternStr)) !== null) {
|
|
|
63
63
|
}
|
|
64
64
|
const allCmdFieldSets = commands.map(c => new Set((c.fields ?? []).map(f => f.name)));
|
|
65
65
|
const needsStreamGuard = streamVars.length > 0 && !allCmdFieldSets.every(fs => streamVars.every(v => fs.has(v)));
|
|
66
|
+
const varInAnyCommand = (v) => allCmdFieldSets.some(fs => fs.has(v));
|
|
67
|
+
const guardVars = needsStreamGuard ? streamVars.filter(v => varInAnyCommand(v)) : [];
|
|
68
|
+
const uuidVars = needsStreamGuard ? streamVars.filter(v => !varInAnyCommand(v)) : [];
|
|
66
69
|
%>
|
|
67
70
|
|
|
71
|
+
<% if (uuidVars.length > 0) { -%>
|
|
72
|
+
import { randomUUID } from 'crypto';
|
|
73
|
+
<% } -%>
|
|
68
74
|
<% integrationSideEffectImports.forEach((importSource) => { %>
|
|
69
75
|
import '<%= importSource %>';
|
|
70
76
|
<% }); %>
|
|
@@ -96,16 +102,21 @@ eventStore: EventStore,
|
|
|
96
102
|
command: <%= commands.map(c => pascalCase(c.type)).join(' | ') %>
|
|
97
103
|
): Promise<MessageHandlerResult> => {
|
|
98
104
|
<% if (stream?.pattern?.includes('${')) { -%>
|
|
99
|
-
<% if (needsStreamGuard) { -%>
|
|
100
|
-
const commandData = command.data;
|
|
101
|
-
<% for (const v of streamVars) { -%>
|
|
102
|
-
if (!('<%= v %>' in commandData)) {
|
|
103
|
-
throw new Error('Cannot determine stream: "<%= v %>" not in command data');
|
|
104
|
-
}
|
|
105
|
-
<% } -%>
|
|
106
|
-
const streamId = `<%= stream.pattern.replace(/\$\{([^}]+)\}/g, (_, key) => `\${commandData.${key}}`) %>`;
|
|
107
|
-
<% } else { -%>
|
|
105
|
+
<% if (!needsStreamGuard) { -%>
|
|
108
106
|
const streamId = `<%= stream.pattern.replace(/\$\{([^}]+)\}/g, (_, key) => `\${command.data.${key}}`) %>`;
|
|
107
|
+
<% } else { -%>
|
|
108
|
+
<% if (guardVars.length > 0) { -%>
|
|
109
|
+
const commandData = command.data;
|
|
110
|
+
<% for (const v of guardVars) { -%>
|
|
111
|
+
if (!('<%= v %>' in commandData)) {
|
|
112
|
+
throw new Error('Cannot determine stream: "<%= v %>" not in command data');
|
|
113
|
+
}
|
|
114
|
+
<% } -%>
|
|
115
|
+
<% } -%>
|
|
116
|
+
const streamId = `<%= stream.pattern.replace(/\$\{([^}]+)\}/g, (_, key) => {
|
|
117
|
+
if (varInAnyCommand(key)) return '${commandData.' + key + '}';
|
|
118
|
+
return '${randomUUID()}';
|
|
119
|
+
}) %>`;
|
|
109
120
|
<% } -%>
|
|
110
121
|
<% } else { -%>
|
|
111
122
|
const streamId = '<%= stream?.pattern ?? 'unknown-stream' %>';
|
|
@@ -114,9 +114,9 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
114
114
|
return model.find((item) => {
|
|
115
115
|
if (location !== undefined && item.location !== location) return false;
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
// TODO: 'maxPrice' has no matching field on the state type — implement custom filter logic.
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
// TODO: 'minGuests' has no matching field on the state type — implement custom filter logic.
|
|
120
120
|
|
|
121
121
|
return true;
|
|
122
122
|
});
|
|
@@ -935,7 +935,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
935
935
|
// If this query should return a single item, switch to findOne().
|
|
936
936
|
|
|
937
937
|
return model.find((item) => {
|
|
938
|
-
|
|
938
|
+
// TODO: 'pantryId' has no matching field on the state type — implement custom filter logic.
|
|
939
939
|
|
|
940
940
|
return true;
|
|
941
941
|
});
|
|
@@ -1177,7 +1177,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
1177
1177
|
// If this query should return a single item, switch to findOne().
|
|
1178
1178
|
|
|
1179
1179
|
return model.find((item) => {
|
|
1180
|
-
|
|
1180
|
+
// TODO: 'pantryId' has no matching field on the state type — implement custom filter logic.
|
|
1181
1181
|
|
|
1182
1182
|
return true;
|
|
1183
1183
|
});
|
|
@@ -165,9 +165,15 @@ const model = new ReadModel<<%= viewType %>>(ctx.database, '<%= collectionName %
|
|
|
165
165
|
|
|
166
166
|
return model.find((<%= hasArgs ? 'item' : '_item' %>) => {
|
|
167
167
|
<% if (parsedRequest?.args?.length) {
|
|
168
|
-
|
|
168
|
+
const stateFieldNames = new Set(messageFields.map(f => f.name));
|
|
169
|
+
for (const arg of parsedRequest.args) {
|
|
170
|
+
if (stateFieldNames.has(arg.name)) { %>
|
|
169
171
|
if (<%= arg.name %> !== undefined && item.<%= arg.name %> !== <%= arg.name %>) return false;
|
|
170
|
-
<%
|
|
172
|
+
<% } else { %>
|
|
173
|
+
// TODO: '<%= arg.name %>' has no matching field on the state type — implement custom filter logic.
|
|
174
|
+
<% }
|
|
175
|
+
}
|
|
176
|
+
} %>
|
|
171
177
|
return true;
|
|
172
178
|
});
|
|
173
179
|
}
|
|
@@ -132,11 +132,32 @@ describe('<%= ruleDescription %>', () => {
|
|
|
132
132
|
}]);
|
|
133
133
|
<% }
|
|
134
134
|
}
|
|
135
|
+
-%>
|
|
136
|
+
<%
|
|
137
|
+
const eventSchema = events.find(e => e.type === exampleEvent.eventRef);
|
|
138
|
+
const mergedEventData = { ...(exampleEvent.exampleData || {}) };
|
|
139
|
+
const eventFieldNames = new Set((eventSchema?.fields || []).map(f => f.name));
|
|
140
|
+
for (const cmd of thenCommands) {
|
|
141
|
+
for (const [key, value] of Object.entries(cmd.exampleData || {})) {
|
|
142
|
+
if (!(key in mergedEventData) && eventFieldNames.has(key)) {
|
|
143
|
+
mergedEventData[key] = value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const field of (eventSchema?.fields || [])) {
|
|
148
|
+
if (!(field.name in mergedEventData)) {
|
|
149
|
+
const tsType = field.tsType || field.type || 'string';
|
|
150
|
+
if (tsType === 'number') mergedEventData[field.name] = 0;
|
|
151
|
+
else if (tsType === 'boolean') mergedEventData[field.name] = false;
|
|
152
|
+
else if (!tsType.includes('{') && !tsType.includes('[]') && !tsType.includes('Array'))
|
|
153
|
+
mergedEventData[field.name] = `test-${field.name}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
135
156
|
-%>
|
|
136
157
|
await given([])
|
|
137
158
|
.when({
|
|
138
159
|
type: '<%= exampleEvent.eventRef %>',
|
|
139
|
-
data: <%- formatDataObject(
|
|
160
|
+
data: <%- formatDataObject(mergedEventData, eventSchema) %>
|
|
140
161
|
})
|
|
141
162
|
<% if (thenCommands.length === 1) {
|
|
142
163
|
const commandSchema = thenCommands[0];
|