@auto-engineer/server-generator-apollo-emmett 1.124.0 → 1.125.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 +56 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
- package/dist/src/codegen/extract/type-helpers.js +7 -5
- 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 +19 -1
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +8 -9
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
- package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.ts +2 -2
- package/dist/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/dist/src/codegen/templates/react/react.ts.ejs +138 -64
- package/dist/src/codegen/templates/react/react.ts.specs.ts +243 -1
- package/dist/src/codegen/templates/react/register.specs.ts +281 -14
- package/dist/src/codegen/templates/react/register.ts.ejs +100 -48
- package/dist/src/commands/generate-server.d.ts +1 -0
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +18 -0
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.d.ts +5 -5
- package/dist/src/domain/shared/reactorSpecification.d.ts.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.js +1 -2
- package/dist/src/domain/shared/reactorSpecification.js.map +1 -1
- package/dist/src/domain/shared/reactorSpecification.ts +7 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +4 -30
- package/package.json +4 -4
- package/src/codegen/extract/type-helpers.specs.ts +50 -1
- package/src/codegen/extract/type-helpers.ts +6 -3
- package/src/codegen/scaffoldFromSchema.ts +21 -1
- package/src/codegen/templates/command/mutation.resolver.specs.ts +0 -1
- package/src/codegen/templates/query/query.resolver.specs.ts +8 -9
- package/src/codegen/templates/query/query.resolver.ts.ejs +9 -3
- package/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.ts +2 -2
- package/src/codegen/templates/react/react.specs.ts.ejs +2 -2
- package/src/codegen/templates/react/react.ts.ejs +138 -64
- package/src/codegen/templates/react/react.ts.specs.ts +243 -1
- package/src/codegen/templates/react/register.specs.ts +281 -14
- package/src/codegen/templates/react/register.ts.ejs +100 -48
- package/src/commands/generate-server.specs.ts +32 -0
- package/src/commands/generate-server.ts +20 -0
- package/src/domain/shared/reactorSpecification.ts +7 -10
|
@@ -1,62 +1,114 @@
|
|
|
1
1
|
<%
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
given: firstExample.given,
|
|
6
|
-
when: firstExample.when,
|
|
7
|
-
then: firstExample.then
|
|
8
|
-
} : null;
|
|
9
|
-
const when = Array.isArray(gwt?.when) ? gwt.when[0] : gwt?.when;
|
|
10
|
-
const then = Array.isArray(gwt?.then) ? gwt.then[0] : gwt?.then;
|
|
2
|
+
if (!eventCommandPairs || eventCommandPairs.length === 0) {
|
|
3
|
+
throw new Error(`register.ts.ejs: slice "${slice.name}" has no event→command pairs — check specs`);
|
|
4
|
+
}
|
|
11
5
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
const importGroups = new Map();
|
|
7
|
+
for (const pair of eventCommandPairs) {
|
|
8
|
+
const event = events.find(e => e.type === pair.eventType);
|
|
9
|
+
const isCrossFlow = event?.sourceFlowName && event.sourceFlowName !== flowName;
|
|
10
|
+
const importBase = isCrossFlow
|
|
11
|
+
? `../../${toKebabCase(event.sourceFlowName)}/${toKebabCase(event.sourceSliceName)}`
|
|
12
|
+
: event?.sourceSliceName ? `../${toKebabCase(event.sourceSliceName)}` : '.';
|
|
13
|
+
if (!importGroups.has(importBase)) importGroups.set(importBase, []);
|
|
14
|
+
const typeName = pascalCase(pair.eventType);
|
|
15
|
+
if (!importGroups.get(importBase).includes(typeName)) {
|
|
16
|
+
importGroups.get(importBase).push(typeName);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
22
19
|
%>
|
|
23
20
|
import { type CommandSender, type EventSubscription, type EventStore } from '@event-driven-io/emmett';
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
<% for (const [importBase, types] of importGroups) { -%>
|
|
22
|
+
import type { <%= types.join(', ') %> } from '<%= importBase %>/events';
|
|
23
|
+
<% } %>
|
|
26
24
|
export async function register(
|
|
27
25
|
messageBus: CommandSender & EventSubscription,
|
|
28
26
|
eventStore: EventStore
|
|
29
27
|
) {
|
|
28
|
+
<% for (const pair of eventCommandPairs) {
|
|
29
|
+
const eventDef = messages.find(m => m.name === pair.eventType);
|
|
30
|
+
const commandDef = messages.find(m => m.name === pair.commandType && m.type === 'command');
|
|
31
|
+
const commandFields = (commandDef?.fields || []);
|
|
32
|
+
const eventFieldSet = new Set((eventDef?.fields || []).map(f => f.name));
|
|
33
|
+
const stateFieldSources = {};
|
|
34
|
+
-%>
|
|
30
35
|
messageBus.subscribe(
|
|
31
|
-
async (event: <%= pascalCase(eventType) %>) => {
|
|
32
|
-
/**
|
|
33
|
-
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
|
|
36
|
+
async (event: <%= pascalCase(pair.eventType) %>) => {
|
|
37
|
+
/**
|
|
38
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
39
|
+
*
|
|
40
|
+
* Complete the command send below. Field sources are pre-classified:
|
|
41
|
+
* - `event.data.<field>` → already wired from the triggering event.
|
|
42
|
+
* - `<stateVar>.<field>` → already wired from loaded aggregate state.
|
|
43
|
+
* - `undefined, // TODO: source unknown` → MUST be dynamically derived:
|
|
44
|
+
* compute from event.data, loaded state, or runtime logic.
|
|
45
|
+
* NEVER hardcode values copied from test assertions.
|
|
46
|
+
*
|
|
47
|
+
* Preserve all import paths above — they are generated from the model.
|
|
48
|
+
*
|
|
49
|
+
* CONSTRAINTS:
|
|
50
|
+
* - NEVER use `as SomeType` type assertions. Use typed variable declarations.
|
|
51
|
+
* - Only reference fields that exist on the event type (<%= pascalCase(pair.eventType) %>). Check the import above.
|
|
52
|
+
* - Do NOT modify the messageBus.subscribe() call, function signature, or import statements.
|
|
53
|
+
* - When event.data contains nested arrays/objects, iterate them. Do NOT cast to primitive types.
|
|
54
|
+
*/
|
|
55
|
+
<% if (eventDef?.fields?.length) { %>
|
|
56
|
+
// Event (<%= pair.eventType %>) fields: <%= eventDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
57
|
+
<% } -%>
|
|
58
|
+
<% if (commandDef?.fields?.length) { %>
|
|
59
|
+
// Command (<%= pair.commandType %>) fields: <%= commandDef.fields.map(f => f.name + ': ' + (f.tsType || f.type)).join(', ') %>
|
|
60
|
+
<% } -%>
|
|
61
|
+
<% if (states.length > 0) {
|
|
62
|
+
for (const state of states) {
|
|
63
|
+
const linkingField = findPrimitiveLinkingField(state.fields, eventDef?.fields || []);
|
|
64
|
+
if (linkingField) {
|
|
65
|
+
const varName = camelCase(state.type);
|
|
66
|
+
for (const f of state.fields) {
|
|
67
|
+
if (!stateFieldSources[f.name]) stateFieldSources[f.name] = varName;
|
|
68
|
+
}
|
|
69
|
+
const stateUsedByCommand = commandFields.some(f =>
|
|
70
|
+
!eventFieldSet.has(f.name) && state.fields.some(sf => sf.name === f.name)
|
|
71
|
+
);
|
|
72
|
+
const varPrefix = stateUsedByCommand ? '' : '_';
|
|
73
|
+
-%>
|
|
74
|
+
|
|
75
|
+
const { state: <%= varPrefix %><%= varName %> } = await eventStore.aggregateStream(
|
|
76
|
+
`<%= state.type %>-${event.data.<%= linkingField %>}`,
|
|
77
|
+
{
|
|
78
|
+
evolve: (currentState: Record<string, unknown>, evt: { type: string; data: Record<string, unknown> }) => ({ ...currentState, ...evt.data }),
|
|
79
|
+
initialState: (): Record<string, unknown> => ({}),
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
// <%= state.type %> fields: <%= state.fields.map(f => f.name).join(', ') %>
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// // Map event fields to command fields here
|
|
54
|
-
// // e.g., userId: event.data.userId,
|
|
55
|
-
// },
|
|
56
|
-
// });
|
|
84
|
+
<% }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
-%>
|
|
57
88
|
|
|
58
|
-
|
|
89
|
+
await messageBus.send({
|
|
90
|
+
type: '<%= pair.commandType %>',
|
|
91
|
+
kind: 'Command',
|
|
92
|
+
data: {
|
|
93
|
+
<% for (const field of commandFields) {
|
|
94
|
+
const fieldName = field.name;
|
|
95
|
+
if (eventFieldSet.has(fieldName)) {
|
|
96
|
+
-%>
|
|
97
|
+
<%= fieldName %>: event.data.<%= fieldName %>,
|
|
98
|
+
<% } else if (stateFieldSources[fieldName]) { -%>
|
|
99
|
+
<%= fieldName %>: <%= stateFieldSources[fieldName] %>.<%= fieldName %>,
|
|
100
|
+
<% } else { -%>
|
|
101
|
+
<%= fieldName %>: undefined, // TODO: source unknown
|
|
102
|
+
<% }
|
|
103
|
+
}
|
|
104
|
+
-%>
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return;
|
|
59
109
|
},
|
|
60
|
-
'<%= eventType %>'
|
|
110
|
+
'<%= pair.eventType %>'
|
|
61
111
|
);
|
|
112
|
+
|
|
113
|
+
<% } -%>
|
|
62
114
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
emitSliceGenerationFailedForDuplicates,
|
|
18
18
|
emitSliceGenerationFailedForFieldIssues,
|
|
19
19
|
saveGenerationState,
|
|
20
|
+
writeBiomeConfig,
|
|
20
21
|
writeHealthResolver,
|
|
21
22
|
writeModelToDisk,
|
|
22
23
|
} from './generate-server';
|
|
@@ -256,6 +257,37 @@ describe('writeHealthResolver', () => {
|
|
|
256
257
|
});
|
|
257
258
|
});
|
|
258
259
|
|
|
260
|
+
describe('writeBiomeConfig', () => {
|
|
261
|
+
let dir: string;
|
|
262
|
+
|
|
263
|
+
beforeEach(async () => {
|
|
264
|
+
dir = await mkdtemp(join(tmpdir(), 'biome-config-'));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
afterEach(async () => {
|
|
268
|
+
await rm(dir, { recursive: true, force: true });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('writes biome.json with decorator support and recommended linting', async () => {
|
|
272
|
+
await writeBiomeConfig(dir);
|
|
273
|
+
|
|
274
|
+
const content = JSON.parse(await readFile(join(dir, 'biome.json'), 'utf8'));
|
|
275
|
+
expect(content).toEqual({
|
|
276
|
+
javascript: {
|
|
277
|
+
parser: {
|
|
278
|
+
unsafeParameterDecoratorsEnabled: true,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
linter: {
|
|
282
|
+
enabled: true,
|
|
283
|
+
rules: {
|
|
284
|
+
recommended: true,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
259
291
|
describe('emitSliceGenerationFailedForFieldIssues', () => {
|
|
260
292
|
it('emits SliceGenerationFailed for command slice referencing message via gherkin step', () => {
|
|
261
293
|
const fieldIssues = [
|
|
@@ -284,6 +284,9 @@ async function writeConfigurationFiles(serverDir: string): Promise<void> {
|
|
|
284
284
|
|
|
285
285
|
debugFiles(`Writing vitest config... to ${serverDir}`);
|
|
286
286
|
await writeVitestConfig(serverDir);
|
|
287
|
+
|
|
288
|
+
debugFiles(`Writing biome.json... to ${serverDir}`);
|
|
289
|
+
await writeBiomeConfig(serverDir);
|
|
287
290
|
}
|
|
288
291
|
|
|
289
292
|
function createServerSuccessEvent(command: GenerateServerCommand, serverDir: string): ServerGeneratedEvent {
|
|
@@ -757,4 +760,21 @@ export default defineConfig({
|
|
|
757
760
|
await writeFile(path.join(dest, 'vitest.config.ts'), vitestConfig, 'utf-8');
|
|
758
761
|
}
|
|
759
762
|
|
|
763
|
+
export async function writeBiomeConfig(dest: string): Promise<void> {
|
|
764
|
+
const biomeConfig = {
|
|
765
|
+
javascript: {
|
|
766
|
+
parser: {
|
|
767
|
+
unsafeParameterDecoratorsEnabled: true,
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
linter: {
|
|
771
|
+
enabled: true,
|
|
772
|
+
rules: {
|
|
773
|
+
recommended: true,
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
await fs.writeJson(path.join(dest, 'biome.json'), biomeConfig, { spaces: 2 });
|
|
778
|
+
}
|
|
779
|
+
|
|
760
780
|
export default commandHandler;
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type CommandSender,
|
|
5
5
|
type ErrorConstructor,
|
|
6
6
|
isErrorConstructor,
|
|
7
|
-
type
|
|
7
|
+
type MessageHandlerResult,
|
|
8
8
|
} from '@event-driven-io/emmett';
|
|
9
9
|
|
|
10
10
|
type CommandCheck<CommandType> = (command: CommandType) => boolean;
|
|
@@ -22,7 +22,9 @@ interface ReactorSpecificationReturn<Event, Command, Context> {
|
|
|
22
22
|
event: Event | Event[],
|
|
23
23
|
context?: Context,
|
|
24
24
|
) => {
|
|
25
|
-
|
|
25
|
+
thenSends: (
|
|
26
|
+
expectedCommand: Command | Command[] | CommandCheck<Command> | CommandCheck<Command[]>,
|
|
27
|
+
) => Promise<void>;
|
|
26
28
|
thenNothingHappened: () => Promise<void>;
|
|
27
29
|
thenThrows: <ErrorType extends Error = Error>(...args: Parameters<ThenThrows<ErrorType>>) => Promise<void>;
|
|
28
30
|
};
|
|
@@ -60,12 +62,8 @@ function createMockCommandSender<Command>(): MockCommandSender<Command> {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
type ReactorLike<Event, Context> =
|
|
63
|
-
|
|
64
|
-
| {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
-
| { eachMessage: (event: Event, context: Context) => Promise<any> }
|
|
67
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
-
| MessageProcessor<any, any, any>;
|
|
65
|
+
| { handle(events: Event[], context: Context): MessageHandlerResult | Promise<MessageHandlerResult> }
|
|
66
|
+
| { eachMessage(event: Event, context: Context): MessageHandlerResult | Promise<MessageHandlerResult> };
|
|
69
67
|
|
|
70
68
|
function reactorSpecificationFor<Event, Command, Context extends { commandSender: CommandSender }>(
|
|
71
69
|
processorOrReactor: ReactorLike<Event, Context> | (() => ReactorLike<Event, Context>),
|
|
@@ -124,7 +122,7 @@ function reactorSpecificationFor<Event, Command, Context extends { commandSender
|
|
|
124
122
|
};
|
|
125
123
|
|
|
126
124
|
return {
|
|
127
|
-
|
|
125
|
+
thenSends: async (
|
|
128
126
|
expectedCommand: Command | Command[] | CommandCheck<Command> | CommandCheck<Command[]>,
|
|
129
127
|
): Promise<void> => {
|
|
130
128
|
try {
|
|
@@ -206,7 +204,6 @@ interface CommandWithMetadata {
|
|
|
206
204
|
metadata?: Record<string, unknown>;
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
// eslint-disable-next-line complexity
|
|
210
207
|
function assertCommandsMatch<Command>(actual: Command, expected: Command, index?: number): void {
|
|
211
208
|
const actualCopy = { ...actual } as CommandWithMetadata & Record<string, unknown>;
|
|
212
209
|
const expectedCopy = { ...expected } as CommandWithMetadata & Record<string, unknown>;
|