@auto-engineer/server-generator-apollo-emmett 1.87.0 → 1.89.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.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +97 -0
  5. package/dist/src/codegen/extract/messages.d.ts +1 -1
  6. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  7. package/dist/src/codegen/extract/projection.d.ts +1 -1
  8. package/dist/src/codegen/extract/projection.d.ts.map +1 -1
  9. package/dist/src/codegen/extract/projection.js +14 -1
  10. package/dist/src/codegen/extract/projection.js.map +1 -1
  11. package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
  12. package/dist/src/codegen/extract/slice-normalizer.js +14 -0
  13. package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
  14. package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
  15. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
  16. package/dist/src/codegen/extract/type-helpers.js +17 -0
  17. package/dist/src/codegen/extract/type-helpers.js.map +1 -1
  18. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  19. package/dist/src/codegen/scaffoldFromSchema.js +6 -4
  20. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  21. package/dist/src/codegen/templates/command/decide.specs.specs.ts +599 -34
  22. package/dist/src/codegen/templates/command/decide.specs.ts +38 -18
  23. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +65 -6
  24. package/dist/src/codegen/templates/command/decide.ts.ejs +33 -5
  25. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  26. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  27. package/dist/src/codegen/templates/query/projection.specs.specs.ts +298 -1
  28. package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
  29. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +43 -13
  30. package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
  31. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  32. package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
  33. package/dist/src/codegen/templates/react/react.specs.ts +9 -2
  34. package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  35. package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
  36. package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
  37. package/dist/src/codegen/templates/react/register.specs.ts +27 -23
  38. package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/ketchup-plan.md +14 -1
  41. package/package.json +4 -4
  42. package/src/codegen/extract/messages.ts +1 -1
  43. package/src/codegen/extract/projection.ts +13 -3
  44. package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
  45. package/src/codegen/extract/slice-normalizer.ts +15 -0
  46. package/src/codegen/extract/type-helpers.specs.ts +77 -1
  47. package/src/codegen/extract/type-helpers.ts +23 -0
  48. package/src/codegen/formatTsValueSimple.specs.ts +8 -0
  49. package/src/codegen/scaffoldFromSchema.ts +8 -4
  50. package/src/codegen/templates/command/decide.specs.specs.ts +599 -34
  51. package/src/codegen/templates/command/decide.specs.ts +38 -18
  52. package/src/codegen/templates/command/decide.specs.ts.ejs +65 -6
  53. package/src/codegen/templates/command/decide.ts.ejs +33 -5
  54. package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  55. package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  56. package/src/codegen/templates/query/projection.specs.specs.ts +298 -1
  57. package/src/codegen/templates/query/projection.specs.ts +20 -0
  58. package/src/codegen/templates/query/projection.specs.ts.ejs +43 -13
  59. package/src/codegen/templates/query/projection.ts.ejs +5 -0
  60. package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  61. package/src/codegen/templates/react/react.specs.specs.ts +115 -0
  62. package/src/codegen/templates/react/react.specs.ts +9 -2
  63. package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  64. package/src/codegen/templates/react/react.ts.ejs +22 -9
  65. package/src/codegen/templates/react/react.ts.specs.ts +253 -0
  66. package/src/codegen/templates/react/register.specs.ts +27 -23
  67. package/src/codegen/templates/react/register.ts.ejs +5 -1
  68. package/dist/src/codegen/extract/graphql.d.ts +0 -14
  69. package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
  70. package/dist/src/codegen/extract/graphql.js +0 -81
  71. package/dist/src/codegen/extract/graphql.js.map +0 -1
  72. package/src/codegen/extract/graphql.ts +0 -103
