@auto-engineer/server-generator-apollo-emmett 1.128.2 → 1.130.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 +76 -0
- 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 +2 -1
- 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/command/handle.specs.ts +1 -1
- package/dist/src/codegen/templates/command/handle.ts.ejs +1 -1
- 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/type-helpers.specs.ts +23 -0
- package/src/codegen/extract/type-helpers.ts +4 -0
- package/src/codegen/scaffoldFromSchema.ts +2 -0
- 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/command/handle.specs.ts +1 -1
- package/src/codegen/templates/command/handle.ts.ejs +1 -1
- 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/narrative": "1.
|
|
36
|
-
"@auto-engineer/message-bus": "1.
|
|
35
|
+
"@auto-engineer/narrative": "1.130.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.130.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.130.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.130.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",
|
|
@@ -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,6 +32,7 @@ import {
|
|
|
32
32
|
createFieldUsesFloat,
|
|
33
33
|
createFieldUsesJSON,
|
|
34
34
|
createIsEnumType,
|
|
35
|
+
escapeJsString,
|
|
35
36
|
extractMessagesFromSpecs,
|
|
36
37
|
extractProjectionName,
|
|
37
38
|
findPrimitiveLinkingField,
|
|
@@ -402,6 +403,7 @@ async function renderTemplate(
|
|
|
402
403
|
|
|
403
404
|
const result = await template({
|
|
404
405
|
...data,
|
|
406
|
+
escapeJsString,
|
|
405
407
|
pascalCase,
|
|
406
408
|
toKebabCase,
|
|
407
409
|
camelCase,
|
|
@@ -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
|
|
|
@@ -557,7 +557,7 @@ describe('generateScaffoldFilePlans', () => {
|
|
|
557
557
|
const handleFile = plans.find((p) => p.outputPath.endsWith('handle.ts'));
|
|
558
558
|
|
|
559
559
|
expect(handleFile?.contents).toMatchInlineSnapshot(`
|
|
560
|
-
"import { randomUUID } from 'crypto';
|
|
560
|
+
"import { randomUUID } from 'node:crypto';
|
|
561
561
|
|
|
562
562
|
import { CommandHandler, type EventStore, type MessageHandlerResult } from '@event-driven-io/emmett';
|
|
563
563
|
import { evolve } from './evolve';
|
|
@@ -69,7 +69,7 @@ const uuidVars = needsStreamGuard ? streamVars.filter(v => !varInAnyCommand(v))
|
|
|
69
69
|
%>
|
|
70
70
|
|
|
71
71
|
<% if (uuidVars.length > 0) { -%>
|
|
72
|
-
import { randomUUID } from 'crypto';
|
|
72
|
+
import { randomUUID } from 'node:crypto';
|
|
73
73
|
<% } -%>
|
|
74
74
|
<% integrationSideEffectImports.forEach((importSource) => { %>
|
|
75
75
|
import '<%= importSource %>';
|
|
@@ -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)
|