@auto-engineer/server-generator-apollo-emmett 1.129.0 → 1.131.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 +65 -0
- package/dist/src/codegen/extract/imports.d.ts +0 -5
- package/dist/src/codegen/extract/imports.d.ts.map +1 -1
- package/dist/src/codegen/extract/imports.js +0 -7
- package/dist/src/codegen/extract/imports.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +1 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +3 -0
- package/dist/src/codegen/extract/type-helpers.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +5 -3
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +3 -3
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -1
- package/dist/src/codegen/templates/query/events.specs.ts +112 -0
- package/dist/src/codegen/templates/query/events.ts.ejs +17 -0
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +125 -0
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +2 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +5 -6
- package/package.json +4 -4
- package/src/codegen/extract/imports.ts +0 -8
- package/src/codegen/extract/type-helpers.specs.ts +23 -0
- package/src/codegen/extract/type-helpers.ts +4 -0
- package/src/codegen/scaffoldFromSchema.ts +7 -3
- package/src/codegen/templates/command/decide.specs.ts.ejs +3 -3
- package/src/codegen/templates/command/decide.ts.ejs +1 -1
- package/src/codegen/templates/query/events.specs.ts +112 -0
- package/src/codegen/templates/query/events.ts.ejs +17 -0
- package/src/codegen/templates/query/projection.specs.specs.ts +125 -0
- package/src/codegen/templates/query/projection.specs.ts.ejs +2 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
package/ketchup-plan.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
# Ketchup Plan: Fix
|
|
1
|
+
# Ketchup Plan: Fix Unescaped Quotes in EJS Template String Literals
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
|
-
(
|
|
5
|
+
- [x] Burst 1: Add `escapeJsString` utility + unit tests + wire into renderTemplate (7a5544a6)
|
|
6
|
+
- [x] Burst 2: Fix `projection.specs.ts.ejs` interpolation points (d108c530)
|
|
7
|
+
- [x] Burst 3: Fix `decide.specs.ts.ejs` and `decide.ts.ejs` interpolation points (f9b46ada)
|
|
8
|
+
- [x] Burst 4: Fix `react.specs.ts.ejs` interpolation points
|
|
6
9
|
|
|
7
10
|
## DONE
|
|
8
|
-
|
|
9
|
-
- [x] Burst 1: Align ReactorLike return type with MessageHandlerResult (3b1217fc)
|
|
10
|
-
- [x] Burst 2: Rename `then` → `thenSends` to eliminate thenable risk (c65fa28d)
|
|
11
|
-
- [x] Burst 3: Prefix unused aggregateStream state var with `_` (04fd4cbb)
|
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/message-bus": "1.131.0",
|
|
36
|
+
"@auto-engineer/narrative": "1.131.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.131.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.131.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",
|
|
@@ -51,14 +51,6 @@ export function groupEventImports(context: CrossSliceImportContext): ImportGroup
|
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
* Filters events to only include those from the current slice (source === 'then').
|
|
56
|
-
* Used for generating local event definitions.
|
|
57
|
-
*/
|
|
58
|
-
export function getLocalEvents(events: Message[]): Message[] {
|
|
59
|
-
return events.filter((event) => event.source === 'then');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
54
|
/**
|
|
63
55
|
* Extracts all unique event types from a list of events.
|
|
64
56
|
*/
|
|
@@ -2,6 +2,7 @@ import { parseInlineObjectFields } from '@auto-engineer/narrative';
|
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import {
|
|
4
4
|
createFieldUsesJSON,
|
|
5
|
+
escapeJsString,
|
|
5
6
|
findPrimitiveLinkingField,
|
|
6
7
|
isPrimitiveTsType,
|
|
7
8
|
isValidTsIdentifier,
|
|
@@ -172,6 +173,28 @@ describe('findPrimitiveLinkingField', () => {
|
|
|
172
173
|
});
|
|
173
174
|
});
|
|
174
175
|
|
|
176
|
+
describe('escapeJsString', () => {
|
|
177
|
+
it('escapes single quotes', () => {
|
|
178
|
+
expect(escapeJsString("user's workouts")).toBe("user\\'s workouts");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('escapes backslashes', () => {
|
|
182
|
+
expect(escapeJsString('path\\to\\file')).toBe('path\\\\to\\\\file');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('escapes newlines and carriage returns', () => {
|
|
186
|
+
expect(escapeJsString('line1\nline2\rline3')).toBe('line1\\nline2\\rline3');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('escapes backslashes before single quotes', () => {
|
|
190
|
+
expect(escapeJsString("it\\'s")).toBe("it\\\\\\'s");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('returns plain strings unchanged', () => {
|
|
194
|
+
expect(escapeJsString('no special chars')).toBe('no special chars');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
175
198
|
describe('createFieldUsesJSON', () => {
|
|
176
199
|
const stubGraphqlType = (ts: string): string => {
|
|
177
200
|
if (ts === 'unknown' || ts === 'any' || ts === 'object') return 'GraphQLJSON';
|
|
@@ -105,6 +105,10 @@ export function isPrimitiveTsType(tsType: string): boolean {
|
|
|
105
105
|
return ['string', 'number', 'boolean', 'Date', 'ID'].includes(baseTs(tsType));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export function escapeJsString(value: string): string {
|
|
109
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
export function findPrimitiveLinkingField(
|
|
109
113
|
stateFields: Array<{ name: string; type?: string; tsType?: string }>,
|
|
110
114
|
eventFields: Array<{ name: string; type?: string; tsType?: string }>,
|
|
@@ -32,11 +32,11 @@ import {
|
|
|
32
32
|
createFieldUsesFloat,
|
|
33
33
|
createFieldUsesJSON,
|
|
34
34
|
createIsEnumType,
|
|
35
|
+
escapeJsString,
|
|
35
36
|
extractMessagesFromSpecs,
|
|
36
37
|
extractProjectionName,
|
|
37
38
|
findPrimitiveLinkingField,
|
|
38
39
|
getAllEventTypes,
|
|
39
|
-
getLocalEvents,
|
|
40
40
|
groupEventImports,
|
|
41
41
|
isPrimitiveTsType,
|
|
42
42
|
isValidTsIdentifier,
|
|
@@ -83,7 +83,7 @@ const defaultFilesByType: Record<string, string[]> = {
|
|
|
83
83
|
'decide.specs.ts.ejs',
|
|
84
84
|
'register.ts.ejs',
|
|
85
85
|
],
|
|
86
|
-
query: ['projection.ts.ejs', 'state.ts.ejs', 'projection.specs.ts.ejs', 'query.resolver.ts.ejs'],
|
|
86
|
+
query: ['events.ts.ejs', 'projection.ts.ejs', 'state.ts.ejs', 'projection.specs.ts.ejs', 'query.resolver.ts.ejs'],
|
|
87
87
|
react: ['events.ts.ejs', 'react.ts.ejs', 'react.specs.ts.ejs', 'register.ts.ejs'],
|
|
88
88
|
};
|
|
89
89
|
|
|
@@ -402,6 +402,7 @@ async function renderTemplate(
|
|
|
402
402
|
|
|
403
403
|
const result = await template({
|
|
404
404
|
...data,
|
|
405
|
+
escapeJsString,
|
|
405
406
|
pascalCase,
|
|
406
407
|
toKebabCase,
|
|
407
408
|
camelCase,
|
|
@@ -665,7 +666,10 @@ async function prepareTemplateData(
|
|
|
665
666
|
const eventImportGroups = groupEventImports({ currentSliceName: slice.name, currentFlowName: flow.name, events });
|
|
666
667
|
const allEventTypesArray = getAllEventTypes(events);
|
|
667
668
|
const allEventTypes = createEventUnionType(events);
|
|
668
|
-
const
|
|
669
|
+
const selfImportedTypes = new Set(
|
|
670
|
+
eventImportGroups.filter((g) => g.importPath === './events').flatMap((g) => g.eventTypes),
|
|
671
|
+
);
|
|
672
|
+
const localEvents = events.filter((e) => selfImportedTypes.has(e.type));
|
|
669
673
|
|
|
670
674
|
const messagesByName = new Map((allMessages ?? []).map((m) => [m.name, m]));
|
|
671
675
|
const targetName = 'server' in slice ? slice.server?.data?.items?.[0]?.target?.name : undefined;
|
|
@@ -110,7 +110,7 @@ import type { <%= group.eventTypes.join(', ') %> } from '<%= group.importPath %>
|
|
|
110
110
|
<% } -%>
|
|
111
111
|
import type { <%= Object.keys(commandSchemasByName).join(', ') %> } from './commands';
|
|
112
112
|
<% for (const [ruleDescription, ruleGwts] of ruleGroups.entries()) { %>
|
|
113
|
-
describe('<%= ruleDescription %>', () => {
|
|
113
|
+
describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
114
114
|
|
|
115
115
|
type Events = <%= uniqueEventTypes.length > 0 ? uniqueEventTypes.join(' | ') : 'never' %>;
|
|
116
116
|
|
|
@@ -134,7 +134,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
134
134
|
? `should throw ${errorResult.errorType} when ${gwt.failingFields?.join(', ') || 'invalid input'}`
|
|
135
135
|
: `should emit ${eventResults.map(e => e.eventRef).join(', ')} for ${commandName}`);
|
|
136
136
|
%>
|
|
137
|
-
it('<%= testDescription %>', () => {
|
|
137
|
+
it('<%= escapeJsString(testDescription) %>', () => {
|
|
138
138
|
given([
|
|
139
139
|
<%_ if (gwt.given && gwt.given.length) { _%>
|
|
140
140
|
<%- gwt.given.map(g => `{
|
|
@@ -158,7 +158,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
158
158
|
metadata: { now: <%= derivedDate ? `new Date('${derivedDate}')` : 'new Date()' %> },
|
|
159
159
|
})
|
|
160
160
|
<% if (errorResult) { %>
|
|
161
|
-
.thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= errorResult.message || '' %>');
|
|
161
|
+
.thenThrows((err) => err instanceof <%= errorResult.errorType %> && err.message === '<%= escapeJsString(errorResult.message || '') %>');
|
|
162
162
|
<% } else { %>
|
|
163
163
|
|
|
164
164
|
.then(expectEvents(
|
|
@@ -95,7 +95,7 @@ if (error && gwt.failingFields?.length) {
|
|
|
95
95
|
const condition = gwt.failingFields.map(field => `command.data.${field} === ''`).join(' || ');
|
|
96
96
|
-%>
|
|
97
97
|
if (<%- condition %>) {
|
|
98
|
-
throw new <%= error.errorType %>('
|
|
98
|
+
throw new <%= error.errorType %>('<%= escapeJsString(error.message ?? "Validation failed") %>');
|
|
99
99
|
}
|
|
100
100
|
<% } } -%>
|
|
101
101
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Model } from '@auto-engineer/narrative';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
4
|
+
|
|
5
|
+
describe('query events.ts.ejs', () => {
|
|
6
|
+
it('generates events.ts for orphan events not produced by any command slice', async () => {
|
|
7
|
+
const spec: Model = {
|
|
8
|
+
variant: 'specs',
|
|
9
|
+
narratives: [
|
|
10
|
+
{
|
|
11
|
+
name: 'appointment-flow',
|
|
12
|
+
slices: [
|
|
13
|
+
{
|
|
14
|
+
type: 'query',
|
|
15
|
+
name: 'view-appointments',
|
|
16
|
+
stream: 'appointments',
|
|
17
|
+
client: { specs: [] },
|
|
18
|
+
server: {
|
|
19
|
+
description: 'projection for booked appointments',
|
|
20
|
+
data: {
|
|
21
|
+
items: [
|
|
22
|
+
{
|
|
23
|
+
target: { type: 'State', name: 'Appointment' },
|
|
24
|
+
origin: {
|
|
25
|
+
type: 'projection',
|
|
26
|
+
name: 'AppointmentsProjection',
|
|
27
|
+
idField: 'appointmentId',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
specs: [
|
|
33
|
+
{
|
|
34
|
+
type: 'gherkin',
|
|
35
|
+
feature: 'View appointments query',
|
|
36
|
+
rules: [
|
|
37
|
+
{
|
|
38
|
+
name: 'Should project appointments',
|
|
39
|
+
examples: [
|
|
40
|
+
{
|
|
41
|
+
name: 'Appointment booked shows in list',
|
|
42
|
+
steps: [
|
|
43
|
+
{
|
|
44
|
+
keyword: 'When',
|
|
45
|
+
text: 'AppointmentBooked',
|
|
46
|
+
docString: {
|
|
47
|
+
appointmentId: 'appt_1',
|
|
48
|
+
date: '2024-06-15',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
keyword: 'Then',
|
|
53
|
+
text: 'Appointment',
|
|
54
|
+
docString: {
|
|
55
|
+
appointmentId: 'appt_1',
|
|
56
|
+
date: '2024-06-15',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: [
|
|
72
|
+
{
|
|
73
|
+
type: 'event',
|
|
74
|
+
name: 'AppointmentBooked',
|
|
75
|
+
source: 'internal',
|
|
76
|
+
fields: [
|
|
77
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
78
|
+
{ name: 'date', type: 'string', required: true },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: 'state',
|
|
83
|
+
name: 'Appointment',
|
|
84
|
+
fields: [
|
|
85
|
+
{ name: 'appointmentId', type: 'string', required: true },
|
|
86
|
+
{ name: 'date', type: 'string', required: true },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
93
|
+
|
|
94
|
+
const eventFile = plans.find((p) => p.outputPath.endsWith('view-appointments/events.ts'));
|
|
95
|
+
expect(eventFile).toBeDefined();
|
|
96
|
+
expect(eventFile?.contents).toMatchInlineSnapshot(`
|
|
97
|
+
"import type { Event } from '@event-driven-io/emmett';
|
|
98
|
+
|
|
99
|
+
export type AppointmentBooked = Event<
|
|
100
|
+
'AppointmentBooked',
|
|
101
|
+
{
|
|
102
|
+
appointmentId: string;
|
|
103
|
+
date: string;
|
|
104
|
+
}
|
|
105
|
+
>;
|
|
106
|
+
"
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
const projectionFile = plans.find((p) => p.outputPath.endsWith('view-appointments/projection.ts'));
|
|
110
|
+
expect(projectionFile?.contents).toContain("import type { AppointmentBooked } from './events'");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%
|
|
2
|
+
const enumList = collectEnumNames(localEvents.flatMap(e => e.fields));
|
|
3
|
+
%><% if (localEvents.length) { -%>
|
|
4
|
+
import type { Event } from '@event-driven-io/emmett';
|
|
5
|
+
<% if (enumList.length > 0) { %>import { <%= enumList.join(', ') %> } from '../../../shared';
|
|
6
|
+
<% } %>
|
|
7
|
+
<% for (const event of localEvents) { -%>
|
|
8
|
+
export type <%= pascalCase(event.type) %> = Event<
|
|
9
|
+
'<%= event.type %>',
|
|
10
|
+
{
|
|
11
|
+
<% for (const field of event.fields) { -%>
|
|
12
|
+
<%- field.name %>: <%- toTsFieldType(field.tsType) %>;
|
|
13
|
+
<% } -%>
|
|
14
|
+
}
|
|
15
|
+
>;
|
|
16
|
+
<% } -%>
|
|
17
|
+
<% } -%>
|
|
@@ -2616,4 +2616,129 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
2616
2616
|
"
|
|
2617
2617
|
`);
|
|
2618
2618
|
});
|
|
2619
|
+
|
|
2620
|
+
it('should escape apostrophes in rule names and test descriptions', async () => {
|
|
2621
|
+
const spec: SpecsSchema = {
|
|
2622
|
+
variant: 'specs',
|
|
2623
|
+
narratives: [
|
|
2624
|
+
{
|
|
2625
|
+
name: 'workout-flow',
|
|
2626
|
+
slices: [
|
|
2627
|
+
{
|
|
2628
|
+
type: 'command',
|
|
2629
|
+
name: 'log-workout',
|
|
2630
|
+
stream: 'workouts-${memberId}',
|
|
2631
|
+
client: { specs: [] },
|
|
2632
|
+
server: {
|
|
2633
|
+
description: '',
|
|
2634
|
+
specs: [
|
|
2635
|
+
{
|
|
2636
|
+
type: 'gherkin',
|
|
2637
|
+
feature: 'Log workout',
|
|
2638
|
+
rules: [
|
|
2639
|
+
{
|
|
2640
|
+
name: 'Should log',
|
|
2641
|
+
examples: [
|
|
2642
|
+
{
|
|
2643
|
+
name: 'Logs workout',
|
|
2644
|
+
steps: [
|
|
2645
|
+
{
|
|
2646
|
+
keyword: 'When',
|
|
2647
|
+
text: 'LogWorkout',
|
|
2648
|
+
docString: { memberId: 'mem_001', caloriesBurned: 250 },
|
|
2649
|
+
},
|
|
2650
|
+
{
|
|
2651
|
+
keyword: 'Then',
|
|
2652
|
+
text: 'WorkoutRecorded',
|
|
2653
|
+
docString: { memberId: 'mem_001', caloriesBurned: 250 },
|
|
2654
|
+
},
|
|
2655
|
+
],
|
|
2656
|
+
},
|
|
2657
|
+
],
|
|
2658
|
+
},
|
|
2659
|
+
],
|
|
2660
|
+
},
|
|
2661
|
+
],
|
|
2662
|
+
},
|
|
2663
|
+
},
|
|
2664
|
+
{
|
|
2665
|
+
type: 'query',
|
|
2666
|
+
name: 'view-user-workouts',
|
|
2667
|
+
stream: 'workouts',
|
|
2668
|
+
client: { specs: [] },
|
|
2669
|
+
server: {
|
|
2670
|
+
description: '',
|
|
2671
|
+
data: {
|
|
2672
|
+
items: [
|
|
2673
|
+
{
|
|
2674
|
+
target: { type: 'State', name: 'UserWorkouts' },
|
|
2675
|
+
origin: { type: 'projection', name: 'UserWorkoutsProjection', idField: 'memberId' },
|
|
2676
|
+
},
|
|
2677
|
+
],
|
|
2678
|
+
},
|
|
2679
|
+
specs: [
|
|
2680
|
+
{
|
|
2681
|
+
type: 'gherkin',
|
|
2682
|
+
feature: "View user's workouts",
|
|
2683
|
+
rules: [
|
|
2684
|
+
{
|
|
2685
|
+
name: "List of user's workouts",
|
|
2686
|
+
examples: [
|
|
2687
|
+
{
|
|
2688
|
+
name: "User's workout history",
|
|
2689
|
+
steps: [
|
|
2690
|
+
{
|
|
2691
|
+
keyword: 'When',
|
|
2692
|
+
text: 'WorkoutRecorded',
|
|
2693
|
+
docString: { memberId: 'mem_001', caloriesBurned: 300 },
|
|
2694
|
+
},
|
|
2695
|
+
{
|
|
2696
|
+
keyword: 'Then',
|
|
2697
|
+
text: 'UserWorkouts',
|
|
2698
|
+
docString: { totalCalories: 300 },
|
|
2699
|
+
},
|
|
2700
|
+
],
|
|
2701
|
+
},
|
|
2702
|
+
],
|
|
2703
|
+
},
|
|
2704
|
+
],
|
|
2705
|
+
},
|
|
2706
|
+
],
|
|
2707
|
+
},
|
|
2708
|
+
},
|
|
2709
|
+
],
|
|
2710
|
+
},
|
|
2711
|
+
],
|
|
2712
|
+
messages: [
|
|
2713
|
+
{
|
|
2714
|
+
type: 'command',
|
|
2715
|
+
name: 'LogWorkout',
|
|
2716
|
+
fields: [
|
|
2717
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
2718
|
+
{ name: 'caloriesBurned', type: 'number', required: true },
|
|
2719
|
+
],
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
type: 'event',
|
|
2723
|
+
name: 'WorkoutRecorded',
|
|
2724
|
+
source: 'internal',
|
|
2725
|
+
fields: [
|
|
2726
|
+
{ name: 'memberId', type: 'string', required: true },
|
|
2727
|
+
{ name: 'caloriesBurned', type: 'number', required: true },
|
|
2728
|
+
],
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
type: 'state',
|
|
2732
|
+
name: 'UserWorkouts',
|
|
2733
|
+
fields: [{ name: 'totalCalories', type: 'number', required: true }],
|
|
2734
|
+
},
|
|
2735
|
+
],
|
|
2736
|
+
} as SpecsSchema;
|
|
2737
|
+
|
|
2738
|
+
const { plans } = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
2739
|
+
const specFile = plans.find((p) => p.outputPath.endsWith('view-user-workouts/projection.specs.ts'));
|
|
2740
|
+
|
|
2741
|
+
expect(specFile?.contents).toContain('describe("List of user\'s workouts"');
|
|
2742
|
+
expect(specFile?.contents).toContain('it("User\'s workout history"');
|
|
2743
|
+
});
|
|
2619
2744
|
});
|
|
@@ -172,7 +172,7 @@ import { <%= TargetType %> } from './state';
|
|
|
172
172
|
type ProjectionEvent = <%= uniqueEventTypes.length ? uniqueEventTypes.join(' | ') : 'never' %>;
|
|
173
173
|
|
|
174
174
|
<% for (const [ruleDescription, ruleTests] of ruleGroups.entries()) { %>
|
|
175
|
-
describe('<%= ruleDescription %>', () => {
|
|
175
|
+
describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
176
176
|
let given: InMemoryProjectionSpec<ProjectionEvent>;
|
|
177
177
|
|
|
178
178
|
beforeEach(() => {
|
|
@@ -199,7 +199,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
199
199
|
: 'should handle events correctly');
|
|
200
200
|
_%>
|
|
201
201
|
|
|
202
|
-
it('<%= description %>', () =>
|
|
202
|
+
it('<%= escapeJsString(description) %>', () =>
|
|
203
203
|
given([<% if (givenEvents.length > 0) {
|
|
204
204
|
for (const evt of givenEvents) {
|
|
205
205
|
if (!evt.eventRef || evt.eventRef === '') continue;
|
|
@@ -85,7 +85,7 @@ type ReactorEvent = <%= allEventTypes.length ? allEventTypes.join(' | ') : 'neve
|
|
|
85
85
|
type ReactorCommand = <%= allCommandTypes.length ? allCommandTypes.join(' | ') : 'never' %>;
|
|
86
86
|
|
|
87
87
|
<% for (const [ruleDescription, ruleTests] of ruleGroups.entries()) { %>
|
|
88
|
-
describe('<%= ruleDescription %>', () => {
|
|
88
|
+
describe('<%= escapeJsString(ruleDescription) %>', () => {
|
|
89
89
|
let eventStore: InMemoryEventStore;
|
|
90
90
|
let given: ReactorSpecification<ReactorEvent, ReactorCommand, ReactorContext>;
|
|
91
91
|
let messageBus: CommandSender;
|
|
@@ -114,7 +114,7 @@ describe('<%= ruleDescription %>', () => {
|
|
|
114
114
|
const description = testCase.description ||
|
|
115
115
|
`should send ${thenCommands.map(c => c.commandRef).join(', ')} when ${exampleEvent.eventRef} is received`;
|
|
116
116
|
%>
|
|
117
|
-
it('<%= description %>', async () => {
|
|
117
|
+
it('<%= escapeJsString(description) %>', async () => {
|
|
118
118
|
<%
|
|
119
119
|
const givenStates = testCase.given.filter(g =>
|
|
120
120
|
messages.some(m => m.type === 'state' && m.name === g.eventRef)
|