@auto-engineer/server-generator-apollo-emmett 1.110.7 → 1.111.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 +48 -0
- package/README.md +0 -1
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +20 -14
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +2 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +67 -0
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +65 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +15 -6
- package/dist/src/commands/generate-server.d.ts +0 -2
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +6 -76
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/commands/initialize-server.d.ts.map +1 -1
- package/dist/src/commands/initialize-server.js +0 -41
- package/dist/src/commands/initialize-server.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -0
- package/package.json +4 -4
- package/src/codegen/extract/messages.specs.ts +172 -0
- package/src/codegen/extract/messages.ts +22 -14
- package/src/codegen/findEventSource.specs.ts +54 -0
- package/src/codegen/scaffoldFromSchema.ts +2 -2
- package/src/codegen/templates/command/decide.specs.specs.ts +67 -0
- package/src/codegen/templates/command/decide.specs.ts.ejs +1 -1
- package/src/codegen/templates/query/query.resolver.specs.ts +65 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +15 -6
- package/src/commands/generate-server.specs.ts +1 -4
- package/src/commands/generate-server.ts +8 -90
- package/src/commands/initialize-server.specs.ts +1 -1
- package/src/commands/initialize-server.ts +0 -48
package/ketchup-plan.md
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
## DONE (Round 3)
|
|
8
8
|
|
|
9
|
+
- [x] Burst 30: Fix 12 — Exclude custom GraphQL input types from enum imports in query.resolver.ts.ejs
|
|
10
|
+
- [x] Burst 29: Fix 11 — SKIPPED: requires modifying standalone types.ts (blocked by type-organization rule); output is identical so no behavioral impact
|
|
11
|
+
- [x] Burst 28: Fix 10 — Remove "valid" qualifier from decide.specs.ts.ejs test descriptions
|
|
12
|
+
- [x] Burst 27: Fix 9 — Map custom GraphQL input type args to GraphQLJSON in query.resolver.ts.ejs
|
|
13
|
+
|
|
9
14
|
- [x] Burst 26: Add fart-model test for query-arg-differs-from-projection case
|
|
10
15
|
- [x] Burst 25: Exclude query arg fields from projection spec expected state (6da49a58)
|
|
11
16
|
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^13.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/
|
|
36
|
-
"@auto-engineer/
|
|
35
|
+
"@auto-engineer/narrative": "1.111.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.111.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.111.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.111.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",
|
|
@@ -557,3 +557,175 @@ describe('extractMessagesFromSpecs (react slice with data target events)', () =>
|
|
|
557
557
|
);
|
|
558
558
|
});
|
|
559
559
|
});
|
|
560
|
+
|
|
561
|
+
describe('extractMessagesFromSpecs (command slice with data target events)', () => {
|
|
562
|
+
it('should extract data.items Event not in GWT Then', () => {
|
|
563
|
+
const slice: Slice = {
|
|
564
|
+
type: 'command',
|
|
565
|
+
name: 'submit workout log',
|
|
566
|
+
server: {
|
|
567
|
+
description: 'Submits a workout log',
|
|
568
|
+
data: {
|
|
569
|
+
items: [
|
|
570
|
+
{
|
|
571
|
+
target: { type: 'Event', name: 'WorkoutLogged' },
|
|
572
|
+
destination: { type: 'stream', pattern: 'workout-${workoutId}' },
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
target: { type: 'Event', name: 'PointsEarned' },
|
|
576
|
+
destination: { type: 'stream', pattern: 'points-${userId}' },
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
target: { type: 'Event', name: 'WorkoutLogRejected' },
|
|
580
|
+
destination: { type: 'stream', pattern: 'workout-${workoutId}' },
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
},
|
|
584
|
+
specs: [
|
|
585
|
+
{
|
|
586
|
+
type: 'gherkin',
|
|
587
|
+
feature: 'Submit workout log',
|
|
588
|
+
rules: [
|
|
589
|
+
{
|
|
590
|
+
name: 'Should submit workout',
|
|
591
|
+
examples: [
|
|
592
|
+
{
|
|
593
|
+
name: 'Workout logged',
|
|
594
|
+
steps: [
|
|
595
|
+
{ keyword: 'When', text: 'SubmitWorkoutLog', docString: { workoutId: 'w1' } },
|
|
596
|
+
{ keyword: 'Then', text: 'WorkoutLogged', docString: { workoutId: 'w1' } },
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: 'Workout rejected',
|
|
601
|
+
steps: [
|
|
602
|
+
{ keyword: 'When', text: 'SubmitWorkoutLog', docString: { workoutId: 'w2' } },
|
|
603
|
+
{ keyword: 'Then', text: 'WorkoutLogRejected', docString: { workoutId: 'w2' } },
|
|
604
|
+
],
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const allMessages: MessageDefinition[] = [
|
|
615
|
+
{
|
|
616
|
+
type: 'command',
|
|
617
|
+
name: 'SubmitWorkoutLog',
|
|
618
|
+
fields: [{ name: 'workoutId', type: 'string', required: true }],
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
type: 'event',
|
|
622
|
+
name: 'WorkoutLogged',
|
|
623
|
+
fields: [{ name: 'workoutId', type: 'string', required: true }],
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
type: 'event',
|
|
627
|
+
name: 'PointsEarned',
|
|
628
|
+
fields: [
|
|
629
|
+
{ name: 'userId', type: 'string', required: true },
|
|
630
|
+
{ name: 'points', type: 'number', required: true },
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
type: 'event',
|
|
635
|
+
name: 'WorkoutLogRejected',
|
|
636
|
+
fields: [{ name: 'workoutId', type: 'string', required: true }],
|
|
637
|
+
},
|
|
638
|
+
];
|
|
639
|
+
|
|
640
|
+
const result = extractMessagesFromSpecs(slice, allMessages);
|
|
641
|
+
|
|
642
|
+
expect(result.events).toEqual([
|
|
643
|
+
{
|
|
644
|
+
type: 'WorkoutLogged',
|
|
645
|
+
fields: [{ name: 'workoutId', tsType: 'string', required: true }],
|
|
646
|
+
source: 'then',
|
|
647
|
+
sourceFlowName: undefined,
|
|
648
|
+
sourceSliceName: 'submit workout log',
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
type: 'WorkoutLogRejected',
|
|
652
|
+
fields: [{ name: 'workoutId', tsType: 'string', required: true }],
|
|
653
|
+
source: 'then',
|
|
654
|
+
sourceFlowName: undefined,
|
|
655
|
+
sourceSliceName: 'submit workout log',
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
type: 'PointsEarned',
|
|
659
|
+
fields: [
|
|
660
|
+
{ name: 'userId', tsType: 'string', required: true },
|
|
661
|
+
{ name: 'points', tsType: 'number', required: true },
|
|
662
|
+
],
|
|
663
|
+
source: 'then',
|
|
664
|
+
sourceSliceName: 'submit workout log',
|
|
665
|
+
},
|
|
666
|
+
]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should deduplicate events present in both GWT Then and data.items', () => {
|
|
670
|
+
const slice: Slice = {
|
|
671
|
+
type: 'command',
|
|
672
|
+
name: 'place order',
|
|
673
|
+
server: {
|
|
674
|
+
description: 'Places an order',
|
|
675
|
+
data: {
|
|
676
|
+
items: [
|
|
677
|
+
{
|
|
678
|
+
target: { type: 'Event', name: 'OrderPlaced' },
|
|
679
|
+
destination: { type: 'stream', pattern: 'order-${orderId}' },
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
specs: [
|
|
684
|
+
{
|
|
685
|
+
type: 'gherkin',
|
|
686
|
+
feature: 'Place order',
|
|
687
|
+
rules: [
|
|
688
|
+
{
|
|
689
|
+
name: 'Should place',
|
|
690
|
+
examples: [
|
|
691
|
+
{
|
|
692
|
+
name: 'Order placed',
|
|
693
|
+
steps: [
|
|
694
|
+
{ keyword: 'When', text: 'PlaceOrder', docString: { orderId: 'o1' } },
|
|
695
|
+
{ keyword: 'Then', text: 'OrderPlaced', docString: { orderId: 'o1' } },
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
const allMessages: MessageDefinition[] = [
|
|
707
|
+
{
|
|
708
|
+
type: 'command',
|
|
709
|
+
name: 'PlaceOrder',
|
|
710
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
type: 'event',
|
|
714
|
+
name: 'OrderPlaced',
|
|
715
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
|
|
719
|
+
const result = extractMessagesFromSpecs(slice, allMessages);
|
|
720
|
+
|
|
721
|
+
expect(result.events).toEqual([
|
|
722
|
+
{
|
|
723
|
+
type: 'OrderPlaced',
|
|
724
|
+
fields: [{ name: 'orderId', tsType: 'string', required: true }],
|
|
725
|
+
source: 'then',
|
|
726
|
+
sourceFlowName: undefined,
|
|
727
|
+
sourceSliceName: 'place order',
|
|
728
|
+
},
|
|
729
|
+
]);
|
|
730
|
+
});
|
|
731
|
+
});
|
|
@@ -52,6 +52,23 @@ function deduplicateMessages<T extends Message>(messages: T[]): T[] {
|
|
|
52
52
|
return result;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function extractDataTargetEvents(slice: Slice, allMessages: MessageDefinition[]): Message[] {
|
|
56
|
+
const events: Message[] = [];
|
|
57
|
+
if ('server' in slice && slice.server?.data?.items) {
|
|
58
|
+
for (const item of slice.server.data.items) {
|
|
59
|
+
if (item.target.type === 'Event') {
|
|
60
|
+
events.push({
|
|
61
|
+
type: item.target.name,
|
|
62
|
+
fields: extractFieldsFromMessage(item.target.name, 'event', allMessages),
|
|
63
|
+
source: 'then' as const,
|
|
64
|
+
sourceSliceName: slice.name,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return events;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
function extractMessagesForCommand(slice: Slice, allMessages: MessageDefinition[]): ExtractedMessages {
|
|
56
73
|
debugCommand('Extracting messages for command slice: %s', slice.name);
|
|
57
74
|
|
|
@@ -98,9 +115,12 @@ function extractMessagesForCommand(slice: Slice, allMessages: MessageDefinition[
|
|
|
98
115
|
});
|
|
99
116
|
debugCommand(' Total events extracted: %d', events.length);
|
|
100
117
|
|
|
118
|
+
const dataTargetEvents = extractDataTargetEvents(slice, allMessages);
|
|
119
|
+
debugCommand(' Extracted %d data target events', dataTargetEvents.length);
|
|
120
|
+
|
|
101
121
|
const result = {
|
|
102
122
|
commands,
|
|
103
|
-
events: deduplicateMessages([...stateAsEvents, ...events]),
|
|
123
|
+
events: deduplicateMessages([...stateAsEvents, ...events, ...dataTargetEvents]),
|
|
104
124
|
states: [],
|
|
105
125
|
commandSchemasByName,
|
|
106
126
|
};
|
|
@@ -215,19 +235,7 @@ function extractMessagesForReact(slice: Slice, allMessages: MessageDefinition[])
|
|
|
215
235
|
const dataStates = extractStatesFromData(slice, allMessages);
|
|
216
236
|
debugReact(' Extracted %d states from data', dataStates.length);
|
|
217
237
|
|
|
218
|
-
const dataTargetEvents
|
|
219
|
-
if (slice.server?.data?.items) {
|
|
220
|
-
for (const item of slice.server.data.items) {
|
|
221
|
-
if (item.target.type === 'Event') {
|
|
222
|
-
dataTargetEvents.push({
|
|
223
|
-
type: item.target.name,
|
|
224
|
-
fields: extractFieldsFromMessage(item.target.name, 'event', allMessages),
|
|
225
|
-
source: 'then' as const,
|
|
226
|
-
sourceSliceName: slice.name,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
238
|
+
const dataTargetEvents = extractDataTargetEvents(slice, allMessages);
|
|
231
239
|
debugReact(' Extracted %d data target events', dataTargetEvents.length);
|
|
232
240
|
|
|
233
241
|
const result = {
|
|
@@ -95,6 +95,60 @@ describe('findEventSource', () => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
it('should find event in command slice data.items when not in GWT specs', () => {
|
|
99
|
+
const flows: Narrative[] = [
|
|
100
|
+
{
|
|
101
|
+
name: 'workout flow',
|
|
102
|
+
slices: [
|
|
103
|
+
{
|
|
104
|
+
type: 'command',
|
|
105
|
+
name: 'submit workout log',
|
|
106
|
+
server: {
|
|
107
|
+
description: 'Submits a workout log',
|
|
108
|
+
data: {
|
|
109
|
+
items: [
|
|
110
|
+
{
|
|
111
|
+
target: { type: 'Event', name: 'WorkoutLogged' },
|
|
112
|
+
destination: { type: 'stream', pattern: 'workout-${workoutId}' },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
target: { type: 'Event', name: 'PointsEarned' },
|
|
116
|
+
destination: { type: 'stream', pattern: 'points-${userId}' },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
specs: [
|
|
121
|
+
{
|
|
122
|
+
type: 'gherkin',
|
|
123
|
+
feature: 'Submit workout',
|
|
124
|
+
rules: [
|
|
125
|
+
{
|
|
126
|
+
name: 'Should submit',
|
|
127
|
+
examples: [
|
|
128
|
+
{
|
|
129
|
+
name: 'Workout logged',
|
|
130
|
+
steps: [
|
|
131
|
+
{ keyword: 'When', text: 'SubmitWorkoutLog', docString: {} },
|
|
132
|
+
{ keyword: 'Then', text: 'WorkoutLogged', docString: {} },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
expect(findEventSource(flows, 'PointsEarned')).toEqual({
|
|
147
|
+
flowName: 'workout flow',
|
|
148
|
+
sliceName: 'submit workout log',
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
98
152
|
it('should return null when event is not found anywhere', () => {
|
|
99
153
|
const flows: Narrative[] = [
|
|
100
154
|
{
|
|
@@ -742,10 +742,10 @@ export function findEventSource(flows: Narrative[], eventType: string): { flowNa
|
|
|
742
742
|
return { flowName: flow.name, sliceName: slice.name };
|
|
743
743
|
}
|
|
744
744
|
}
|
|
745
|
-
if (
|
|
745
|
+
if ('server' in slice && slice.server?.data?.items) {
|
|
746
746
|
for (const item of slice.server.data.items) {
|
|
747
747
|
if (item.target.type === 'Event' && item.target.name === eventType) {
|
|
748
|
-
debugSlice(' Found event source in
|
|
748
|
+
debugSlice(' Found event source in data target: flow=%s, slice=%s', flow.name, slice.name);
|
|
749
749
|
return { flowName: flow.name, sliceName: slice.name };
|
|
750
750
|
}
|
|
751
751
|
}
|
|
@@ -1387,4 +1387,71 @@ describe('spec.ts.ejs', () => {
|
|
|
1387
1387
|
expect(decideFile?.contents).toContain('...command.data');
|
|
1388
1388
|
expect(decideFile?.contents).not.toContain('`NotFoundError`');
|
|
1389
1389
|
});
|
|
1390
|
+
|
|
1391
|
+
it('should not include "valid" qualifier in fallback test description', async () => {
|
|
1392
|
+
const spec: SpecsSchema = {
|
|
1393
|
+
variant: 'specs',
|
|
1394
|
+
narratives: [
|
|
1395
|
+
{
|
|
1396
|
+
name: 'order-flow',
|
|
1397
|
+
slices: [
|
|
1398
|
+
{
|
|
1399
|
+
type: 'command',
|
|
1400
|
+
name: 'place order',
|
|
1401
|
+
client: { specs: [] },
|
|
1402
|
+
server: {
|
|
1403
|
+
description: '',
|
|
1404
|
+
specs: [
|
|
1405
|
+
{
|
|
1406
|
+
type: 'gherkin',
|
|
1407
|
+
feature: 'Place order',
|
|
1408
|
+
rules: [
|
|
1409
|
+
{
|
|
1410
|
+
name: 'Order placement',
|
|
1411
|
+
examples: [
|
|
1412
|
+
{
|
|
1413
|
+
name: '',
|
|
1414
|
+
steps: [
|
|
1415
|
+
{
|
|
1416
|
+
keyword: 'When',
|
|
1417
|
+
text: 'PlaceOrder',
|
|
1418
|
+
docString: { orderId: 'o1' },
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
keyword: 'Then',
|
|
1422
|
+
text: 'OrderPlaced',
|
|
1423
|
+
docString: { orderId: 'o1' },
|
|
1424
|
+
},
|
|
1425
|
+
],
|
|
1426
|
+
},
|
|
1427
|
+
],
|
|
1428
|
+
},
|
|
1429
|
+
],
|
|
1430
|
+
},
|
|
1431
|
+
],
|
|
1432
|
+
},
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1435
|
+
},
|
|
1436
|
+
],
|
|
1437
|
+
messages: [
|
|
1438
|
+
{
|
|
1439
|
+
type: 'command',
|
|
1440
|
+
name: 'PlaceOrder',
|
|
1441
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
type: 'event',
|
|
1445
|
+
name: 'OrderPlaced',
|
|
1446
|
+
fields: [{ name: 'orderId', type: 'string', required: true }],
|
|
1447
|
+
},
|
|
1448
|
+
],
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1452
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
1453
|
+
|
|
1454
|
+
expect(specFile?.contents).toContain('should emit OrderPlaced for PlaceOrder');
|
|
1455
|
+
expect(specFile?.contents).not.toContain('for valid');
|
|
1456
|
+
});
|
|
1390
1457
|
});
|
|
@@ -132,7 +132,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
132
132
|
const testDescription = gwt.description ||
|
|
133
133
|
(errorResult
|
|
134
134
|
? `should throw ${errorResult.errorType} when ${gwt.failingFields?.join(', ') || 'invalid input'}`
|
|
135
|
-
: `should emit ${eventResults.map(e => e.eventRef).join(', ')} for
|
|
135
|
+
: `should emit ${eventResults.map(e => e.eventRef).join(', ')} for ${commandName}`);
|
|
136
136
|
%>
|
|
137
137
|
it('<%= testDescription %>', () => {
|
|
138
138
|
given([
|
|
@@ -1186,4 +1186,69 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
1186
1186
|
"
|
|
1187
1187
|
`);
|
|
1188
1188
|
});
|
|
1189
|
+
|
|
1190
|
+
it('should map custom input type args to GraphQLJSON and Record<string, unknown>', async () => {
|
|
1191
|
+
const spec: SpecsSchema = {
|
|
1192
|
+
variant: 'specs',
|
|
1193
|
+
narratives: [
|
|
1194
|
+
{
|
|
1195
|
+
name: 'workout-flow',
|
|
1196
|
+
slices: [
|
|
1197
|
+
{
|
|
1198
|
+
type: 'query',
|
|
1199
|
+
name: 'list-workouts',
|
|
1200
|
+
request: `
|
|
1201
|
+
query ListWorkouts($filter: ListWorkoutsFilterInput, $limit: Int) {
|
|
1202
|
+
listWorkouts(filter: $filter, limit: $limit) {
|
|
1203
|
+
workoutId
|
|
1204
|
+
exercise
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
`,
|
|
1208
|
+
client: { specs: [] },
|
|
1209
|
+
server: {
|
|
1210
|
+
description: '',
|
|
1211
|
+
data: {
|
|
1212
|
+
items: [
|
|
1213
|
+
{
|
|
1214
|
+
origin: {
|
|
1215
|
+
type: 'projection',
|
|
1216
|
+
idField: 'workoutId',
|
|
1217
|
+
name: 'WorkoutsProjection',
|
|
1218
|
+
},
|
|
1219
|
+
target: {
|
|
1220
|
+
type: 'State',
|
|
1221
|
+
name: 'WorkoutSummary',
|
|
1222
|
+
},
|
|
1223
|
+
},
|
|
1224
|
+
],
|
|
1225
|
+
},
|
|
1226
|
+
specs: [],
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
],
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
messages: [
|
|
1233
|
+
{
|
|
1234
|
+
type: 'state',
|
|
1235
|
+
name: 'WorkoutSummary',
|
|
1236
|
+
fields: [
|
|
1237
|
+
{ name: 'workoutId', type: 'string', required: true },
|
|
1238
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
1239
|
+
],
|
|
1240
|
+
},
|
|
1241
|
+
],
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
1245
|
+
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
1246
|
+
|
|
1247
|
+
expect(resolverFile?.contents).toContain("import { GraphQLJSON } from 'graphql-type-json';");
|
|
1248
|
+
expect(resolverFile?.contents).toContain(
|
|
1249
|
+
"@Arg('filter', () => GraphQLJSON, { nullable: true }) filter?: Record<string, unknown>",
|
|
1250
|
+
);
|
|
1251
|
+
expect(resolverFile?.contents).toContain("@Arg('limit', () => Float, { nullable: true }) limit?: number");
|
|
1252
|
+
expect(resolverFile?.contents).not.toContain('ListWorkoutsFilterInput');
|
|
1253
|
+
});
|
|
1189
1254
|
});
|
|
@@ -11,14 +11,17 @@ const usesID = parsedRequest?.args?.some(arg => graphqlType(arg.tsType) === 'ID'
|
|
|
11
11
|
|
|
12
12
|
const messageFields = message?.fields ?? [];
|
|
13
13
|
const refFields = (referencedTypes ?? []).flatMap(rt => rt.fields ?? []);
|
|
14
|
+
const KNOWN_GQL_SCALARS = new Set(['String', 'Int', 'Float', 'Number', 'Boolean', 'Date', 'ID']);
|
|
14
15
|
const usesDate = [...messageFields, ...refFields].some(f => fieldUsesDate(f.type)) ||
|
|
15
16
|
(parsedRequest?.args ?? []).some(a => fieldUsesDate(a.tsType));
|
|
17
|
+
const hasCustomTypeArgs = (parsedRequest?.args ?? []).some(a => !KNOWN_GQL_SCALARS.has(a.graphqlType));
|
|
16
18
|
const usesJSON = [...messageFields, ...refFields].some(f => fieldUsesJSON(f.type)) ||
|
|
17
|
-
(parsedRequest?.args ?? []).some(a => fieldUsesJSON(a.tsType))
|
|
19
|
+
(parsedRequest?.args ?? []).some(a => fieldUsesJSON(a.tsType)) ||
|
|
20
|
+
hasCustomTypeArgs;
|
|
18
21
|
const usesFloat = [...messageFields, ...refFields].some(f => fieldUsesFloat(f.type)) ||
|
|
19
22
|
(parsedRequest?.args ?? []).some(a => fieldUsesFloat(a.tsType));
|
|
20
23
|
|
|
21
|
-
const enumList = collectEnumNames([...messageFields, ...
|
|
24
|
+
const enumList = collectEnumNames([...messageFields, ...refFields]);
|
|
22
25
|
const filteredReferencedTypes = (referencedTypes ?? []).filter(rt => rt.name !== viewType);
|
|
23
26
|
|
|
24
27
|
const embeddedTypes = [];
|
|
@@ -32,6 +35,14 @@ for (const field of messageFields) {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
const hasArgs = parsedRequest?.args?.length > 0;
|
|
38
|
+
|
|
39
|
+
const resolveArgTypes = (arg) => {
|
|
40
|
+
const isCustom = !KNOWN_GQL_SCALARS.has(arg.graphqlType);
|
|
41
|
+
return {
|
|
42
|
+
gqlType: isCustom ? 'GraphQLJSON' : graphqlType(arg.tsType),
|
|
43
|
+
tsType: arg.tsType === 'ID' ? 'string' : (isCustom ? 'Record<string, unknown>' : arg.tsType),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
35
46
|
%>
|
|
36
47
|
import { Query, Resolver<% if (hasArgs) { %>, Arg<% } %>, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
37
48
|
<% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
|
|
@@ -106,8 +117,7 @@ async <%= queryName %>(
|
|
|
106
117
|
@Ctx() ctx: GraphQLContext<% if (parsedRequest?.args?.length) { %>,
|
|
107
118
|
<% for (let i = 0; i < parsedRequest.args.length; i++) {
|
|
108
119
|
const arg = parsedRequest.args[i];
|
|
109
|
-
const gqlType =
|
|
110
|
-
const tsType = arg.tsType === 'ID' ? 'string' : arg.tsType;
|
|
120
|
+
const { gqlType, tsType } = resolveArgTypes(arg);
|
|
111
121
|
%> @Arg('<%= arg.name %>', () => <%= gqlType %>, { nullable: true }) <%= arg.name %>?: <%= tsType %><%= i < parsedRequest.args.length - 1 ? ',' : '' %>
|
|
112
122
|
<% } } %>
|
|
113
123
|
): Promise<<%= viewType %>> {
|
|
@@ -149,8 +159,7 @@ async <%= queryName %>(
|
|
|
149
159
|
@Ctx() ctx: GraphQLContext<% if (parsedRequest?.args?.length) { %>,
|
|
150
160
|
<% for (let i = 0; i < parsedRequest.args.length; i++) {
|
|
151
161
|
const arg = parsedRequest.args[i];
|
|
152
|
-
const gqlType =
|
|
153
|
-
const tsType = arg.tsType === 'ID' ? 'string' : arg.tsType;
|
|
162
|
+
const { gqlType, tsType } = resolveArgTypes(arg);
|
|
154
163
|
%> @Arg('<%= arg.name %>', () => <%= gqlType %>, { nullable: true }) <%= arg.name %>?: <%= tsType %><%= i < parsedRequest.args.length - 1 ? ',' : '' %>
|
|
155
164
|
<% } } %>
|
|
156
165
|
): Promise<<%= viewType %>[]> {
|
|
@@ -89,15 +89,12 @@ describe('cleanServerDir', () => {
|
|
|
89
89
|
expect(await fs.pathExists(join(dir, 'src'))).toBe(false);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('removes
|
|
93
|
-
await fs.ensureDir(join(dir, 'scripts'));
|
|
94
|
-
await fs.writeFile(join(dir, 'scripts', 'generate-schema.ts'), 'export {}');
|
|
92
|
+
it('removes dist/ directory when present', async () => {
|
|
95
93
|
await fs.ensureDir(join(dir, 'dist'));
|
|
96
94
|
await fs.writeFile(join(dir, 'dist', 'server.js'), 'export {}');
|
|
97
95
|
|
|
98
96
|
await cleanServerDir(dir);
|
|
99
97
|
|
|
100
|
-
expect(await fs.pathExists(join(dir, 'scripts'))).toBe(false);
|
|
101
98
|
expect(await fs.pathExists(join(dir, 'dist'))).toBe(false);
|
|
102
99
|
});
|
|
103
100
|
});
|