@auto-engineer/server-generator-apollo-emmett 1.150.0 → 1.152.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.
- 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 +74 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +3 -2
- package/dist/src/codegen/extract/type-helpers.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +44 -6
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templateHelpers.d.ts +1 -0
- package/dist/src/codegen/templateHelpers.d.ts.map +1 -1
- package/dist/src/codegen/templateHelpers.js +12 -0
- package/dist/src/codegen/templateHelpers.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +688 -11
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +90 -3
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +6 -3
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +3 -8
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +63 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +19 -5
- package/dist/src/codegen/templates/react/react.ts.ejs +71 -19
- package/dist/src/codegen/templates/react/react.ts.specs.ts +139 -0
- package/dist/src/codegen/templates/react/register.ts.ejs +34 -9
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -5
- package/package.json +4 -4
- package/src/codegen/extract/type-helpers.specs.ts +21 -0
- package/src/codegen/extract/type-helpers.ts +3 -2
- package/src/codegen/scaffoldFromSchema.filter.specs.ts +110 -1
- package/src/codegen/scaffoldFromSchema.ts +47 -8
- package/src/codegen/templateHelpers.specs.ts +21 -1
- package/src/codegen/templateHelpers.ts +9 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +688 -11
- package/src/codegen/templates/command/decide.specs.ts.ejs +90 -3
- package/src/codegen/templates/query/projection.specs.specs.ts +6 -3
- package/src/codegen/templates/query/projection.specs.ts.ejs +3 -8
- package/src/codegen/templates/query/query.resolver.specs.ts +63 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +8 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +19 -5
- package/src/codegen/templates/react/react.ts.ejs +71 -19
- package/src/codegen/templates/react/react.ts.specs.ts +139 -0
- package/src/codegen/templates/react/register.ts.ejs +34 -9
|
@@ -54,8 +54,33 @@ for (const [importPath, eventTypes] of testEventsByPath.entries()) {
|
|
|
54
54
|
|
|
55
55
|
const uniqueEventTypes = Array.from(new Set(allEvents.map(e => e?.type).filter(Boolean))).sort();
|
|
56
56
|
|
|
57
|
+
const successScenarios = [];
|
|
58
|
+
const seenCommandEvents = new Set();
|
|
59
|
+
for (const commandName in gwtMapping) {
|
|
60
|
+
const cases = gwtMapping[commandName];
|
|
61
|
+
const schema = commandSchemasByName[commandName];
|
|
62
|
+
const cmdFieldNames = new Set(schema?.fields?.map(f => f.name) || []);
|
|
63
|
+
for (const gwt of cases) {
|
|
64
|
+
const eventResults = gwt.then.filter(t => 'eventRef' in t);
|
|
65
|
+
const errorResult = gwt.then.find(t => 'errorType' in t);
|
|
66
|
+
if (errorResult) continue;
|
|
67
|
+
const givenEventRefs = (gwt.given || []).filter(g => events.some(ev => ev.type === g.eventRef));
|
|
68
|
+
for (const e of eventResults) {
|
|
69
|
+
const dedupeKey = `${commandName}::${e.eventRef}`;
|
|
70
|
+
if (seenCommandEvents.has(dedupeKey)) continue;
|
|
71
|
+
seenCommandEvents.add(dedupeKey);
|
|
72
|
+
const evtSchema = events.find(evt => evt.type === e.eventRef);
|
|
73
|
+
const evtFields = evtSchema?.fields || [];
|
|
74
|
+
const nonCmdFields = evtFields.filter(f => !cmdFieldNames.has(f.name));
|
|
75
|
+
if (nonCmdFields.length > 0) {
|
|
76
|
+
successScenarios.push({ commandName, gwt, eventRef: e.eventRef, evtSchema, nonCmdFields, cmdFieldNames, thenExampleData: e.exampleData || {}, givenEventRefs });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const hasFieldCompletenessTests = successScenarios.length > 0;
|
|
57
82
|
_%>
|
|
58
|
-
import { describe, it } from 'vitest';
|
|
83
|
+
import { describe, <% if (hasFieldCompletenessTests) { %>expect, <% } %>it } from 'vitest';
|
|
59
84
|
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
60
85
|
import { decide } from './decide';
|
|
61
86
|
import { evolve } from './evolve';
|
|
@@ -108,8 +133,8 @@ describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
|
108
133
|
-%>
|
|
109
134
|
data: <%- formatDataObject(filteredCmdData, schema) %>,
|
|
110
135
|
<%
|
|
111
|
-
const { date: derivedDate, fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, commandFieldNames,
|
|
112
|
-
const keepFieldNames = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames,
|
|
136
|
+
const { date: derivedDate, fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, commandFieldNames, givenEvents);
|
|
137
|
+
const keepFieldNames = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents);
|
|
113
138
|
-%>
|
|
114
139
|
metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
|
|
115
140
|
})
|
|
@@ -133,4 +158,66 @@ describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
|
133
158
|
});
|
|
134
159
|
<% } %>
|
|
135
160
|
});
|
|
161
|
+
<% } %>
|
|
162
|
+
<% if (hasFieldCompletenessTests) {
|
|
163
|
+
const scenariosByCommand = new Map();
|
|
164
|
+
for (const s of successScenarios) {
|
|
165
|
+
if (!scenariosByCommand.has(s.commandName)) scenariosByCommand.set(s.commandName, []);
|
|
166
|
+
scenariosByCommand.get(s.commandName).push(s);
|
|
167
|
+
}
|
|
168
|
+
%>
|
|
169
|
+
describe('field completeness', () => {
|
|
170
|
+
<% for (const [cmdName, scenarios] of scenariosByCommand.entries()) {
|
|
171
|
+
const firstScenario = scenarios[0];
|
|
172
|
+
const gwt = firstScenario.gwt;
|
|
173
|
+
const schema = commandSchemasByName[cmdName];
|
|
174
|
+
const givenEvents = (gwt.given || []).filter(g => events.some(e => e.type === g.eventRef));
|
|
175
|
+
const example = gwt.when;
|
|
176
|
+
const cmdFieldNames = firstScenario.cmdFieldNames;
|
|
177
|
+
const filteredCmdData = Object.fromEntries(
|
|
178
|
+
Object.entries(example.exampleData || {}).filter(([key]) => cmdFieldNames.has(key))
|
|
179
|
+
);
|
|
180
|
+
%>
|
|
181
|
+
it('should include all required fields in <%= scenarios.map(s => s.eventRef).join(', ') %>', () => {
|
|
182
|
+
<%_ if (givenEvents.length) { _%>
|
|
183
|
+
const state = [
|
|
184
|
+
<%- givenEvents.map(g => `{
|
|
185
|
+
type: '${g.eventRef}' as const,
|
|
186
|
+
data: ${formatDataObject(g.exampleData, events.find(e => e.type === g.eventRef))}
|
|
187
|
+
}`).join(',\n ') %>
|
|
188
|
+
].reduce((s, e) => evolve(s, e), initialState());
|
|
189
|
+
<%_ } else { _%>
|
|
190
|
+
const state = initialState();
|
|
191
|
+
<%_ } _%>
|
|
192
|
+
const result = decide(
|
|
193
|
+
{ type: '<%= example.commandRef %>', data: <%- formatDataObject(filteredCmdData, schema) %>, metadata: { now: new Date() } },
|
|
194
|
+
state,
|
|
195
|
+
);
|
|
196
|
+
const evtArray = Array.isArray(result) ? result : [result];
|
|
197
|
+
<% for (let i = 0; i < scenarios.length; i++) {
|
|
198
|
+
const s = scenarios[i];
|
|
199
|
+
const allFields = s.evtSchema?.fields || [];
|
|
200
|
+
%>
|
|
201
|
+
expect(evtArray[<%= i %>]).toMatchObject({
|
|
202
|
+
type: '<%= s.eventRef %>',
|
|
203
|
+
data: expect.objectContaining({
|
|
204
|
+
<% for (const f of allFields) {
|
|
205
|
+
const isCommandField = s.cmdFieldNames.has(f.name);
|
|
206
|
+
const modelValue = s.thenExampleData[f.name];
|
|
207
|
+
const isTraceableToGivenEvent = modelValue !== undefined
|
|
208
|
+
&& isKeyTraceable(f.name, modelValue, s.givenEventRefs);
|
|
209
|
+
const hasModelValue = !isCommandField && isTraceableToGivenEvent;
|
|
210
|
+
-%>
|
|
211
|
+
<% if (hasModelValue) { -%>
|
|
212
|
+
<%= f.name %>: <%- formatTsValue(modelValue, f.tsType ?? f.type ?? 'string') %>,
|
|
213
|
+
<% } else { -%>
|
|
214
|
+
<%= f.name %>: expect.any(<%= jsConstructorForType(f.tsType ?? f.type ?? 'string') %>),
|
|
215
|
+
<% } -%>
|
|
216
|
+
<% } -%>
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
<% } %>
|
|
220
|
+
});
|
|
221
|
+
<% } %>
|
|
222
|
+
});
|
|
136
223
|
<% } %>
|
|
@@ -1202,7 +1202,8 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
1202
1202
|
.collection<WorkoutHistory>('WorkoutHistoryProjection')
|
|
1203
1203
|
.findOne((doc) => doc.memberId === 'mem_001');
|
|
1204
1204
|
|
|
1205
|
-
const expected = {
|
|
1205
|
+
const expected: WorkoutHistory = {
|
|
1206
|
+
memberId: 'mem_001',
|
|
1206
1207
|
totalCalories: 250,
|
|
1207
1208
|
};
|
|
1208
1209
|
|
|
@@ -1213,7 +1214,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
1213
1214
|
`);
|
|
1214
1215
|
});
|
|
1215
1216
|
|
|
1216
|
-
it('should
|
|
1217
|
+
it('should include all Then fields in query action assertions', async () => {
|
|
1217
1218
|
const spec: SpecsSchema = {
|
|
1218
1219
|
variant: 'specs',
|
|
1219
1220
|
scenes: [
|
|
@@ -1417,11 +1418,13 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
1417
1418
|
.collection<FartHistory>('FartHistoryProjection')
|
|
1418
1419
|
.findOne((doc) => doc.userId === 'usr_123');
|
|
1419
1420
|
|
|
1420
|
-
const expected = {
|
|
1421
|
+
const expected: FartHistory = {
|
|
1422
|
+
userId: 'usr_123',
|
|
1421
1423
|
farts: [
|
|
1422
1424
|
{ fartId: 'fart_456', intensity: 7 },
|
|
1423
1425
|
{ fartId: 'fart_789', intensity: 9 },
|
|
1424
1426
|
],
|
|
1427
|
+
minIntensity: 6,
|
|
1425
1428
|
};
|
|
1426
1429
|
|
|
1427
1430
|
expect(document).toMatchObject(expected);
|
|
@@ -282,15 +282,10 @@ if (isCompositeKey) {
|
|
|
282
282
|
|
|
283
283
|
<%
|
|
284
284
|
const stateKeys = Object.keys(expectedState.exampleData || {});
|
|
285
|
-
const
|
|
286
|
-
? (testCase.when?.args
|
|
287
|
-
|| (Array.isArray(testCase.when) && testCase.when[0]?.exampleData)
|
|
288
|
-
|| {})
|
|
289
|
-
: {};
|
|
290
|
-
const queryArgKeys = new Set(Object.keys(queryArgs));
|
|
291
|
-
const assertionKeys = stateKeys.filter(k => !queryArgKeys.has(k));
|
|
285
|
+
const assertionKeys = stateKeys;
|
|
292
286
|
const stateMessage = messages.find(m => m.name === targetName);
|
|
293
|
-
const
|
|
287
|
+
const stateFieldSet = new Set((stateMessage?.fields || []).map(f => f.name));
|
|
288
|
+
const isPartial = assertionKeys.length !== stateFieldSet.size || assertionKeys.some(k => !stateFieldSet.has(k));
|
|
294
289
|
-%>
|
|
295
290
|
const expected<%= isPartial ? '' : `: ${TargetType}` %> = {
|
|
296
291
|
<% for (let i = 0; i < assertionKeys.length; i++) {
|
|
@@ -1478,4 +1478,67 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
1478
1478
|
expect(resolverFile?.contents).toContain('item.numBedrooms < minBedrooms');
|
|
1479
1479
|
expect(resolverFile?.contents).toContain('item.pricePerNight > maxPrice');
|
|
1480
1480
|
});
|
|
1481
|
+
|
|
1482
|
+
it('should generate Date-aware comparison for Date-typed filter fields', async () => {
|
|
1483
|
+
const spec: SpecsSchema = {
|
|
1484
|
+
variant: 'specs',
|
|
1485
|
+
scenes: [
|
|
1486
|
+
{
|
|
1487
|
+
name: 'booking-scene',
|
|
1488
|
+
moments: [
|
|
1489
|
+
{
|
|
1490
|
+
type: 'query',
|
|
1491
|
+
name: 'search-bookings',
|
|
1492
|
+
request: `
|
|
1493
|
+
query SearchBookings($checkIn: String) {
|
|
1494
|
+
searchBookings(checkIn: $checkIn) {
|
|
1495
|
+
bookingId
|
|
1496
|
+
checkIn
|
|
1497
|
+
guestName
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
`,
|
|
1501
|
+
client: { specs: [] },
|
|
1502
|
+
server: {
|
|
1503
|
+
description: '',
|
|
1504
|
+
data: {
|
|
1505
|
+
items: [
|
|
1506
|
+
{
|
|
1507
|
+
origin: {
|
|
1508
|
+
type: 'projection',
|
|
1509
|
+
idField: 'bookingId',
|
|
1510
|
+
name: 'BookingsProjection',
|
|
1511
|
+
},
|
|
1512
|
+
target: {
|
|
1513
|
+
type: 'State',
|
|
1514
|
+
name: 'BookingView',
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
},
|
|
1519
|
+
specs: [],
|
|
1520
|
+
},
|
|
1521
|
+
},
|
|
1522
|
+
],
|
|
1523
|
+
},
|
|
1524
|
+
],
|
|
1525
|
+
messages: [
|
|
1526
|
+
{
|
|
1527
|
+
type: 'state',
|
|
1528
|
+
name: 'BookingView',
|
|
1529
|
+
fields: [
|
|
1530
|
+
{ name: 'bookingId', type: 'string', required: true },
|
|
1531
|
+
{ name: 'checkIn', type: 'Date', required: true },
|
|
1532
|
+
{ name: 'guestName', type: 'string', required: true },
|
|
1533
|
+
],
|
|
1534
|
+
},
|
|
1535
|
+
],
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
1539
|
+
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
1540
|
+
|
|
1541
|
+
expect(resolverFile?.contents).toContain('new Date(item.checkIn).getTime()');
|
|
1542
|
+
expect(resolverFile?.contents).toContain('new Date(checkIn).getTime()');
|
|
1543
|
+
});
|
|
1481
1544
|
});
|
|
@@ -195,9 +195,15 @@ return model.<%= isIdLookup ? 'findOne' : 'find' %>((<%= hasMatchingArgs ? 'item
|
|
|
195
195
|
const mapping = argFieldMap[arg.name];
|
|
196
196
|
const mappedField = mapping ? mapping.field : (stateFieldNames.has(arg.name) ? arg.name : null);
|
|
197
197
|
const op = mapping ? negatedOp[mapping.operator] : '!==';
|
|
198
|
-
if (mappedField) {
|
|
198
|
+
if (mappedField) {
|
|
199
|
+
const stateField = messageFields.find(f => f.name === mappedField);
|
|
200
|
+
const isDateField = stateField && fieldUsesDate(stateField.type ?? 'string');
|
|
201
|
+
if (isDateField) { %>
|
|
202
|
+
if (<%= arg.name %> !== undefined && new Date(item.<%= mappedField %>).getTime() <%= op %> new Date(<%= arg.name %>).getTime()) return false;
|
|
203
|
+
<% } else { %>
|
|
199
204
|
if (<%= arg.name %> !== undefined && item.<%= mappedField %> <%= op %> <%= arg.name %>) return false;
|
|
200
|
-
<%
|
|
205
|
+
<% }
|
|
206
|
+
} else { %>
|
|
201
207
|
// NOTE: '<%= arg.name %>' has no matching field on the state type — add custom filter logic if needed.
|
|
202
208
|
<% }
|
|
203
209
|
}
|
|
@@ -119,18 +119,32 @@ describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
|
119
119
|
const givenStates = testCase.given.filter(g =>
|
|
120
120
|
messages.some(m => m.type === 'state' && m.name === g.eventRef)
|
|
121
121
|
);
|
|
122
|
+
const targetCommand = thenCommands[0]?.commandRef;
|
|
123
|
+
const matchedPair = eventCommandPairs.find(p => p.commandType === targetCommand);
|
|
124
|
+
const pairStreamPattern = matchedPair?.streamPattern;
|
|
122
125
|
for (const gs of givenStates) {
|
|
123
126
|
const stateSchema = states.find(s => s.type === gs.eventRef);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
if (pairStreamPattern) {
|
|
128
|
+
const resolvedStream = pairStreamPattern.replace(/\$\{([^}]+)\}/g, (_, key) =>
|
|
129
|
+
String(exampleEvent.exampleData?.[key] ?? gs.exampleData?.[key] ?? 'unknown')
|
|
130
|
+
);
|
|
131
|
+
-%>
|
|
132
|
+
await eventStore.appendToStream('<%= resolvedStream %>', [{
|
|
133
|
+
type: '<%= gs.eventRef %>Initialized',
|
|
134
|
+
data: <%- formatDataObject(gs.exampleData, stateSchema) %>
|
|
135
|
+
}]);
|
|
136
|
+
<% } else {
|
|
137
|
+
const eventDef = messages.find(m => m.name === exampleEvent.eventRef);
|
|
138
|
+
const linkingField = findPrimitiveLinkingField(stateSchema?.fields || [], eventDef?.fields || []);
|
|
139
|
+
if (linkingField) {
|
|
140
|
+
const linkingValue = gs.exampleData[linkingField];
|
|
128
141
|
-%>
|
|
129
142
|
await eventStore.appendToStream('<%= gs.eventRef %>-<%= linkingValue %>', [{
|
|
130
143
|
type: '<%= gs.eventRef %>Initialized',
|
|
131
144
|
data: <%- formatDataObject(gs.exampleData, stateSchema) %>
|
|
132
145
|
}]);
|
|
133
|
-
<%
|
|
146
|
+
<% }
|
|
147
|
+
}
|
|
134
148
|
}
|
|
135
149
|
-%>
|
|
136
150
|
<%
|
|
@@ -22,6 +22,7 @@ const uniqueEventTypeNames = [...new Set(allEventTypeNames)];
|
|
|
22
22
|
const eventUnion = uniqueEventTypeNames.join(' | ');
|
|
23
23
|
|
|
24
24
|
const willHaveAnyAggregateStream = eventCommandPairs.some(pair => {
|
|
25
|
+
if (pair.streamPattern) return true;
|
|
25
26
|
const eventDef = messages.find(m => m.name === pair.eventType);
|
|
26
27
|
return states.some(state =>
|
|
27
28
|
findPrimitiveLinkingField(state.fields, eventDef?.fields || []) !== undefined
|
|
@@ -84,18 +85,43 @@ eachMessage: async (event): Promise<MessageHandlerResult> => {
|
|
|
84
85
|
<% } -%>
|
|
85
86
|
<% if (states.length > 0) {
|
|
86
87
|
let hasAggregateStream = false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
if (pair.streamPattern) {
|
|
89
|
+
hasAggregateStream = true;
|
|
90
|
+
const streamExpr = pair.streamPattern.replace(/\$\{([^}]+)\}/g, (_, key) => '${event.data.' + key + '}');
|
|
91
|
+
const varName = camelCase(states[0].type);
|
|
92
|
+
for (const state of states) {
|
|
92
93
|
for (const f of state.fields) {
|
|
93
94
|
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
94
95
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
96
|
+
}
|
|
97
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
98
|
+
!eventFieldSet.has(f.name) && states.some(s => s.fields.some(sf => sf.name === f.name))
|
|
99
|
+
);
|
|
100
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
101
|
+
-%>
|
|
102
|
+
|
|
103
|
+
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
104
|
+
`<%= streamExpr %>`,
|
|
105
|
+
{
|
|
106
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
107
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
// <%= states[0].type %> fields: <%= states.flatMap(s => s.fields).map(f => f.name).join(', ') %>
|
|
111
|
+
|
|
112
|
+
<% } else {
|
|
113
|
+
for (const state of states) {
|
|
114
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
115
|
+
if (linkingField) {
|
|
116
|
+
hasAggregateStream = true;
|
|
117
|
+
const varName = camelCase(state.type);
|
|
118
|
+
for (const f of state.fields) {
|
|
119
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
120
|
+
}
|
|
121
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
122
|
+
!eventFieldSet.has(f.name) && state.fields.some(sf => sf.name === f.name)
|
|
123
|
+
);
|
|
124
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
99
125
|
-%>
|
|
100
126
|
|
|
101
127
|
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
@@ -107,7 +133,8 @@ eachMessage: async (event): Promise<MessageHandlerResult> => {
|
|
|
107
133
|
);
|
|
108
134
|
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
109
135
|
|
|
110
|
-
<%
|
|
136
|
+
<% }
|
|
137
|
+
}
|
|
111
138
|
}
|
|
112
139
|
if (!hasAggregateStream) {
|
|
113
140
|
-%>
|
|
@@ -155,17 +182,41 @@ eachMessage: async (event): Promise<MessageHandlerResult> => {
|
|
|
155
182
|
// Command (<%= pair.commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
156
183
|
<% } -%>
|
|
157
184
|
<% if (states.length > 0) {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
if (pair.streamPattern) {
|
|
186
|
+
const streamExpr = pair.streamPattern.replace(/\$\{([^}]+)\}/g, (_, key) => '${event.data.' + key + '}');
|
|
187
|
+
const varName = camelCase(states[0].type);
|
|
188
|
+
for (const state of states) {
|
|
162
189
|
for (const f of state.fields) {
|
|
163
190
|
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
164
191
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)
|
|
168
|
-
|
|
192
|
+
}
|
|
193
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
194
|
+
!eventFieldSet.has(f.name) && states.some(s => s.fields.some(sf => sf.name === f.name))
|
|
195
|
+
);
|
|
196
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
197
|
+
-%>
|
|
198
|
+
|
|
199
|
+
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
200
|
+
`<%= streamExpr %>`,
|
|
201
|
+
{
|
|
202
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
203
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
// <%= states[0].type %> fields: <%= states.flatMap(s => s.fields).map(f => f.name).join(', ') %>
|
|
207
|
+
|
|
208
|
+
<% } else {
|
|
209
|
+
for (const state of states) {
|
|
210
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
211
|
+
if (linkingField) {
|
|
212
|
+
const varName = camelCase(state.type);
|
|
213
|
+
for (const f of state.fields) {
|
|
214
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
215
|
+
}
|
|
216
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
217
|
+
!eventFieldSet.has(f.name) && state.fields.some(sf => sf.name === f.name)
|
|
218
|
+
);
|
|
219
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
169
220
|
-%>
|
|
170
221
|
|
|
171
222
|
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
@@ -177,7 +228,8 @@ eachMessage: async (event): Promise<MessageHandlerResult> => {
|
|
|
177
228
|
);
|
|
178
229
|
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
179
230
|
|
|
180
|
-
<%
|
|
231
|
+
<% }
|
|
232
|
+
}
|
|
181
233
|
}
|
|
182
234
|
}
|
|
183
235
|
-%>
|
|
@@ -1008,4 +1008,143 @@ export type BarberNotified = Event<
|
|
|
1008
1008
|
"
|
|
1009
1009
|
`);
|
|
1010
1010
|
});
|
|
1011
|
+
|
|
1012
|
+
it('should use target command stream pattern for aggregateStream when destination exists', async () => {
|
|
1013
|
+
const spec: SpecsSchema = {
|
|
1014
|
+
variant: 'specs',
|
|
1015
|
+
scenes: [
|
|
1016
|
+
{
|
|
1017
|
+
name: 'booking scene',
|
|
1018
|
+
moments: [
|
|
1019
|
+
{
|
|
1020
|
+
type: 'command',
|
|
1021
|
+
name: 'accept booking',
|
|
1022
|
+
client: { specs: [] },
|
|
1023
|
+
server: {
|
|
1024
|
+
description: '',
|
|
1025
|
+
data: {
|
|
1026
|
+
items: [
|
|
1027
|
+
{
|
|
1028
|
+
target: { type: 'Event', name: 'BookingAccepted' },
|
|
1029
|
+
destination: {
|
|
1030
|
+
type: 'stream',
|
|
1031
|
+
pattern: 'bookings-${bookingId}',
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
},
|
|
1036
|
+
specs: [
|
|
1037
|
+
{
|
|
1038
|
+
type: 'gherkin',
|
|
1039
|
+
feature: 'Accept booking',
|
|
1040
|
+
rules: [
|
|
1041
|
+
{
|
|
1042
|
+
name: 'Should accept',
|
|
1043
|
+
examples: [
|
|
1044
|
+
{
|
|
1045
|
+
name: 'Booking accepted',
|
|
1046
|
+
steps: [
|
|
1047
|
+
{
|
|
1048
|
+
keyword: 'When',
|
|
1049
|
+
text: 'AcceptBooking',
|
|
1050
|
+
docString: { bookingId: 'b1', hostId: 'h1' },
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
keyword: 'Then',
|
|
1054
|
+
text: 'BookingAccepted',
|
|
1055
|
+
docString: { bookingId: 'b1', hostId: 'h1' },
|
|
1056
|
+
},
|
|
1057
|
+
],
|
|
1058
|
+
},
|
|
1059
|
+
],
|
|
1060
|
+
},
|
|
1061
|
+
],
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
type: 'react',
|
|
1068
|
+
name: 'notify guest of acceptance',
|
|
1069
|
+
server: {
|
|
1070
|
+
description: 'Notifies guest when booking accepted',
|
|
1071
|
+
specs: [
|
|
1072
|
+
{
|
|
1073
|
+
type: 'gherkin',
|
|
1074
|
+
feature: 'Notify guest reaction',
|
|
1075
|
+
rules: [
|
|
1076
|
+
{
|
|
1077
|
+
name: 'Should notify guest',
|
|
1078
|
+
examples: [
|
|
1079
|
+
{
|
|
1080
|
+
name: 'Guest notified',
|
|
1081
|
+
steps: [
|
|
1082
|
+
{
|
|
1083
|
+
keyword: 'Given',
|
|
1084
|
+
text: 'BookingState',
|
|
1085
|
+
docString: { bookingId: 'b1', guestEmail: 'guest@test.com' },
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
keyword: 'When',
|
|
1089
|
+
text: 'BookingAccepted',
|
|
1090
|
+
docString: { bookingId: 'b1', hostId: 'h1' },
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
keyword: 'Then',
|
|
1094
|
+
text: 'AcceptBooking',
|
|
1095
|
+
docString: { bookingId: 'b1', hostId: 'h1' },
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
],
|
|
1107
|
+
},
|
|
1108
|
+
],
|
|
1109
|
+
messages: [
|
|
1110
|
+
{
|
|
1111
|
+
type: 'command',
|
|
1112
|
+
name: 'AcceptBooking',
|
|
1113
|
+
fields: [
|
|
1114
|
+
{ name: 'bookingId', type: 'string', required: true },
|
|
1115
|
+
{ name: 'hostId', type: 'string', required: true },
|
|
1116
|
+
],
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
type: 'event',
|
|
1120
|
+
name: 'BookingAccepted',
|
|
1121
|
+
source: 'internal',
|
|
1122
|
+
fields: [
|
|
1123
|
+
{ name: 'bookingId', type: 'string', required: true },
|
|
1124
|
+
{ name: 'hostId', type: 'string', required: true },
|
|
1125
|
+
],
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
type: 'state',
|
|
1129
|
+
name: 'BookingState',
|
|
1130
|
+
fields: [
|
|
1131
|
+
{ name: 'bookingId', type: 'string', required: true },
|
|
1132
|
+
{ name: 'guestEmail', type: 'string', required: true },
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
],
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
1139
|
+
const reactFile = plans.find((p) => p.outputPath.endsWith('notify-guest-of-acceptance/react.ts'));
|
|
1140
|
+
|
|
1141
|
+
expect(reactFile?.contents).toContain('`bookings-${event.data.bookingId}`');
|
|
1142
|
+
expect(reactFile?.contents).toContain('aggregateStream');
|
|
1143
|
+
expect(reactFile?.contents).not.toContain('BookingState-${event.data');
|
|
1144
|
+
|
|
1145
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('notify-guest-of-acceptance/react.specs.ts'));
|
|
1146
|
+
|
|
1147
|
+
expect(specFile?.contents).toContain("'bookings-b1'");
|
|
1148
|
+
expect(specFile?.contents).not.toContain('BookingState-');
|
|
1149
|
+
});
|
|
1011
1150
|
});
|
|
@@ -59,17 +59,41 @@ async (event: <%= pascalCase(pair.eventType) %>) => {
|
|
|
59
59
|
// Command (<%= pair.commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
60
60
|
<% } -%>
|
|
61
61
|
<% if (states.length > 0) {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
if (pair.streamPattern) {
|
|
63
|
+
const streamExpr = pair.streamPattern.replace(/\$\{([^}]+)\}/g, (_, key) => '${event.data.' + key + '}');
|
|
64
|
+
const varName = camelCase(states[0].type);
|
|
65
|
+
for (const state of states) {
|
|
66
66
|
for (const f of state.fields) {
|
|
67
67
|
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
|
|
69
|
+
}
|
|
70
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
71
|
+
!eventFieldSet.has(f.name) && states.some(s => s.fields.some(sf => sf.name === f.name))
|
|
72
|
+
);
|
|
73
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
74
|
+
-%>
|
|
75
|
+
|
|
76
|
+
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
77
|
+
`<%= streamExpr %>`,
|
|
78
|
+
{
|
|
79
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
80
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
// <%= states[0].type %> fields: <%= states.flatMap(s => s.fields).map(f => f.name).join(', ') %>
|
|
84
|
+
|
|
85
|
+
<% } else {
|
|
86
|
+
for (const state of states) {
|
|
87
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
88
|
+
if (linkingField) {
|
|
89
|
+
const varName = camelCase(state.type);
|
|
90
|
+
for (const f of state.fields) {
|
|
91
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
92
|
+
}
|
|
93
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
94
|
+
!eventFieldSet.has(f.name) && state.fields.some(sf => sf.name === f.name)
|
|
95
|
+
);
|
|
96
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
73
97
|
-%>
|
|
74
98
|
|
|
75
99
|
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
@@ -81,7 +105,8 @@ async (event: <%= pascalCase(pair.eventType) %>) => {
|
|
|
81
105
|
);
|
|
82
106
|
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
83
107
|
|
|
84
|
-
<%
|
|
108
|
+
<% }
|
|
109
|
+
}
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
-%>
|