@auto-engineer/server-generator-apollo-emmett 1.131.0 → 1.134.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 +131 -0
- package/dist/src/codegen/extract/data-sink.d.ts +1 -0
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.js +9 -0
- package/dist/src/codegen/extract/data-sink.js.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +6 -1
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.js +17 -0
- package/dist/src/codegen/extract/projection.js.map +1 -1
- package/dist/src/codegen/extract/query.d.ts +8 -2
- package/dist/src/codegen/extract/query.d.ts.map +1 -1
- package/dist/src/codegen/extract/query.js +36 -0
- package/dist/src/codegen/extract/query.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +9 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/query/projection.specs.ts +323 -0
- package/dist/src/codegen/templates/query/projection.ts.ejs +27 -4
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +242 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
- package/dist/src/commands/generate-server.js +5 -5
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/data-sink.specs.ts +24 -0
- package/src/codegen/extract/data-sink.ts +10 -0
- package/src/codegen/extract/projection.specs.ts +212 -0
- package/src/codegen/extract/projection.ts +22 -1
- package/src/codegen/extract/query.specs.ts +137 -0
- package/src/codegen/extract/query.ts +46 -2
- package/src/codegen/scaffoldFromSchema.ts +13 -0
- package/src/codegen/templates/query/projection.specs.ts +323 -0
- package/src/codegen/templates/query/projection.ts.ejs +27 -4
- package/src/codegen/templates/query/query.resolver.specs.ts +242 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +8 -4
- package/src/commands/generate-server.ts +5 -5
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.134.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.134.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.134.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.134.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,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { extractStreamIdFields } from './data-sink';
|
|
3
|
+
|
|
4
|
+
describe('extractStreamIdFields', () => {
|
|
5
|
+
it('extracts a single template variable from a stream pattern', () => {
|
|
6
|
+
expect(extractStreamIdFields('workouts-${id}')).toEqual(['id']);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('extracts a different single template variable', () => {
|
|
10
|
+
expect(extractStreamIdFields('workouts-${workoutId}')).toEqual(['workoutId']);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('extracts multiple template variables', () => {
|
|
14
|
+
expect(extractStreamIdFields('user-project-${userId}-${projectId}')).toEqual(['userId', 'projectId']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns empty array for pattern with no variables', () => {
|
|
18
|
+
expect(extractStreamIdFields('static-stream')).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns empty array for empty string', () => {
|
|
22
|
+
expect(extractStreamIdFields('')).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -62,6 +62,16 @@ function processStreamSink(item: unknown, exampleData: Record<string, unknown>)
|
|
|
62
62
|
return { streamPattern, streamId };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export function extractStreamIdFields(pattern: string): string[] {
|
|
66
|
+
const fields: string[] = [];
|
|
67
|
+
const regex = /\$\{([^}]+)\}/g;
|
|
68
|
+
let match: RegExpExecArray | null;
|
|
69
|
+
while ((match = regex.exec(pattern)) !== null) {
|
|
70
|
+
fields.push(match[1]);
|
|
71
|
+
}
|
|
72
|
+
return fields;
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
export function getStreamFromSink(slice: Slice): { streamPattern?: string; streamId?: string } {
|
|
66
76
|
if (!('server' in slice)) return {};
|
|
67
77
|
if (slice.server?.data?.items == null || !Array.isArray(slice.server.data.items)) {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Narrative } from '@auto-engineer/narrative';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { Message } from '../types';
|
|
4
|
+
import { buildEventIdFieldMap } from './projection';
|
|
5
|
+
|
|
6
|
+
describe('buildEventIdFieldMap', () => {
|
|
7
|
+
it('maps events to their producing slice stream id fields', () => {
|
|
8
|
+
const events: Message[] = [
|
|
9
|
+
{ type: 'WorkoutCreated', fields: [] },
|
|
10
|
+
{ type: 'ExerciseLogged', fields: [] },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const flows: Narrative[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'workout-flow',
|
|
16
|
+
slices: [
|
|
17
|
+
{
|
|
18
|
+
type: 'command',
|
|
19
|
+
name: 'create-workout',
|
|
20
|
+
stream: 'workouts-${id}',
|
|
21
|
+
client: { specs: [] },
|
|
22
|
+
server: {
|
|
23
|
+
description: '',
|
|
24
|
+
specs: [
|
|
25
|
+
{
|
|
26
|
+
type: 'gherkin',
|
|
27
|
+
feature: 'Create workout',
|
|
28
|
+
rules: [
|
|
29
|
+
{
|
|
30
|
+
name: 'rule',
|
|
31
|
+
examples: [
|
|
32
|
+
{
|
|
33
|
+
name: 'ex',
|
|
34
|
+
steps: [
|
|
35
|
+
{ keyword: 'When', text: 'CreateWorkout', docString: { id: 'w1' } },
|
|
36
|
+
{ keyword: 'Then', text: 'WorkoutCreated', docString: { id: 'w1' } },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'command',
|
|
48
|
+
name: 'log-exercises',
|
|
49
|
+
stream: 'workouts-${workoutId}',
|
|
50
|
+
client: { specs: [] },
|
|
51
|
+
server: {
|
|
52
|
+
description: '',
|
|
53
|
+
specs: [
|
|
54
|
+
{
|
|
55
|
+
type: 'gherkin',
|
|
56
|
+
feature: 'Log exercises',
|
|
57
|
+
rules: [
|
|
58
|
+
{
|
|
59
|
+
name: 'rule',
|
|
60
|
+
examples: [
|
|
61
|
+
{
|
|
62
|
+
name: 'ex',
|
|
63
|
+
steps: [
|
|
64
|
+
{ keyword: 'When', text: 'LogExercises', docString: { workoutId: 'w1' } },
|
|
65
|
+
{ keyword: 'Then', text: 'ExerciseLogged', docString: { workoutId: 'w1' } },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const findEventSourceFn = (_fls: Narrative[], eventType: string) => {
|
|
80
|
+
if (eventType === 'WorkoutCreated') return { flowName: 'workout-flow', sliceName: 'create-workout' };
|
|
81
|
+
if (eventType === 'ExerciseLogged') return { flowName: 'workout-flow', sliceName: 'log-exercises' };
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const result = buildEventIdFieldMap(events, flows, findEventSourceFn);
|
|
86
|
+
expect(result).toEqual(
|
|
87
|
+
new Map([
|
|
88
|
+
['WorkoutCreated', 'id'],
|
|
89
|
+
['ExerciseLogged', 'workoutId'],
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns undefined when all events share the same id field', () => {
|
|
95
|
+
const events: Message[] = [
|
|
96
|
+
{ type: 'WorkoutCreated', fields: [] },
|
|
97
|
+
{ type: 'WorkoutUpdated', fields: [] },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const flows: Narrative[] = [
|
|
101
|
+
{
|
|
102
|
+
name: 'workout-flow',
|
|
103
|
+
slices: [
|
|
104
|
+
{
|
|
105
|
+
type: 'command',
|
|
106
|
+
name: 'manage-workout',
|
|
107
|
+
stream: 'workouts-${id}',
|
|
108
|
+
client: { specs: [] },
|
|
109
|
+
server: {
|
|
110
|
+
description: '',
|
|
111
|
+
specs: [
|
|
112
|
+
{
|
|
113
|
+
type: 'gherkin',
|
|
114
|
+
feature: 'Manage workout',
|
|
115
|
+
rules: [
|
|
116
|
+
{
|
|
117
|
+
name: 'rule',
|
|
118
|
+
examples: [
|
|
119
|
+
{
|
|
120
|
+
name: 'ex1',
|
|
121
|
+
steps: [
|
|
122
|
+
{ keyword: 'When', text: 'CreateWorkout', docString: { id: 'w1' } },
|
|
123
|
+
{ keyword: 'Then', text: 'WorkoutCreated', docString: { id: 'w1' } },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'ex2',
|
|
128
|
+
steps: [
|
|
129
|
+
{ keyword: 'When', text: 'UpdateWorkout', docString: { id: 'w1' } },
|
|
130
|
+
{ keyword: 'Then', text: 'WorkoutUpdated', docString: { id: 'w1' } },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const findEventSourceFn = (_fls: Narrative[], _eventType: string) => ({
|
|
145
|
+
flowName: 'workout-flow',
|
|
146
|
+
sliceName: 'manage-workout',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = buildEventIdFieldMap(events, flows, findEventSourceFn);
|
|
150
|
+
expect(result).toEqual(
|
|
151
|
+
new Map([
|
|
152
|
+
['WorkoutCreated', 'id'],
|
|
153
|
+
['WorkoutUpdated', 'id'],
|
|
154
|
+
]),
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns undefined when no producing slice is found', () => {
|
|
159
|
+
const events: Message[] = [{ type: 'UnknownEvent', fields: [] }];
|
|
160
|
+
const flows: Narrative[] = [];
|
|
161
|
+
const findEventSourceFn = () => null;
|
|
162
|
+
|
|
163
|
+
const result = buildEventIdFieldMap(events, flows, findEventSourceFn);
|
|
164
|
+
expect(result).toEqual(undefined);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('skips events from slices with composite keys', () => {
|
|
168
|
+
const events: Message[] = [{ type: 'UserJoined', fields: [] }];
|
|
169
|
+
|
|
170
|
+
const flows: Narrative[] = [
|
|
171
|
+
{
|
|
172
|
+
name: 'flow',
|
|
173
|
+
slices: [
|
|
174
|
+
{
|
|
175
|
+
type: 'command',
|
|
176
|
+
name: 'join',
|
|
177
|
+
stream: 'membership-${userId}-${projectId}',
|
|
178
|
+
client: { specs: [] },
|
|
179
|
+
server: {
|
|
180
|
+
description: '',
|
|
181
|
+
specs: [
|
|
182
|
+
{
|
|
183
|
+
type: 'gherkin',
|
|
184
|
+
feature: 'Join',
|
|
185
|
+
rules: [
|
|
186
|
+
{
|
|
187
|
+
name: 'rule',
|
|
188
|
+
examples: [
|
|
189
|
+
{
|
|
190
|
+
name: 'ex',
|
|
191
|
+
steps: [
|
|
192
|
+
{ keyword: 'When', text: 'Join', docString: { userId: 'u1', projectId: 'p1' } },
|
|
193
|
+
{ keyword: 'Then', text: 'UserJoined', docString: { userId: 'u1', projectId: 'p1' } },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
const findEventSourceFn = () => ({ flowName: 'flow', sliceName: 'join' });
|
|
208
|
+
|
|
209
|
+
const result = buildEventIdFieldMap(events, flows, findEventSourceFn);
|
|
210
|
+
expect(result).toEqual(undefined);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Slice } from '@auto-engineer/narrative';
|
|
1
|
+
import type { Narrative, Slice } from '@auto-engineer/narrative';
|
|
2
|
+
import type { Message } from '../types';
|
|
3
|
+
import { extractStreamIdFields } from './data-sink';
|
|
2
4
|
|
|
3
5
|
interface ProjectionOrigin {
|
|
4
6
|
type: 'projection';
|
|
@@ -58,6 +60,25 @@ export function extractProjectionName(slice: Slice): string | undefined {
|
|
|
58
60
|
return extractProjectionField(slice, 'name');
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
export function buildEventIdFieldMap(
|
|
64
|
+
events: Message[],
|
|
65
|
+
flows: Narrative[],
|
|
66
|
+
findEventSourceFn: (flows: Narrative[], eventType: string) => { flowName: string; sliceName: string } | null,
|
|
67
|
+
): Map<string, string> | undefined {
|
|
68
|
+
const map = new Map<string, string>();
|
|
69
|
+
for (const event of events) {
|
|
70
|
+
const source = findEventSourceFn(flows, event.type);
|
|
71
|
+
if (!source) continue;
|
|
72
|
+
const slice = flows.find((f) => f.name === source.flowName)?.slices.find((s) => s.name === source.sliceName);
|
|
73
|
+
if (!slice || typeof slice.stream !== 'string') continue;
|
|
74
|
+
const fields = extractStreamIdFields(slice.stream);
|
|
75
|
+
if (fields.length === 1) {
|
|
76
|
+
map.set(event.type, fields[0]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return map.size > 0 ? map : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
export function extractProjectionSingleton(slice: Slice): boolean {
|
|
62
83
|
if (!('server' in slice)) return false;
|
|
63
84
|
const dataSource = slice.server?.data?.items?.[0];
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { MappingEntry } from '@auto-engineer/narrative';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { buildArgToStateFieldMap, type QueryGwtCondition } from './query';
|
|
4
|
+
|
|
5
|
+
describe('buildArgToStateFieldMap', () => {
|
|
6
|
+
it('maps arg to state field via matching example data values', () => {
|
|
7
|
+
const queryGwtMapping: QueryGwtCondition[] = [
|
|
8
|
+
{
|
|
9
|
+
description: 'fetch workout by workoutId',
|
|
10
|
+
when: {
|
|
11
|
+
queryAction: 'GetWorkout',
|
|
12
|
+
args: { workoutId: 'w1' },
|
|
13
|
+
},
|
|
14
|
+
then: [{ stateRef: 'WorkoutView', exampleData: { id: 'w1', name: 'Leg Day' } }],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
const stateFieldNames = new Set(['id', 'name']);
|
|
18
|
+
|
|
19
|
+
const result = buildArgToStateFieldMap(queryGwtMapping, stateFieldNames);
|
|
20
|
+
expect(result).toEqual({ workoutId: { field: 'id', operator: 'eq' } });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('skips args that already match state field names', () => {
|
|
24
|
+
const queryGwtMapping: QueryGwtCondition[] = [
|
|
25
|
+
{
|
|
26
|
+
description: 'fetch by id',
|
|
27
|
+
when: {
|
|
28
|
+
queryAction: 'GetItem',
|
|
29
|
+
args: { id: 'item1' },
|
|
30
|
+
},
|
|
31
|
+
then: [{ stateRef: 'ItemView', exampleData: { id: 'item1', title: 'Test' } }],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
const stateFieldNames = new Set(['id', 'title']);
|
|
35
|
+
|
|
36
|
+
const result = buildArgToStateFieldMap(queryGwtMapping, stateFieldNames);
|
|
37
|
+
expect(result).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns empty map when no GWT data has query actions', () => {
|
|
41
|
+
const queryGwtMapping: QueryGwtCondition[] = [
|
|
42
|
+
{
|
|
43
|
+
description: 'event-driven query',
|
|
44
|
+
when: [{ eventRef: 'SomeEvent', exampleData: { id: '1' } }],
|
|
45
|
+
then: [{ stateRef: 'SomeState', exampleData: { id: '1' } }],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const stateFieldNames = new Set(['id']);
|
|
49
|
+
|
|
50
|
+
const result = buildArgToStateFieldMap(queryGwtMapping, stateFieldNames);
|
|
51
|
+
expect(result).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns empty map when GWT mapping is empty', () => {
|
|
55
|
+
const result = buildArgToStateFieldMap([], new Set(['id']));
|
|
56
|
+
expect(result).toEqual({});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('uses explicit mapping with operator', () => {
|
|
60
|
+
const mappings: MappingEntry[] = [
|
|
61
|
+
{
|
|
62
|
+
source: { type: 'Query', name: 'SearchListings', field: 'minBedrooms' },
|
|
63
|
+
target: { type: 'State', name: 'SearchResult', field: 'numBedrooms' },
|
|
64
|
+
operator: 'gte',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
const stateFieldNames = new Set(['numBedrooms', 'price']);
|
|
68
|
+
|
|
69
|
+
const result = buildArgToStateFieldMap([], stateFieldNames, mappings);
|
|
70
|
+
expect(result).toEqual({ minBedrooms: { field: 'numBedrooms', operator: 'gte' } });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('defaults to eq when explicit mapping has no operator', () => {
|
|
74
|
+
const mappings: MappingEntry[] = [
|
|
75
|
+
{
|
|
76
|
+
source: { type: 'Query', name: 'GetWorkout', field: 'workoutId' },
|
|
77
|
+
target: { type: 'State', name: 'WorkoutView', field: 'id' },
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const stateFieldNames = new Set(['id', 'name']);
|
|
81
|
+
|
|
82
|
+
const result = buildArgToStateFieldMap([], stateFieldNames, mappings);
|
|
83
|
+
expect(result).toEqual({ workoutId: { field: 'id', operator: 'eq' } });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('explicit mapping takes priority over GWT value match', () => {
|
|
87
|
+
const mappings: MappingEntry[] = [
|
|
88
|
+
{
|
|
89
|
+
source: { type: 'Query', name: 'SearchListings', field: 'minPrice' },
|
|
90
|
+
target: { type: 'State', name: 'Listing', field: 'pricePerNight' },
|
|
91
|
+
operator: 'gte',
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
const queryGwtMapping: QueryGwtCondition[] = [
|
|
95
|
+
{
|
|
96
|
+
description: 'search by price',
|
|
97
|
+
when: {
|
|
98
|
+
queryAction: 'SearchListings',
|
|
99
|
+
args: { minPrice: 100 },
|
|
100
|
+
},
|
|
101
|
+
then: [{ stateRef: 'Listing', exampleData: { pricePerNight: 100, title: 'Beach House' } }],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
const stateFieldNames = new Set(['pricePerNight', 'title']);
|
|
105
|
+
|
|
106
|
+
const result = buildArgToStateFieldMap(queryGwtMapping, stateFieldNames, mappings);
|
|
107
|
+
expect(result).toEqual({ minPrice: { field: 'pricePerNight', operator: 'gte' } });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('skips explicit mappings where target field is not in state', () => {
|
|
111
|
+
const mappings: MappingEntry[] = [
|
|
112
|
+
{
|
|
113
|
+
source: { type: 'Query', name: 'Search', field: 'minPrice' },
|
|
114
|
+
target: { type: 'State', name: 'Result', field: 'nonExistentField' },
|
|
115
|
+
operator: 'gte',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const stateFieldNames = new Set(['price', 'title']);
|
|
119
|
+
|
|
120
|
+
const result = buildArgToStateFieldMap([], stateFieldNames, mappings);
|
|
121
|
+
expect(result).toEqual({});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('skips explicit mappings where source type is not Query', () => {
|
|
125
|
+
const mappings: MappingEntry[] = [
|
|
126
|
+
{
|
|
127
|
+
source: { type: 'Command', name: 'CreateItem', field: 'id' },
|
|
128
|
+
target: { type: 'State', name: 'Item', field: 'id' },
|
|
129
|
+
operator: 'eq',
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const stateFieldNames = new Set(['id']);
|
|
133
|
+
|
|
134
|
+
const result = buildArgToStateFieldMap([], stateFieldNames, mappings);
|
|
135
|
+
expect(result).toEqual({});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { Slice } from '@auto-engineer/narrative';
|
|
1
|
+
import type { MappingEntry, Slice } from '@auto-engineer/narrative';
|
|
2
2
|
import type { EventRef, StateRef } from '../types';
|
|
3
3
|
import { extractGwtFromSpecs, isQueryAction, type QueryActionRef } from './step-converter';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type FilterOperator = NonNullable<MappingEntry['operator']>;
|
|
6
|
+
|
|
7
|
+
export interface ArgMapping {
|
|
8
|
+
field: string;
|
|
9
|
+
operator: FilterOperator;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QueryGwtCondition {
|
|
6
13
|
description: string;
|
|
7
14
|
given?: EventRef[];
|
|
8
15
|
when: EventRef[] | QueryActionRef;
|
|
@@ -34,3 +41,40 @@ export function buildQueryGwtMapping(slice: Slice): QueryGwtCondition[] {
|
|
|
34
41
|
};
|
|
35
42
|
});
|
|
36
43
|
}
|
|
44
|
+
|
|
45
|
+
export function buildArgToStateFieldMap(
|
|
46
|
+
queryGwtMapping: QueryGwtCondition[],
|
|
47
|
+
stateFieldNames: Set<string>,
|
|
48
|
+
mappings?: MappingEntry[],
|
|
49
|
+
): Record<string, ArgMapping> {
|
|
50
|
+
const map: Record<string, ArgMapping> = {};
|
|
51
|
+
|
|
52
|
+
if (mappings) {
|
|
53
|
+
for (const mapping of mappings) {
|
|
54
|
+
if (mapping.source.type !== 'Query') continue;
|
|
55
|
+
if (!stateFieldNames.has(mapping.target.field)) continue;
|
|
56
|
+
map[mapping.source.field] = {
|
|
57
|
+
field: mapping.target.field,
|
|
58
|
+
operator: mapping.operator ?? 'eq',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const gwt of queryGwtMapping) {
|
|
64
|
+
if (!isQueryAction(gwt.when)) continue;
|
|
65
|
+
const args = gwt.when.args;
|
|
66
|
+
const stateExample = gwt.then[0]?.exampleData;
|
|
67
|
+
if (!args || !stateExample) continue;
|
|
68
|
+
for (const [argName, argValue] of Object.entries(args)) {
|
|
69
|
+
if (stateFieldNames.has(argName)) continue;
|
|
70
|
+
if (map[argName]) continue;
|
|
71
|
+
for (const [fieldName, fieldValue] of Object.entries(stateExample)) {
|
|
72
|
+
if (stateFieldNames.has(fieldName) && argValue === fieldValue) {
|
|
73
|
+
map[argName] = { field: fieldName, operator: 'eq' };
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
}
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
sanitizeFieldType,
|
|
44
44
|
} from './extract';
|
|
45
45
|
import { getStreamFromSink } from './extract/data-sink';
|
|
46
|
+
import { buildEventIdFieldMap } from './extract/projection';
|
|
47
|
+
import { buildArgToStateFieldMap } from './extract/query';
|
|
46
48
|
import { normalizeSliceForTemplate } from './extract/slice-normalizer';
|
|
47
49
|
import { extractGwtSpecsFromSlice, type GwtResult } from './extract/step-converter';
|
|
48
50
|
import type { GwtCondition, Message, MessageDefinition } from './types';
|
|
@@ -635,6 +637,7 @@ async function prepareTemplateData(
|
|
|
635
637
|
projectionSingleton: boolean | undefined,
|
|
636
638
|
allMessages?: MessageDefinition[],
|
|
637
639
|
integrations?: Model['integrations'],
|
|
640
|
+
flows?: Narrative[],
|
|
638
641
|
): Promise<Record<string, unknown>> {
|
|
639
642
|
debug('Preparing template data for slice: %s (flow: %s)', slice.name, flow.name);
|
|
640
643
|
debug(' Commands: %d', commands.length);
|
|
@@ -699,6 +702,13 @@ async function prepareTemplateData(
|
|
|
699
702
|
}
|
|
700
703
|
}
|
|
701
704
|
|
|
705
|
+
const eventIdFieldMap =
|
|
706
|
+
slice.type === 'query' && flows ? buildEventIdFieldMap(events, flows, findEventSource) : undefined;
|
|
707
|
+
|
|
708
|
+
const stateFieldNames = new Set((queryMessage?.fields ?? []).map((f) => f.name));
|
|
709
|
+
const argToStateFieldMap =
|
|
710
|
+
slice.type === 'query' ? buildArgToStateFieldMap(queryGwtMapping, stateFieldNames, slice.mappings) : undefined;
|
|
711
|
+
|
|
702
712
|
return {
|
|
703
713
|
flowName: flow.name,
|
|
704
714
|
sliceName: slice.name,
|
|
@@ -727,6 +737,8 @@ async function prepareTemplateData(
|
|
|
727
737
|
localEvents,
|
|
728
738
|
referencedTypes,
|
|
729
739
|
eventCommandPairs,
|
|
740
|
+
eventIdFieldMap,
|
|
741
|
+
argToStateFieldMap,
|
|
730
742
|
};
|
|
731
743
|
}
|
|
732
744
|
|
|
@@ -903,6 +915,7 @@ async function generateFilesForSlice(
|
|
|
903
915
|
extracted.projectionSingleton,
|
|
904
916
|
messages,
|
|
905
917
|
integrations,
|
|
918
|
+
flows,
|
|
906
919
|
);
|
|
907
920
|
|
|
908
921
|
debugSlice(' Generating %d files from templates', filteredTemplates.length);
|