@auto-engineer/server-generator-apollo-emmett 1.88.0 → 1.90.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 +90 -0
- package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
- package/dist/src/codegen/extract/slice-normalizer.js +14 -0
- package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +17 -0
- 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 +6 -4
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +293 -34
- package/dist/src/codegen/templates/command/decide.specs.ts +34 -14
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
- package/dist/src/codegen/templates/command/decide.ts.ejs +32 -4
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +124 -0
- package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
- package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/dist/src/codegen/templates/react/react.specs.ts +9 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
- package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/dist/src/codegen/templates/react/register.specs.ts +27 -23
- package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +12 -3
- package/package.json +4 -4
- package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
- package/src/codegen/extract/slice-normalizer.ts +15 -0
- package/src/codegen/extract/type-helpers.specs.ts +77 -1
- package/src/codegen/extract/type-helpers.ts +23 -0
- package/src/codegen/formatTsValueSimple.specs.ts +8 -0
- package/src/codegen/scaffoldFromSchema.ts +7 -3
- package/src/codegen/templates/command/decide.specs.specs.ts +293 -34
- package/src/codegen/templates/command/decide.specs.ts +34 -14
- package/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
- package/src/codegen/templates/command/decide.ts.ejs +32 -4
- package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
- package/src/codegen/templates/query/projection.specs.specs.ts +124 -0
- package/src/codegen/templates/query/projection.specs.ts +20 -0
- package/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
- package/src/codegen/templates/query/projection.ts.ejs +5 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
- package/src/codegen/templates/react/react.specs.specs.ts +115 -0
- package/src/codegen/templates/react/react.specs.ts +9 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
- package/src/codegen/templates/react/react.ts.ejs +22 -9
- package/src/codegen/templates/react/react.ts.specs.ts +253 -0
- package/src/codegen/templates/react/register.specs.ts +27 -23
- package/src/codegen/templates/react/register.ts.ejs +5 -1
- package/dist/src/codegen/extract/graphql.d.ts +0 -14
- package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
- package/dist/src/codegen/extract/graphql.js +0 -81
- package/dist/src/codegen/extract/graphql.js.map +0 -1
- package/src/codegen/extract/graphql.ts +0 -103
|
@@ -64,8 +64,8 @@ case '<%= command %>': {
|
|
|
64
64
|
<% if (integrationReturnType) { -%>
|
|
65
65
|
* - Use `<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
|
|
66
66
|
<% } -%>
|
|
67
|
-
* - If invalid, throw one of the following domain errors: `
|
|
68
|
-
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }
|
|
67
|
+
* - If invalid, throw one of the following domain errors: `IllegalStateError`<% if (usedErrors.includes('ValidationError')) { %>, `ValidationError`<% } %><% if (usedErrors.includes('NotFoundError')) { %>, `NotFoundError`<% } %>
|
|
68
|
+
* ⚠️ Error constructors: IllegalStateError takes a string message<% if (usedErrors.includes('ValidationError')) { %>, ValidationError takes a string message<% } %><% if (usedErrors.includes('NotFoundError')) { %>, NotFoundError takes { id, type, message? }<% } %>
|
|
69
69
|
* - If valid, return one or more events with the correct structure
|
|
70
70
|
*
|
|
71
71
|
* ⚠️ Only read from inputs — never mutate them. `evolve.ts` handles state updates.
|
|
@@ -95,10 +95,38 @@ throw new <%= error.errorType %>('<%- error.message ?? 'Validation failed' %>');
|
|
|
95
95
|
}
|
|
96
96
|
<% } } -%>
|
|
97
97
|
|
|
98
|
+
<%
|
|
99
|
+
const cmdFieldSet = new Set((commandSchemasByName?.[command]?.fields || []).map(f => f.name));
|
|
100
|
+
const nonCommandFields = [];
|
|
101
|
+
for (const scenario of scenarios) {
|
|
102
|
+
for (const t of scenario.then.filter(t => 'eventRef' in t)) {
|
|
103
|
+
const eventMsg = (messages || []).find(m => m.name === t.eventRef && m.type === 'event');
|
|
104
|
+
for (const f of (eventMsg?.fields || [])) {
|
|
105
|
+
if (!cmdFieldSet.has(f.name) && !nonCommandFields.some(e => e.name === f.name)) {
|
|
106
|
+
nonCommandFields.push(f);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
-%>
|
|
112
|
+
<% if (nonCommandFields.length > 0) { -%>
|
|
113
|
+
// ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
|
|
114
|
+
// Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
|
|
115
|
+
// Do NOT use 'as <%= fallbackEventTypes[0] ? pascalCase(fallbackEventTypes[0]) : 'EventType' %>' to silence missing fields.
|
|
116
|
+
//
|
|
117
|
+
// Fields from command input → use ...command.data or command.data.<fieldName>
|
|
118
|
+
// Fields NOT in command input → produce dynamically (never hardcode):
|
|
119
|
+
<% for (const f of nonCommandFields) { -%>
|
|
120
|
+
// <%= f.name %>: <%= f.type || f.tsType %> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
|
|
121
|
+
<% } -%>
|
|
122
|
+
<% } else { -%>
|
|
123
|
+
// All event fields come from command input — use ...command.data to pass them through.
|
|
124
|
+
<% } -%>
|
|
125
|
+
|
|
98
126
|
// return {
|
|
99
127
|
// type: '<%= fallbackEventTypes[0] ?? 'TODO_EVENT_TYPE' %>',
|
|
100
|
-
// data: { ...command.data },
|
|
101
|
-
// }
|
|
128
|
+
// data: { ...command.data<%= nonCommandFields.length > 0 ? `, /* + dynamically produce: ${nonCommandFields.map(f => f.name).join(', ')} */` : '' %> },
|
|
129
|
+
// };
|
|
102
130
|
|
|
103
131
|
throw new IllegalStateError('Not yet implemented: ' + command.type);
|
|
104
132
|
}
|
|
@@ -150,7 +150,7 @@ describe('mutation.resolver.ts.ejs', () => {
|
|
|
150
150
|
specs: [],
|
|
151
151
|
},
|
|
152
152
|
request:
|
|
153
|
-
'mutation AnswerQuestion($input: AnswerQuestionInput!) {
|
|
153
|
+
'mutation AnswerQuestion($input: AnswerQuestionInput!) {\n answerQuestion(input: $input) {\n success\n }\n}',
|
|
154
154
|
server: {
|
|
155
155
|
description: '',
|
|
156
156
|
data: {
|
|
@@ -549,4 +549,75 @@ export class AddItemsToCartResolver {
|
|
|
549
549
|
"
|
|
550
550
|
`);
|
|
551
551
|
});
|
|
552
|
+
|
|
553
|
+
it('uses parsedRequest operationName when it differs from camelCase(cmd.type)', async () => {
|
|
554
|
+
const spec: SpecsSchema = {
|
|
555
|
+
variant: 'specs',
|
|
556
|
+
narratives: [
|
|
557
|
+
{
|
|
558
|
+
name: 'Questionnaires',
|
|
559
|
+
slices: [
|
|
560
|
+
{
|
|
561
|
+
name: 'submit answer',
|
|
562
|
+
type: 'command',
|
|
563
|
+
client: { specs: [] },
|
|
564
|
+
request:
|
|
565
|
+
'mutation SubmitAnswer($input: SubmitAnswerInput!) {\n submitQuestionnaireAnswer(input: $input) {\n success\n }\n}',
|
|
566
|
+
server: {
|
|
567
|
+
description: '',
|
|
568
|
+
data: {
|
|
569
|
+
items: [
|
|
570
|
+
{
|
|
571
|
+
target: { type: 'Event', name: 'AnswerSubmitted' },
|
|
572
|
+
destination: { type: 'stream', pattern: 'questionnaire-participantId' },
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
},
|
|
576
|
+
specs: [
|
|
577
|
+
{
|
|
578
|
+
type: 'gherkin',
|
|
579
|
+
feature: '',
|
|
580
|
+
rules: [
|
|
581
|
+
{
|
|
582
|
+
name: 'submit answer',
|
|
583
|
+
examples: [
|
|
584
|
+
{
|
|
585
|
+
name: 'happy path',
|
|
586
|
+
steps: [
|
|
587
|
+
{
|
|
588
|
+
keyword: 'When',
|
|
589
|
+
text: 'SubmitAnswer',
|
|
590
|
+
docString: { participantId: 'p-1', answer: 'Yes' },
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
messages: [
|
|
605
|
+
{
|
|
606
|
+
type: 'command',
|
|
607
|
+
name: 'SubmitAnswer',
|
|
608
|
+
fields: [
|
|
609
|
+
{ name: 'participantId', type: 'string', required: true },
|
|
610
|
+
{ name: 'answer', type: 'string', required: true },
|
|
611
|
+
],
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
617
|
+
const mutationFile = plans.find(
|
|
618
|
+
(p) => p.outputPath.endsWith('mutation.resolver.ts') && p.contents.includes('export class SubmitAnswerResolver'),
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
expect(mutationFile?.contents).toContain('async submitQuestionnaireAnswer(');
|
|
622
|
+
});
|
|
552
623
|
});
|
|
@@ -82,7 +82,7 @@ if (isInlineObjectArray(tsType)) { %>
|
|
|
82
82
|
@Resolver()
|
|
83
83
|
export class <%= pascalCase(cmd.type) %>Resolver {
|
|
84
84
|
@Mutation(() => MutationResponse)
|
|
85
|
-
async <%= camelCase(cmd.type) %>(
|
|
85
|
+
async <%= parsedRequest?.operationName ?? camelCase(cmd.type) %>(
|
|
86
86
|
@Arg('input', () => <%= pascalCase(cmd.type) %>Input) input: <%= pascalCase(cmd.type) %>Input,
|
|
87
87
|
@Ctx() ctx: GraphQLContext,
|
|
88
88
|
): Promise<MutationResponse> {
|
|
@@ -1567,6 +1567,11 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
1567
1567
|
case 'WorkoutRecorded': {
|
|
1568
1568
|
/**
|
|
1569
1569
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
1570
|
+
*
|
|
1571
|
+
* Derive ALL field values from event.data or existing document state.
|
|
1572
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
1573
|
+
* Preserve all import paths above — they are generated from the model.
|
|
1574
|
+
*
|
|
1570
1575
|
* Implement how this event updates the projection.
|
|
1571
1576
|
*
|
|
1572
1577
|
* **Internal State Pattern (extends, never replaces the imported type):**
|
|
@@ -2094,6 +2099,125 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
2094
2099
|
expect(specFile?.contents).not.toContain('"[{\\"appointmentId\\"');
|
|
2095
2100
|
});
|
|
2096
2101
|
|
|
2102
|
+
it('should serialize array values correctly when event field is missing from schema', async () => {
|
|
2103
|
+
const spec: SpecsSchema = {
|
|
2104
|
+
variant: 'specs',
|
|
2105
|
+
narratives: [
|
|
2106
|
+
{
|
|
2107
|
+
name: 'fitness-flow',
|
|
2108
|
+
slices: [
|
|
2109
|
+
{
|
|
2110
|
+
type: 'command',
|
|
2111
|
+
name: 'update-leaderboard',
|
|
2112
|
+
stream: 'leaderboard-${week}',
|
|
2113
|
+
client: { specs: [] },
|
|
2114
|
+
server: {
|
|
2115
|
+
description: '',
|
|
2116
|
+
specs: [
|
|
2117
|
+
{
|
|
2118
|
+
type: 'gherkin',
|
|
2119
|
+
feature: 'Update leaderboard',
|
|
2120
|
+
rules: [
|
|
2121
|
+
{
|
|
2122
|
+
name: 'Should update leaderboard',
|
|
2123
|
+
examples: [
|
|
2124
|
+
{
|
|
2125
|
+
name: 'Leaderboard updated',
|
|
2126
|
+
steps: [
|
|
2127
|
+
{
|
|
2128
|
+
keyword: 'When',
|
|
2129
|
+
text: 'UpdateLeaderboard',
|
|
2130
|
+
docString: { week: 'week-1' },
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
keyword: 'Then',
|
|
2134
|
+
text: 'LeaderboardUpdated',
|
|
2135
|
+
docString: { week: 'week-1', rankings: [{ memberId: 'm1', points: 100 }] },
|
|
2136
|
+
},
|
|
2137
|
+
],
|
|
2138
|
+
},
|
|
2139
|
+
],
|
|
2140
|
+
},
|
|
2141
|
+
],
|
|
2142
|
+
},
|
|
2143
|
+
],
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
{
|
|
2147
|
+
type: 'query',
|
|
2148
|
+
name: 'view-leaderboard',
|
|
2149
|
+
stream: 'leaderboard',
|
|
2150
|
+
client: { specs: [] },
|
|
2151
|
+
server: {
|
|
2152
|
+
description: '',
|
|
2153
|
+
data: {
|
|
2154
|
+
items: [
|
|
2155
|
+
{
|
|
2156
|
+
target: { type: 'State', name: 'LeaderboardStats' },
|
|
2157
|
+
origin: { type: 'projection', name: 'LeaderboardProjection', idField: 'week' },
|
|
2158
|
+
},
|
|
2159
|
+
],
|
|
2160
|
+
},
|
|
2161
|
+
specs: [
|
|
2162
|
+
{
|
|
2163
|
+
type: 'gherkin',
|
|
2164
|
+
feature: 'View leaderboard',
|
|
2165
|
+
rules: [
|
|
2166
|
+
{
|
|
2167
|
+
name: 'Should show leaderboard',
|
|
2168
|
+
examples: [
|
|
2169
|
+
{
|
|
2170
|
+
name: 'Shows rankings after update',
|
|
2171
|
+
steps: [
|
|
2172
|
+
{
|
|
2173
|
+
keyword: 'When',
|
|
2174
|
+
text: 'LeaderboardUpdated',
|
|
2175
|
+
docString: { week: 'week-1', rankings: [{ memberId: 'm1', points: 100 }] },
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
keyword: 'Then',
|
|
2179
|
+
text: 'LeaderboardStats',
|
|
2180
|
+
docString: { week: 'week-1', rankings: [{ memberId: 'm1', points: 100 }] },
|
|
2181
|
+
},
|
|
2182
|
+
],
|
|
2183
|
+
},
|
|
2184
|
+
],
|
|
2185
|
+
},
|
|
2186
|
+
],
|
|
2187
|
+
},
|
|
2188
|
+
],
|
|
2189
|
+
},
|
|
2190
|
+
},
|
|
2191
|
+
],
|
|
2192
|
+
},
|
|
2193
|
+
],
|
|
2194
|
+
messages: [
|
|
2195
|
+
{
|
|
2196
|
+
type: 'command',
|
|
2197
|
+
name: 'UpdateLeaderboard',
|
|
2198
|
+
fields: [{ name: 'week', type: 'string', required: true }],
|
|
2199
|
+
},
|
|
2200
|
+
{
|
|
2201
|
+
type: 'event',
|
|
2202
|
+
name: 'LeaderboardUpdated',
|
|
2203
|
+
source: 'internal',
|
|
2204
|
+
fields: [{ name: 'week', type: 'string', required: true }],
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
type: 'state',
|
|
2208
|
+
name: 'LeaderboardStats',
|
|
2209
|
+
fields: [{ name: 'week', type: 'string', required: true }],
|
|
2210
|
+
},
|
|
2211
|
+
],
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
2215
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('view-leaderboard/projection.specs.ts'));
|
|
2216
|
+
|
|
2217
|
+
expect(specFile?.contents).not.toContain('[object Object]');
|
|
2218
|
+
expect(specFile?.contents).toContain('week');
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2097
2221
|
it('should resolve idField value from Given/When events when not in Then state data', async () => {
|
|
2098
2222
|
const spec: SpecsSchema = {
|
|
2099
2223
|
variant: 'specs',
|
|
@@ -239,6 +239,11 @@ describe('projection.ts.ejs', () => {
|
|
|
239
239
|
case 'ListingCreated': {
|
|
240
240
|
/**
|
|
241
241
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
242
|
+
*
|
|
243
|
+
* Derive ALL field values from event.data or existing document state.
|
|
244
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
245
|
+
* Preserve all import paths above — they are generated from the model.
|
|
246
|
+
*
|
|
242
247
|
* Implement how this event updates the projection.
|
|
243
248
|
*
|
|
244
249
|
* **Internal State Pattern (extends, never replaces the imported type):**
|
|
@@ -273,6 +278,11 @@ describe('projection.ts.ejs', () => {
|
|
|
273
278
|
case 'ListingRemoved': {
|
|
274
279
|
/**
|
|
275
280
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
281
|
+
*
|
|
282
|
+
* Derive ALL field values from event.data or existing document state.
|
|
283
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
284
|
+
* Preserve all import paths above — they are generated from the model.
|
|
285
|
+
*
|
|
276
286
|
* This event might indicate removal of a AvailableListings.
|
|
277
287
|
*
|
|
278
288
|
* - If the intent is to **remove the document**, return \`null\`.
|
|
@@ -568,6 +578,11 @@ describe('projection.ts.ejs', () => {
|
|
|
568
578
|
case 'TodoAdded': {
|
|
569
579
|
/**
|
|
570
580
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
581
|
+
*
|
|
582
|
+
* Derive ALL field values from event.data or existing document state.
|
|
583
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
584
|
+
* Preserve all import paths above — they are generated from the model.
|
|
585
|
+
*
|
|
571
586
|
* **SINGLETON AGGREGATION PATTERN**
|
|
572
587
|
*
|
|
573
588
|
* This projection maintains ONE document aggregating data from MANY entities.
|
|
@@ -784,6 +799,11 @@ describe('projection.ts.ejs', () => {
|
|
|
784
799
|
case 'UserJoinedProject': {
|
|
785
800
|
/**
|
|
786
801
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
802
|
+
*
|
|
803
|
+
* Derive ALL field values from event.data or existing document state.
|
|
804
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
805
|
+
* Preserve all import paths above — they are generated from the model.
|
|
806
|
+
*
|
|
787
807
|
* **COMPOSITE KEY PROJECTION**
|
|
788
808
|
*
|
|
789
809
|
* This projection uses a composite key: userId + projectId
|
|
@@ -35,7 +35,7 @@ function isQueryAction(whenText, queryName) {
|
|
|
35
35
|
|
|
36
36
|
function formatSpecValue(value, tsType) {
|
|
37
37
|
if (value === null || value === undefined) return 'null';
|
|
38
|
-
if (tsType === 'string' || tsType === 'ID') return `'${value}'`;
|
|
38
|
+
if ((tsType === 'string' || tsType === 'ID') && typeof value === 'string') return `'${value}'`;
|
|
39
39
|
if (tsType === 'number' || tsType === 'boolean') return String(value);
|
|
40
40
|
if (tsType === 'Date') return `new Date('${value}')`;
|
|
41
41
|
if (Array.isArray(value)) {
|
|
@@ -60,6 +60,10 @@ function formatSpecValue(value, tsType) {
|
|
|
60
60
|
});
|
|
61
61
|
return `{ ${entries.join(', ')} }`;
|
|
62
62
|
}
|
|
63
|
+
const entries = Object.entries(value).map(([k, v]) =>
|
|
64
|
+
`${k}: ${formatSpecValue(v, typeof v)}`
|
|
65
|
+
);
|
|
66
|
+
return `{ ${entries.join(', ')} }`;
|
|
63
67
|
}
|
|
64
68
|
if (typeof value === 'string' && (tsType.includes('[]') || tsType.startsWith('Array<'))) {
|
|
65
69
|
try {
|
|
@@ -120,6 +120,11 @@ switch (event.type) {
|
|
|
120
120
|
case '<%= event.type %>': {
|
|
121
121
|
/**
|
|
122
122
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
123
|
+
*
|
|
124
|
+
* Derive ALL field values from event.data or existing document state.
|
|
125
|
+
* NEVER hardcode constant values — every output field must trace to an input.
|
|
126
|
+
* Preserve all import paths above — they are generated from the model.
|
|
127
|
+
*
|
|
123
128
|
<% if (isSingleton) { -%>
|
|
124
129
|
* **SINGLETON AGGREGATION PATTERN**
|
|
125
130
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const target = slice?.server?.data?.items?.[0]?.target;
|
|
3
3
|
const projection = slice?.server?.data?.items?.[0]?.origin;
|
|
4
4
|
const isSingleton = projection?.singleton === true;
|
|
5
|
-
const queryName = parsedRequest?.
|
|
5
|
+
const queryName = parsedRequest?.operationName ?? camelCase(sliceName);
|
|
6
6
|
const viewType = target?.name ? pascalCase(target.name) : 'UnknownView';
|
|
7
7
|
const collectionName = projectionName || 'unknown-collection';
|
|
8
8
|
const message = messages?.find(m => m.name === viewType);
|
|
@@ -369,6 +369,121 @@ describe('react.specs.ts.ejs (react slice)', () => {
|
|
|
369
369
|
expect(specFile?.contents).not.toContain('tipAmount: "5.00"');
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
+
it('should exclude event And-steps from ReactorCommand type and then assertions', async () => {
|
|
373
|
+
const spec: SpecsSchema = {
|
|
374
|
+
variant: 'specs',
|
|
375
|
+
narratives: [
|
|
376
|
+
{
|
|
377
|
+
name: 'notification flow',
|
|
378
|
+
slices: [
|
|
379
|
+
{
|
|
380
|
+
type: 'command',
|
|
381
|
+
name: 'earn points',
|
|
382
|
+
client: { specs: [] },
|
|
383
|
+
server: {
|
|
384
|
+
description: '',
|
|
385
|
+
specs: [
|
|
386
|
+
{
|
|
387
|
+
type: 'gherkin',
|
|
388
|
+
feature: 'Earn points',
|
|
389
|
+
rules: [
|
|
390
|
+
{
|
|
391
|
+
name: 'Should earn',
|
|
392
|
+
examples: [
|
|
393
|
+
{
|
|
394
|
+
name: 'Points earned',
|
|
395
|
+
steps: [
|
|
396
|
+
{ keyword: 'When', text: 'EarnPoints', docString: { memberId: 'm1', amount: 10 } },
|
|
397
|
+
{ keyword: 'Then', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
type: 'react',
|
|
409
|
+
name: 'notify milestone',
|
|
410
|
+
server: {
|
|
411
|
+
description: 'Notifies on milestone',
|
|
412
|
+
data: {
|
|
413
|
+
items: [
|
|
414
|
+
{
|
|
415
|
+
target: { type: 'Command', name: 'SendNotification' },
|
|
416
|
+
destination: { type: 'stream', pattern: 'notif-${memberId}' },
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
specs: [
|
|
421
|
+
{
|
|
422
|
+
type: 'gherkin',
|
|
423
|
+
feature: 'Milestone notification',
|
|
424
|
+
rules: [
|
|
425
|
+
{
|
|
426
|
+
name: 'Should notify',
|
|
427
|
+
examples: [
|
|
428
|
+
{
|
|
429
|
+
name: 'Milestone reached',
|
|
430
|
+
steps: [
|
|
431
|
+
{ keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', amount: 10 } },
|
|
432
|
+
{ keyword: 'Then', text: 'MilestoneNotified', docString: { memberId: 'm1' } },
|
|
433
|
+
{ keyword: 'And', text: 'SendNotification', docString: { memberId: 'm1' } },
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
messages: [
|
|
447
|
+
{
|
|
448
|
+
type: 'command',
|
|
449
|
+
name: 'EarnPoints',
|
|
450
|
+
fields: [
|
|
451
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
452
|
+
{ name: 'amount', type: 'number', required: true },
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: 'event',
|
|
457
|
+
name: 'PointsEarned',
|
|
458
|
+
source: 'internal',
|
|
459
|
+
fields: [
|
|
460
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
461
|
+
{ name: 'amount', type: 'number', required: true },
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
type: 'command',
|
|
466
|
+
name: 'SendNotification',
|
|
467
|
+
fields: [{ name: 'memberId', type: 'string', required: true }],
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
type: 'event',
|
|
471
|
+
name: 'MilestoneNotified',
|
|
472
|
+
source: 'internal',
|
|
473
|
+
fields: [{ name: 'memberId', type: 'string', required: true }],
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
479
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('notify-milestone/react.specs.ts'));
|
|
480
|
+
|
|
481
|
+
expect(specFile?.contents).toContain('type ReactorCommand = SendNotification;');
|
|
482
|
+
expect(specFile?.contents).not.toContain('MilestoneNotified');
|
|
483
|
+
expect(specFile?.contents).toContain("type: 'SendNotification'");
|
|
484
|
+
expect(specFile?.contents).toContain('.then({');
|
|
485
|
+
});
|
|
486
|
+
|
|
372
487
|
it('should seed event store with Given state data via appendToStream', async () => {
|
|
373
488
|
const spec: SpecsSchema = {
|
|
374
489
|
variant: 'specs',
|
|
@@ -248,8 +248,15 @@ describe('handle.ts.ejs (react slice)', () => {
|
|
|
248
248
|
/**
|
|
249
249
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
250
250
|
*
|
|
251
|
-
*
|
|
252
|
-
* -
|
|
251
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
252
|
+
* - \`event.data.<field>\` → already wired from the triggering event.
|
|
253
|
+
* - \`<stateVar>.<field>\` → already wired from loaded aggregate state.
|
|
254
|
+
* - \`undefined, // TODO: source unknown\` → MUST be dynamically derived:
|
|
255
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
256
|
+
* NEVER hardcode values copied from test assertions.
|
|
257
|
+
*
|
|
258
|
+
* Preserve all import paths above — they are generated from the model.
|
|
259
|
+
* Add business logic (validation, conditional sends) as needed.
|
|
253
260
|
*/
|
|
254
261
|
|
|
255
262
|
// Event (BookingRequested) fields: bookingId: string, hostId: string, message: string
|
|
@@ -122,9 +122,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
122
122
|
for (const gs of givenStates) {
|
|
123
123
|
const stateSchema = states.find(s => s.type === gs.eventRef);
|
|
124
124
|
const eventDef = messages.find(m => m.name === exampleEvent.eventRef);
|
|
125
|
-
const
|
|
126
|
-
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
127
|
-
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
125
|
+
const linkingField = findPrimitiveLinkingField(stateSchema?.fields || [], eventDef?.fields || []);
|
|
128
126
|
if (linkingField) {
|
|
129
127
|
const linkingValue = gs.exampleData[linkingField];
|
|
130
128
|
-%>
|
|
@@ -39,15 +39,30 @@ connectionOptions: {
|
|
|
39
39
|
database,
|
|
40
40
|
},
|
|
41
41
|
eachMessage: async (event, context): Promise<MessageHandlerResult> => {
|
|
42
|
+
<%
|
|
43
|
+
const eventDef = messages.find(m => m.name === eventType);
|
|
44
|
+
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
45
|
+
const willHaveAggregateStream = states.some(state =>
|
|
46
|
+
findPrimitiveLinkingField(state.fields, eventDef?.fields || []) !== undefined
|
|
47
|
+
);
|
|
48
|
+
-%>
|
|
42
49
|
/**
|
|
43
50
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
44
51
|
*
|
|
45
|
-
*
|
|
46
|
-
* -
|
|
52
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
53
|
+
* - `event.data.<field>` → already wired from the triggering event.
|
|
54
|
+
* - `<stateVar>.<field>` → already wired from loaded aggregate state.
|
|
55
|
+
* - `undefined, // TODO: source unknown` → MUST be dynamically derived:
|
|
56
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
57
|
+
* NEVER hardcode values copied from test assertions.
|
|
58
|
+
*
|
|
59
|
+
* Preserve all import paths above — they are generated from the model.
|
|
60
|
+
<% if (willHaveAggregateStream) { -%>
|
|
61
|
+
* Do NOT modify or remove aggregateStream calls — they load required state.
|
|
62
|
+
<% } -%>
|
|
63
|
+
* Add business logic (validation, conditional sends) as needed.
|
|
47
64
|
*/
|
|
48
65
|
<%
|
|
49
|
-
const eventDef = messages.find(m => m.name === eventType);
|
|
50
|
-
const commandDef = messages.find(m => m.name === commandType && m.type === 'command');
|
|
51
66
|
const commandFields = (commandDef?.fields || []);
|
|
52
67
|
const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
|
|
53
68
|
const stateFieldSources = {};
|
|
@@ -61,9 +76,7 @@ const stateFieldSources = {};
|
|
|
61
76
|
<% if (states.length > 0) {
|
|
62
77
|
let hasAggregateStream = false;
|
|
63
78
|
for (const state of states) {
|
|
64
|
-
const
|
|
65
|
-
const eventFieldNames = (eventDef?.fields || []).map(f => f.name);
|
|
66
|
-
const linkingField = stateFieldNames.find(f => eventFieldNames.includes(f));
|
|
79
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
67
80
|
if (linkingField) {
|
|
68
81
|
hasAggregateStream = true;
|
|
69
82
|
const varName = camelCase(state.type);
|
|
@@ -75,8 +88,8 @@ const stateFieldSources = {};
|
|
|
75
88
|
const { state: <%= varName %> } = await eventStore.aggregateStream(
|
|
76
89
|
'<%= state.type %>-' + event.data.<%= linkingField %>,
|
|
77
90
|
{
|
|
78
|
-
evolve: (currentState, evt) => ({ ...currentState, ...evt.data }),
|
|
79
|
-
initialState: () => ({}),
|
|
91
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
92
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
80
93
|
},
|
|
81
94
|
);
|
|
82
95
|
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|