@@ -96,9 +96,9 @@ describe('decide.ts.ejs', () => {
96
96
  *
97
97
  * You should:
98
98
  * - Validate the command input fields
99
- * - Inspect the current domain \`state\` to determine if the command is allowed
100
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
101
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
99
+ * - Inspect the current domain \`_state\` to determine if the command is allowed
100
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
101
+ * ⚠️ Error constructors: IllegalStateError takes a string message
102
102
  * - If valid, return one or more events with the correct structure
103
103
  *
104
104
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -107,10 +107,12 @@ describe('decide.ts.ejs', () => {
107
107
  * - Should create listing with valid data
108
108
  */
109
109
 
110
+ // All event fields come from command input — use ...command.data to pass them through.
111
+
110
112
  // return {
111
113
  // type: 'ListingCreated',
112
114
  // data: { ...command.data },
113
- // } as ListingCreated;
115
+ // };
114
116
 
115
117
  throw new IllegalStateError('Not yet implemented: ' + command.type);
116
118
  }
@@ -229,9 +231,9 @@ describe('decide.ts.ejs', () => {
229
231
  *
230
232
  * You should:
231
233
  * - Validate the command input fields
232
- * - Inspect the current domain \`state\` to determine if the command is allowed
233
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
234
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
234
+ * - Inspect the current domain \`_state\` to determine if the command is allowed
235
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
236
+ * ⚠️ Error constructors: IllegalStateError takes a string message
235
237
  * - If valid, return one or more events with the correct structure
236
238
  *
237
239
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -240,10 +242,18 @@ describe('decide.ts.ejs', () => {
240
242
  * - Should remove existing listing
241
243
  */
242
244
 
245
+ // ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
246
+ // Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
247
+ // Do NOT use 'as ListingRemoved' to silence missing fields.
248
+ //
249
+ // Fields from command input → use ...command.data or command.data.<fieldName>
250
+ // Fields NOT in command input → produce dynamically (never hardcode):
251
+ // removedAt: Date — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
252
+
243
253
  // return {
244
254
  // type: 'ListingRemoved',
245
- // data: { ...command.data },
246
- // } as ListingRemoved;
255
+ // data: { ...command.data, /* + dynamically produce: removedAt */ },
256
+ // };
247
257
 
248
258
  throw new IllegalStateError('Not yet implemented: ' + command.type);
249
259
  }
@@ -382,9 +392,9 @@ describe('decide.ts.ejs', () => {
382
392
  *
383
393
  * You should:
384
394
  * - Validate the command input fields
385
- * - Inspect the current domain \`state\` to determine if the command is allowed
386
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
387
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
395
+ * - Inspect the current domain \`_state\` to determine if the command is allowed
396
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`, \`ValidationError\`
397
+ * ⚠️ Error constructors: IllegalStateError takes a string message, ValidationError takes a string message
388
398
  * - If valid, return one or more events with the correct structure
389
399
  *
390
400
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -397,10 +407,12 @@ describe('decide.ts.ejs', () => {
397
407
  throw new ValidationError('Title must not be empty');
398
408
  }
399
409
 
410
+ // All event fields come from command input — use ...command.data to pass them through.
411
+
400
412
  // return {
401
413
  // type: 'ListingCreated',
402
414
  // data: { ...command.data },
403
- // } as ListingCreated;
415
+ // };
404
416
 
405
417
  throw new IllegalStateError('Not yet implemented: ' + command.type);
406
418
  }
@@ -579,10 +591,10 @@ describe('decide.ts.ejs', () => {
579
591
  *
580
592
  * You should:
581
593
  * - Validate the command input fields
582
- * - Inspect the current domain \`state\` to determine if the command is allowed
594
+ * - Inspect the current domain \`_state\` to determine if the command is allowed
583
595
  * - Use \`products\` (integration result) to enrich or filter the output
584
- * - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
585
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
596
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
597
+ * ⚠️ Error constructors: IllegalStateError takes a string message
586
598
  * - If valid, return one or more events with the correct structure
587
599
  *
588
600
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
@@ -599,10 +611,18 @@ describe('decide.ts.ejs', () => {
599
611
  * - Should suggest items successfully
600
612
  */
601
613
 
614
+ // ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
615
+ // Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
616
+ // Do NOT use 'as ItemsSuggested' to silence missing fields.
617
+ //
618
+ // Fields from command input → use ...command.data or command.data.<fieldName>
619
+ // Fields NOT in command input → produce dynamically (never hardcode):
620
+ // items: Array<object> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
621
+
602
622
  // return {
603
623
  // type: 'ItemsSuggested',
604
- // data: { ...command.data },
605
- // } as ItemsSuggested;
624
+ // data: { ...command.data, /* + dynamically produce: items */ },
625
+ // };
606
626
 
607
627
  throw new IllegalStateError('Not yet implemented: ' + command.type);
608
628
  }
@@ -53,6 +53,52 @@ for (const [importPath, eventTypes] of testEventsByPath.entries()) {
53
53
  }
54
54
 
55
55
  const uniqueEventTypes = Array.from(new Set(allEvents.map(e => e?.type).filter(Boolean))).sort();
56
+
57
+ function findDerivedDateInfo(eventResults, commandFieldNames, givenEvents) {
58
+ const givenValues = new Set();
59
+ for (const g of givenEvents || []) {
60
+ for (const val of Object.values(g.exampleData || {})) {
61
+ if (typeof val === 'string') givenValues.add(val);
62
+ }
63
+ }
64
+ const fieldsByDate = new Map();
65
+ for (const e of eventResults) {
66
+ for (const [key, val] of Object.entries(e.exampleData || {})) {
67
+ if (
68
+ !commandFieldNames.has(key) &&
69
+ typeof val === 'string' &&
70
+ /^\d{4}-\d{2}-\d{2}$/.test(val) &&
71
+ !givenValues.has(val)
72
+ ) {
73
+ if (!fieldsByDate.has(val)) fieldsByDate.set(val, []);
74
+ fieldsByDate.get(val).push(key);
75
+ }
76
+ }
77
+ }
78
+ if (fieldsByDate.size !== 1) return { date: null, fields: [] };
79
+ const [date, fields] = [...fieldsByDate.entries()][0];
80
+ return { date, fields };
81
+ }
82
+
83
+ function isKeyTraceable(key, value, givenEvents) {
84
+ if (value === null || value === undefined || typeof value === 'object') return false;
85
+ for (const g of givenEvents || []) {
86
+ if ((g.exampleData || {})[key] === value) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ function buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents) {
92
+ const keep = new Set([...commandFieldNames, ...derivedDateFieldNames]);
93
+ for (const e of eventResults) {
94
+ for (const [key, value] of Object.entries(e.exampleData || {})) {
95
+ if (!keep.has(key) && isKeyTraceable(key, value, givenEvents)) {
96
+ keep.add(key);
97
+ }
98
+ }
99
+ }
100
+ return keep;
101
+ }
56
102
  _%>
57
103
  import { describe, it } from 'vitest';
58
104
  import { DeciderSpecification } from '@event-driven-io/emmett';
@@ -74,6 +120,9 @@ describe('<%= ruleDescription %>', () => {
74
120
  initialState,
75
121
  });
76
122
 
123
+ const expectEvents = (...events: Array<{ type: string; data: unknown }>) =>
124
+ events as Events[];
125
+
77
126
  <% for (const { commandName, gwt } of ruleGwts) {
78
127
  const schema = commandSchemasByName[commandName];
79
128
  const example = gwt.when;
@@ -97,18 +146,28 @@ describe('<%= ruleDescription %>', () => {
97
146
  .when({
98
147
  type: '<%= example.commandRef %>',
99
148
  data: <%- formatDataObject(example.exampleData, schema) %>,
100
- metadata: { now: new Date() },
149
+ <% const commandFieldNames = new Set(schema?.fields?.map(f => f.name) || []);
150
+ const { date: derivedDate, fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, commandFieldNames, gwt.given);
151
+ const keepFieldNames = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, gwt.given);
152
+ -%>
153
+ metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
101
154
  })
102
155
  <% if (errorResult) { %>
103
156
  .thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= errorResult.message || '' %>');
104
157
  <% } else { %>
105
158
 
106
- .then([
107
- <%- eventResults.map(e => `{
159
+ .then(expectEvents(
160
+ <%- eventResults.map(e => {
161
+ const evtSchema = events.find(evt => evt.type === e.eventRef);
162
+ const filteredData = Object.fromEntries(
163
+ Object.entries(e.exampleData || {}).filter(([key]) => keepFieldNames.has(key))
164
+ );
165
+ return `{
108
166
  type: '${e.eventRef}',
109
- data: ${formatDataObject(e.exampleData, events.find(evt => evt.type === e.eventRef))}
110
- }`).join(',\n ') %>
111
- ]);
167
+ data: ${formatDataObject(filteredData, evtSchema)}
168
+ }`;
169
+ }).join(',\n ') %>
170
+ ));
112
171
  <% } %>
113
172
  });
114
173
  <% } %>
@@ -60,12 +60,12 @@ case '<%= command %>': {
60
60
  *
61
61
  * You should:
62
62
  * - Validate the command input fields
63
- * - Inspect the current domain `state` to determine if the command is allowed
63
+ * - Inspect the current domain `_state` to determine if the command is allowed
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: `NotFoundError`, `ValidationError`, or `IllegalStateError`
68
- * ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
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
- // } as <%= fallbackEventTypes[0] ? pascalCase(fallbackEventTypes[0]) : 'TODO_EVENT_TYPE' %>;
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!) {\\n answerQuestion(input: $input) {\\n success\\n }\\n}',
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> {