@auto-engineer/server-generator-apollo-emmett 1.45.3 → 1.46.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 +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts +9 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +69 -1
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +109 -0
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +28 -4
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +246 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +26 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -2
- package/package.json +4 -4
- package/src/codegen/resolveReferencedMessageTypes.specs.ts +235 -0
- package/src/codegen/scaffoldFromSchema.ts +83 -1
- package/src/codegen/templates/command/mutation.resolver.specs.ts +109 -0
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +28 -4
- package/src/codegen/templates/query/query.resolver.specs.ts +246 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +26 -5
package/ketchup-plan.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
1
|
+
# Ketchup Plan: Referenced message type fields fail GraphQL schema generation
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
5
|
## DONE
|
|
6
6
|
|
|
7
|
-
- [x] Burst 1:
|
|
7
|
+
- [x] Burst 1: Add `resolveReferencedMessageTypes` helper + tests (a50d7d73)
|
|
8
|
+
- [x] Burst 2: Wire into `prepareTemplateData` + fix `graphqlType()` + fix `isEnumType` regression (93edcc4a)
|
|
9
|
+
- [x] Burst 3: Update query resolver template + test (e6a06086)
|
|
10
|
+
- [x] Burst 4: Update mutation resolver template + test (75e3ed80)
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^11.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/narrative": "1.
|
|
36
|
-
"@auto-engineer/message-bus": "1.
|
|
35
|
+
"@auto-engineer/narrative": "1.46.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.46.0"
|
|
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.
|
|
47
|
+
"@auto-engineer/cli": "1.46.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.46.0",
|
|
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",
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveReferencedMessageTypes } from './scaffoldFromSchema';
|
|
3
|
+
import type { MessageDefinition } from './types';
|
|
4
|
+
|
|
5
|
+
function buildMessagesByName(messages: MessageDefinition[]): Map<string, MessageDefinition> {
|
|
6
|
+
return new Map(messages.map((m) => [m.name, m]));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('resolveReferencedMessageTypes', () => {
|
|
10
|
+
it('resolves a direct message type reference', () => {
|
|
11
|
+
const messages: MessageDefinition[] = [
|
|
12
|
+
{
|
|
13
|
+
type: 'state',
|
|
14
|
+
name: 'Ingredient',
|
|
15
|
+
fields: [
|
|
16
|
+
{ name: 'name', type: 'string' },
|
|
17
|
+
{ name: 'quantity', type: 'number' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const result = resolveReferencedMessageTypes([{ name: 'item', type: 'Ingredient' }], buildMessagesByName(messages));
|
|
23
|
+
|
|
24
|
+
expect(result).toEqual([
|
|
25
|
+
{
|
|
26
|
+
name: 'Ingredient',
|
|
27
|
+
fields: [
|
|
28
|
+
{ name: 'name', type: 'string' },
|
|
29
|
+
{ name: 'quantity', type: 'number' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('resolves Array<Type> references', () => {
|
|
36
|
+
const messages: MessageDefinition[] = [
|
|
37
|
+
{
|
|
38
|
+
type: 'state',
|
|
39
|
+
name: 'Ingredient',
|
|
40
|
+
fields: [
|
|
41
|
+
{ name: 'name', type: 'string' },
|
|
42
|
+
{ name: 'amount', type: 'number' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const result = resolveReferencedMessageTypes(
|
|
48
|
+
[{ name: 'ingredients', type: 'Array<Ingredient>' }],
|
|
49
|
+
buildMessagesByName(messages),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(result).toEqual([
|
|
53
|
+
{
|
|
54
|
+
name: 'Ingredient',
|
|
55
|
+
fields: [
|
|
56
|
+
{ name: 'name', type: 'string' },
|
|
57
|
+
{ name: 'amount', type: 'number' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('resolves Type[] references', () => {
|
|
64
|
+
const messages: MessageDefinition[] = [
|
|
65
|
+
{
|
|
66
|
+
type: 'state',
|
|
67
|
+
name: 'Tag',
|
|
68
|
+
fields: [{ name: 'label', type: 'string' }],
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const result = resolveReferencedMessageTypes([{ name: 'tags', type: 'Tag[]' }], buildMessagesByName(messages));
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual([
|
|
75
|
+
{
|
|
76
|
+
name: 'Tag',
|
|
77
|
+
fields: [{ name: 'label', type: 'string' }],
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('resolves nested references in depth-first order', () => {
|
|
83
|
+
const messages: MessageDefinition[] = [
|
|
84
|
+
{
|
|
85
|
+
type: 'state',
|
|
86
|
+
name: 'Supplier',
|
|
87
|
+
fields: [{ name: 'companyName', type: 'string' }],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: 'state',
|
|
91
|
+
name: 'Ingredient',
|
|
92
|
+
fields: [
|
|
93
|
+
{ name: 'name', type: 'string' },
|
|
94
|
+
{ name: 'supplier', type: 'Supplier' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const result = resolveReferencedMessageTypes(
|
|
100
|
+
[{ name: 'ingredients', type: 'Array<Ingredient>' }],
|
|
101
|
+
buildMessagesByName(messages),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result).toEqual([
|
|
105
|
+
{
|
|
106
|
+
name: 'Supplier',
|
|
107
|
+
fields: [{ name: 'companyName', type: 'string' }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Ingredient',
|
|
111
|
+
fields: [
|
|
112
|
+
{ name: 'name', type: 'string' },
|
|
113
|
+
{ name: 'supplier', type: 'Supplier' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles circular references without infinite loop', () => {
|
|
120
|
+
const messages: MessageDefinition[] = [
|
|
121
|
+
{
|
|
122
|
+
type: 'state',
|
|
123
|
+
name: 'Node',
|
|
124
|
+
fields: [
|
|
125
|
+
{ name: 'value', type: 'string' },
|
|
126
|
+
{ name: 'children', type: 'Array<Node>' },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const result = resolveReferencedMessageTypes([{ name: 'root', type: 'Node' }], buildMessagesByName(messages));
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual([
|
|
134
|
+
{
|
|
135
|
+
name: 'Node',
|
|
136
|
+
fields: [
|
|
137
|
+
{ name: 'value', type: 'string' },
|
|
138
|
+
{ name: 'children', type: 'Array<Node>' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('does not include primitives as referenced types', () => {
|
|
145
|
+
const messages: MessageDefinition[] = [
|
|
146
|
+
{
|
|
147
|
+
type: 'state',
|
|
148
|
+
name: 'Item',
|
|
149
|
+
fields: [{ name: 'label', type: 'string' }],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const result = resolveReferencedMessageTypes(
|
|
154
|
+
[
|
|
155
|
+
{ name: 'name', type: 'string' },
|
|
156
|
+
{ name: 'count', type: 'number' },
|
|
157
|
+
{ name: 'active', type: 'boolean' },
|
|
158
|
+
{ name: 'created', type: 'Date' },
|
|
159
|
+
{ name: 'tags', type: 'Array<string>' },
|
|
160
|
+
],
|
|
161
|
+
buildMessagesByName(messages),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(result).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('deduplicates types referenced by multiple fields', () => {
|
|
168
|
+
const messages: MessageDefinition[] = [
|
|
169
|
+
{
|
|
170
|
+
type: 'state',
|
|
171
|
+
name: 'Ingredient',
|
|
172
|
+
fields: [{ name: 'name', type: 'string' }],
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const result = resolveReferencedMessageTypes(
|
|
177
|
+
[
|
|
178
|
+
{ name: 'required', type: 'Array<Ingredient>' },
|
|
179
|
+
{ name: 'optional', type: 'Array<Ingredient>' },
|
|
180
|
+
],
|
|
181
|
+
buildMessagesByName(messages),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(result).toEqual([
|
|
185
|
+
{
|
|
186
|
+
name: 'Ingredient',
|
|
187
|
+
fields: [{ name: 'name', type: 'string' }],
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('handles nullable type references', () => {
|
|
193
|
+
const messages: MessageDefinition[] = [
|
|
194
|
+
{
|
|
195
|
+
type: 'state',
|
|
196
|
+
name: 'Address',
|
|
197
|
+
fields: [{ name: 'street', type: 'string' }],
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = resolveReferencedMessageTypes(
|
|
202
|
+
[{ name: 'address', type: 'Address | null' }],
|
|
203
|
+
buildMessagesByName(messages),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(result).toEqual([
|
|
207
|
+
{
|
|
208
|
+
name: 'Address',
|
|
209
|
+
fields: [{ name: 'street', type: 'string' }],
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('handles fields using tsType instead of type', () => {
|
|
215
|
+
const messages: MessageDefinition[] = [
|
|
216
|
+
{
|
|
217
|
+
type: 'state',
|
|
218
|
+
name: 'LineItem',
|
|
219
|
+
fields: [{ name: 'sku', type: 'string' }],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const result = resolveReferencedMessageTypes(
|
|
224
|
+
[{ name: 'items', tsType: 'Array<LineItem>' }],
|
|
225
|
+
buildMessagesByName(messages),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(result).toEqual([
|
|
229
|
+
{
|
|
230
|
+
name: 'LineItem',
|
|
231
|
+
fields: [{ name: 'sku', type: 'string' }],
|
|
232
|
+
},
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -281,6 +281,12 @@ async function renderTemplate(
|
|
|
281
281
|
const isInlineObject = isInlineObjectHelper;
|
|
282
282
|
const isInlineObjectArray = isInlineObjectArrayHelper;
|
|
283
283
|
|
|
284
|
+
const messageNames = new Set(
|
|
285
|
+
(Array.isArray(data.messages) ? data.messages : [])
|
|
286
|
+
.filter((m): m is MessageDefinition => m != null && typeof m === 'object' && 'name' in m)
|
|
287
|
+
.map((m) => m.name),
|
|
288
|
+
);
|
|
289
|
+
|
|
284
290
|
const convertPrimitiveType = (base: string): string => {
|
|
285
291
|
if (base === 'ID') return 'ID';
|
|
286
292
|
if (base === 'Int') return 'Int';
|
|
@@ -314,6 +320,7 @@ async function renderTemplate(
|
|
|
314
320
|
if (base === 'object') return 'GraphQLJSON';
|
|
315
321
|
if (isInlineObject(base)) return 'GraphQLJSON';
|
|
316
322
|
if (isStringLiteralUnion(base)) return resolveEnumOrString(base);
|
|
323
|
+
if (messageNames.has(base)) return base;
|
|
317
324
|
|
|
318
325
|
return convertPrimitiveType(base);
|
|
319
326
|
};
|
|
@@ -338,12 +345,44 @@ async function renderTemplate(
|
|
|
338
345
|
|
|
339
346
|
const isNullable = (rawTs: string): boolean => /\|\s*null\b/.test(rawTs);
|
|
340
347
|
|
|
341
|
-
const
|
|
348
|
+
const rawIsEnumType = createIsEnumType(toTsFieldType);
|
|
349
|
+
const isEnumType = (tsType: string): boolean => {
|
|
350
|
+
if (!rawIsEnumType(tsType)) return false;
|
|
351
|
+
const baseName = toTsFieldType(tsType)
|
|
352
|
+
.replace(/\s*\|\s*null\b/g, '')
|
|
353
|
+
.replace(/\[\]$/, '')
|
|
354
|
+
.trim();
|
|
355
|
+
return !messageNames.has(baseName);
|
|
356
|
+
};
|
|
342
357
|
const fieldUsesDate = createFieldUsesDate(graphqlType);
|
|
343
358
|
const fieldUsesJSON = createFieldUsesJSON(graphqlType);
|
|
344
359
|
const fieldUsesFloat = createFieldUsesFloat(graphqlType);
|
|
345
360
|
const collectEnumNames = createCollectEnumNames(isEnumType, toTsFieldType);
|
|
346
361
|
|
|
362
|
+
const isReferencedMessageType = (ts: string): boolean => {
|
|
363
|
+
const base = (ts ?? '')
|
|
364
|
+
.trim()
|
|
365
|
+
.replace(/\s*\|\s*null\b/g, '')
|
|
366
|
+
.trim();
|
|
367
|
+
return messageNames.has(base);
|
|
368
|
+
};
|
|
369
|
+
const isReferencedMessageTypeArray = (ts: string): boolean => {
|
|
370
|
+
const base = (ts ?? '')
|
|
371
|
+
.trim()
|
|
372
|
+
.replace(/\s*\|\s*null\b/g, '')
|
|
373
|
+
.trim();
|
|
374
|
+
const m = base.match(/^Array<(.+)>$/) ?? base.match(/^(.+)\[\]$/);
|
|
375
|
+
return m !== null && messageNames.has(m[1].trim());
|
|
376
|
+
};
|
|
377
|
+
const extractReferencedTypeName = (ts: string): string => {
|
|
378
|
+
const base = (ts ?? '')
|
|
379
|
+
.trim()
|
|
380
|
+
.replace(/\s*\|\s*null\b/g, '')
|
|
381
|
+
.trim();
|
|
382
|
+
const m = base.match(/^Array<(.+)>$/) ?? base.match(/^(.+)\[\]$/);
|
|
383
|
+
return m !== null ? m[1].trim() : base;
|
|
384
|
+
};
|
|
385
|
+
|
|
347
386
|
const result = await template({
|
|
348
387
|
...data,
|
|
349
388
|
pascalCase,
|
|
@@ -365,6 +404,11 @@ async function renderTemplate(
|
|
|
365
404
|
fieldUsesJSON,
|
|
366
405
|
fieldUsesFloat,
|
|
367
406
|
collectEnumNames,
|
|
407
|
+
messageNames,
|
|
408
|
+
isReferencedMessageType,
|
|
409
|
+
isReferencedMessageTypeArray,
|
|
410
|
+
extractReferencedTypeName,
|
|
411
|
+
referencedTypes: data.referencedTypes,
|
|
368
412
|
});
|
|
369
413
|
|
|
370
414
|
debugTemplate('Template rendered, output size: %d bytes', result.length);
|
|
@@ -455,6 +499,34 @@ async function generateFileForTemplate(
|
|
|
455
499
|
return plan;
|
|
456
500
|
}
|
|
457
501
|
|
|
502
|
+
export function resolveReferencedMessageTypes(
|
|
503
|
+
fields: Array<{ name: string; type?: string; tsType?: string }>,
|
|
504
|
+
messagesByName: Map<string, MessageDefinition>,
|
|
505
|
+
visited?: Set<string>,
|
|
506
|
+
): Array<{ name: string; fields: MessageDefinition['fields'] }> {
|
|
507
|
+
const seen = visited ?? new Set<string>();
|
|
508
|
+
const results: Array<{ name: string; fields: MessageDefinition['fields'] }> = [];
|
|
509
|
+
|
|
510
|
+
for (const field of fields) {
|
|
511
|
+
const raw = field.type ?? field.tsType ?? 'string';
|
|
512
|
+
const stripped = raw.replace(/\s*\|\s*null\b/g, '').trim();
|
|
513
|
+
const arr1 = stripped.match(/^Array<(.+)>$/);
|
|
514
|
+
const arr2 = stripped.match(/^(.+)\[\]$/);
|
|
515
|
+
const base = arr1 ? arr1[1].trim() : arr2 ? arr2[1].trim() : stripped;
|
|
516
|
+
|
|
517
|
+
if (!messagesByName.has(base) || seen.has(base)) continue;
|
|
518
|
+
seen.add(base);
|
|
519
|
+
|
|
520
|
+
const msg = messagesByName.get(base)!;
|
|
521
|
+
const childFields = (msg.fields ?? []).map((f) => ({ name: f.name, type: f.type }));
|
|
522
|
+
const nested = resolveReferencedMessageTypes(childFields, messagesByName, seen);
|
|
523
|
+
results.push(...nested);
|
|
524
|
+
results.push({ name: msg.name, fields: msg.fields });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return results;
|
|
528
|
+
}
|
|
529
|
+
|
|
458
530
|
function extractUsedErrors(gwtMapping: Record<string, (GwtCondition & { failingFields?: string[] })[]>): string[] {
|
|
459
531
|
debug('Extracting used errors from GWT mapping');
|
|
460
532
|
const usedErrors = new Set<string>();
|
|
@@ -520,6 +592,15 @@ async function prepareTemplateData(
|
|
|
520
592
|
const allEventTypes = createEventUnionType(events);
|
|
521
593
|
const localEvents = getLocalEvents(events);
|
|
522
594
|
|
|
595
|
+
const messagesByName = new Map((allMessages ?? []).map((m) => [m.name, m]));
|
|
596
|
+
const targetName = 'server' in slice ? slice.server?.data?.items?.[0]?.target?.name : undefined;
|
|
597
|
+
const queryMessage = targetName != null ? allMessages?.find((m) => m.name === targetName) : undefined;
|
|
598
|
+
const commandFields = filteredCommands.flatMap((c) => c.fields ?? []);
|
|
599
|
+
const referencedTypes = resolveReferencedMessageTypes(
|
|
600
|
+
[...(queryMessage?.fields ?? []), ...commandFields],
|
|
601
|
+
messagesByName,
|
|
602
|
+
);
|
|
603
|
+
|
|
523
604
|
return {
|
|
524
605
|
flowName: flow.name,
|
|
525
606
|
sliceName: slice.name,
|
|
@@ -547,6 +628,7 @@ async function prepareTemplateData(
|
|
|
547
628
|
allEventTypes,
|
|
548
629
|
allEventTypesArray,
|
|
549
630
|
localEvents,
|
|
631
|
+
referencedTypes,
|
|
550
632
|
};
|
|
551
633
|
}
|
|
552
634
|
|
|
@@ -440,4 +440,113 @@ export class AddItemsToCartResolver {
|
|
|
440
440
|
);
|
|
441
441
|
expect(mutationFile?.contents).toContain('@Field(() => Float)');
|
|
442
442
|
});
|
|
443
|
+
|
|
444
|
+
it('should generate @InputType classes for referenced message types with Input suffix', async () => {
|
|
445
|
+
const spec: SpecsSchema = {
|
|
446
|
+
variant: 'specs',
|
|
447
|
+
narratives: [
|
|
448
|
+
{
|
|
449
|
+
name: 'Shopping',
|
|
450
|
+
slices: [
|
|
451
|
+
{
|
|
452
|
+
type: 'command',
|
|
453
|
+
name: 'Place order',
|
|
454
|
+
client: { specs: [] },
|
|
455
|
+
server: {
|
|
456
|
+
description: '',
|
|
457
|
+
specs: [
|
|
458
|
+
{
|
|
459
|
+
type: 'gherkin',
|
|
460
|
+
feature: '',
|
|
461
|
+
rules: [
|
|
462
|
+
{
|
|
463
|
+
name: 'place order',
|
|
464
|
+
examples: [
|
|
465
|
+
{
|
|
466
|
+
name: 'happy path',
|
|
467
|
+
steps: [
|
|
468
|
+
{
|
|
469
|
+
keyword: 'When',
|
|
470
|
+
text: 'PlaceOrder',
|
|
471
|
+
docString: {
|
|
472
|
+
orderId: 'o-1',
|
|
473
|
+
items: [{ sku: 'abc', quantity: 2 }],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
messages: [
|
|
489
|
+
{
|
|
490
|
+
type: 'state',
|
|
491
|
+
name: 'CartItem',
|
|
492
|
+
fields: [
|
|
493
|
+
{ name: 'sku', type: 'string' },
|
|
494
|
+
{ name: 'quantity', type: 'number' },
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
type: 'command',
|
|
499
|
+
name: 'PlaceOrder',
|
|
500
|
+
fields: [
|
|
501
|
+
{ name: 'orderId', type: 'string', required: true },
|
|
502
|
+
{ name: 'items', type: 'Array<CartItem>', required: true },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
509
|
+
const mutationFile = plans.find(
|
|
510
|
+
(p) => p.outputPath.endsWith('mutation.resolver.ts') && p.contents.includes('export class PlaceOrderResolver'),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
expect(mutationFile?.contents).toMatchInlineSnapshot(`
|
|
514
|
+
"import { Mutation, Resolver, Arg, Ctx, Field, InputType, Float } from 'type-graphql';
|
|
515
|
+
import { type GraphQLContext, sendCommand, MutationResponse } from '../../../shared';
|
|
516
|
+
|
|
517
|
+
@InputType()
|
|
518
|
+
export class CartItemInput {
|
|
519
|
+
@Field(() => String)
|
|
520
|
+
sku!: string;
|
|
521
|
+
|
|
522
|
+
@Field(() => Float)
|
|
523
|
+
quantity!: number;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
@InputType()
|
|
527
|
+
export class PlaceOrderInput {
|
|
528
|
+
@Field(() => String)
|
|
529
|
+
orderId!: string;
|
|
530
|
+
|
|
531
|
+
@Field(() => [CartItemInput])
|
|
532
|
+
items!: CartItemInput[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
@Resolver()
|
|
536
|
+
export class PlaceOrderResolver {
|
|
537
|
+
@Mutation(() => MutationResponse)
|
|
538
|
+
async placeOrder(
|
|
539
|
+
@Arg('input', () => PlaceOrderInput) input: PlaceOrderInput,
|
|
540
|
+
@Ctx() ctx: GraphQLContext,
|
|
541
|
+
): Promise<MutationResponse> {
|
|
542
|
+
return await sendCommand(ctx.messageBus, {
|
|
543
|
+
type: 'PlaceOrder',
|
|
544
|
+
kind: 'Command',
|
|
545
|
+
data: { ...input },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
"
|
|
550
|
+
`);
|
|
551
|
+
});
|
|
443
552
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<%
|
|
2
2
|
const cmd = commands[0];
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
3
|
+
const refFields = (referencedTypes ?? []).flatMap(rt => rt.fields ?? []);
|
|
4
|
+
const usesDate = [...cmd.fields, ...refFields].some(f => fieldUsesDate(f.type ?? f.tsType));
|
|
5
|
+
const usesJSON = [...cmd.fields, ...refFields].some(f => fieldUsesJSON(f.type ?? f.tsType));
|
|
6
|
+
const usesFloat = [...cmd.fields, ...refFields].some(f => fieldUsesFloat(f.type ?? f.tsType));
|
|
7
|
+
const enumList = collectEnumNames([...cmd.fields, ...refFields]);
|
|
7
8
|
|
|
8
9
|
const embeddedInputs = [];
|
|
9
10
|
for (const f of cmd.fields) {
|
|
@@ -35,6 +36,23 @@ export class <%= typeName %> {
|
|
|
35
36
|
<% } %>
|
|
36
37
|
}
|
|
37
38
|
<% } %>
|
|
39
|
+
<% for (const ref of (referencedTypes ?? [])) {
|
|
40
|
+
const parsedRefFields = (ref.fields ?? []).map(f => ({
|
|
41
|
+
name: f.name,
|
|
42
|
+
tsType: f.type ?? 'string',
|
|
43
|
+
gqlType: graphqlType(f.type ?? 'string'),
|
|
44
|
+
nullable: isNullable(f.type ?? 'string'),
|
|
45
|
+
}));
|
|
46
|
+
%>
|
|
47
|
+
|
|
48
|
+
@InputType()
|
|
49
|
+
export class <%= ref.name %>Input {
|
|
50
|
+
<% for (const f of parsedRefFields) { %>
|
|
51
|
+
@Field(() => <%= f.gqlType %><%= f.nullable ? ', { nullable: true }' : '' %>)
|
|
52
|
+
<%= f.name %><%= f.nullable ? '?' : '!' %>: <%= toTsFieldType(f.tsType) %>;
|
|
53
|
+
<% } %>
|
|
54
|
+
}
|
|
55
|
+
<% } %>
|
|
38
56
|
|
|
39
57
|
@InputType()
|
|
40
58
|
export class <%= pascalCase(cmd.type) %>Input {
|
|
@@ -49,6 +67,12 @@ if (isInlineObjectArray(tsType)) { %>
|
|
|
49
67
|
<% } else if (isInlineObject(tsType)) { %>
|
|
50
68
|
@Field(() => <%= nestedName %><%= (field.required === false || isNullable(tsType)) ? ', { nullable: true }' : '' %>)
|
|
51
69
|
<%= field.name %><%= field.required === false ? '?' : '!' %>: <%= nestedName %>;
|
|
70
|
+
<% } else if (isReferencedMessageTypeArray(tsType)) { %>
|
|
71
|
+
@Field(() => [<%= extractReferencedTypeName(tsType) %>Input]<%= (field.required === false || isNullable(tsType)) ? ', { nullable: true }' : '' %>)
|
|
72
|
+
<%= field.name %><%= field.required === false ? '?' : '!' %>: <%= extractReferencedTypeName(tsType) %>Input[];
|
|
73
|
+
<% } else if (isReferencedMessageType(tsType)) { %>
|
|
74
|
+
@Field(() => <%= extractReferencedTypeName(tsType) %>Input<%= (field.required === false || isNullable(tsType)) ? ', { nullable: true }' : '' %>)
|
|
75
|
+
<%= field.name %><%= field.required === false ? '?' : '!' %>: <%= extractReferencedTypeName(tsType) %>Input;
|
|
52
76
|
<% } else { %>
|
|
53
77
|
@Field(() => <%= gqlType %><%= (field.required === false || isNullable(tsType)) ? ', { nullable: true }' : '' %>)
|
|
54
78
|
<%= field.name %><%= field.required === false ? '?' : '!' %>: <%= toTsFieldType(tsType) %>;
|