@auto-engineer/server-generator-apollo-emmett 0.11.8 → 0.11.10
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/CHANGELOG.md +18 -0
- package/dist/src/codegen/extract/index.d.ts +1 -0
- package/dist/src/codegen/extract/index.d.ts.map +1 -1
- package/dist/src/codegen/extract/index.js +1 -0
- package/dist/src/codegen/extract/index.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +13 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -0
- package/dist/src/codegen/extract/type-helpers.js +98 -0
- package/dist/src/codegen/extract/type-helpers.js.map +1 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +202 -19
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +47 -47
- package/dist/src/codegen/templates/command/decide.specs.ts +4 -0
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/dist/src/codegen/templates/command/register.specs.ts +1 -1
- package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
- package/dist/src/codegen/templates/command/state.specs.ts +51 -47
- package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
- package/dist/src/codegen/templates/query/projection.specs.ts +19 -2
- package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +6 -3
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
- package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
- package/dist/src/commands/generate-server.d.ts +0 -1
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +42 -23
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/graphql-types.d.ts +10 -0
- package/dist/src/domain/shared/graphql-types.d.ts.map +1 -0
- package/dist/src/domain/shared/graphql-types.js +40 -0
- package/dist/src/domain/shared/graphql-types.js.map +1 -0
- package/dist/src/domain/shared/graphql-types.ts +20 -0
- package/dist/src/domain/shared/index.d.ts +1 -0
- package/dist/src/domain/shared/index.d.ts.map +1 -1
- package/dist/src/domain/shared/index.js +1 -0
- package/dist/src/domain/shared/index.js.map +1 -1
- package/dist/src/domain/shared/index.ts +1 -0
- package/dist/src/domain/shared/sendCommand.d.ts +1 -1
- package/dist/src/domain/shared/sendCommand.d.ts.map +1 -1
- package/dist/src/domain/shared/sendCommand.ts +1 -1
- package/dist/src/domain/shared/types.d.ts +5 -7
- package/dist/src/domain/shared/types.d.ts.map +1 -1
- package/dist/src/domain/shared/types.js +11 -38
- package/dist/src/domain/shared/types.js.map +1 -1
- package/dist/src/domain/shared/types.ts +10 -16
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/index.ts +1 -0
- package/src/codegen/extract/type-helpers.ts +102 -0
- package/src/codegen/scaffoldFromSchema.ts +273 -15
- package/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/src/codegen/templates/command/decide.specs.specs.ts +47 -47
- package/src/codegen/templates/command/decide.specs.ts +4 -0
- package/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/src/codegen/templates/command/events.ts.ejs +16 -13
- package/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/src/codegen/templates/command/register.specs.ts +1 -1
- package/src/codegen/templates/command/register.ts.ejs +1 -4
- package/src/codegen/templates/command/state.specs.ts +51 -47
- package/src/codegen/templates/command/state.ts.ejs +8 -4
- package/src/codegen/templates/query/projection.specs.ts +19 -2
- package/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/src/codegen/templates/query/query.resolver.specs.ts +6 -3
- package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/src/codegen/templates/react/react.ts.ejs +0 -1
- package/src/codegen/templates/react/register.ts.ejs +0 -1
- package/src/commands/generate-server.ts +50 -25
- package/src/domain/shared/graphql-types.ts +20 -0
- package/src/domain/shared/index.ts +1 -0
- package/src/domain/shared/sendCommand.ts +1 -1
- package/src/domain/shared/types.ts +10 -16
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-format.log +0 -4
- package/.turbo/turbo-lint.log +0 -4
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-type-check.log +0 -4
|
@@ -95,57 +95,57 @@ describe('spec.ts.ejs', () => {
|
|
|
95
95
|
const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
|
|
96
96
|
|
|
97
97
|
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
"import { describe, it } from 'vitest';
|
|
99
|
+
import { DeciderSpecification } from '@event-driven-io/emmett';
|
|
100
|
+
import { decide } from './decide';
|
|
101
|
+
import { evolve } from './evolve';
|
|
102
|
+
import { initialState, State } from './state';
|
|
103
|
+
import type { ListingCreated } from './events';
|
|
104
|
+
import type { CreateListing } from './commands';
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
describe('Should create listing successfully', () => {
|
|
107
|
+
type Events = ListingCreated;
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const given = DeciderSpecification.for<CreateListing, Events, State>({
|
|
110
|
+
decide,
|
|
111
|
+
evolve,
|
|
112
|
+
initialState,
|
|
113
|
+
});
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
115
|
+
it('User creates listing with valid data', () => {
|
|
116
|
+
given([])
|
|
117
|
+
.when({
|
|
118
|
+
type: 'CreateListing',
|
|
119
|
+
data: {
|
|
120
|
+
propertyId: 'listing_123',
|
|
121
|
+
title: 'blah',
|
|
122
|
+
pricePerNight: 250,
|
|
123
|
+
maxGuests: 4,
|
|
124
|
+
amenities: ['wifi', 'kitchen'],
|
|
125
|
+
available: true,
|
|
126
|
+
tags: ['some tag'],
|
|
127
|
+
rating: 4.8,
|
|
128
|
+
metadata: { foo: 'bar' },
|
|
129
|
+
listedAt: new Date('2024-01-15T10:00:00Z'),
|
|
130
|
+
},
|
|
131
|
+
metadata: { now: new Date() },
|
|
132
|
+
})
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
.then([
|
|
135
|
+
{
|
|
136
|
+
type: 'ListingCreated',
|
|
137
|
+
data: {
|
|
138
|
+
propertyId: 'listing_123',
|
|
139
|
+
listedAt: new Date('2024-01-15T10:00:00Z'),
|
|
140
|
+
rating: 4.8,
|
|
141
|
+
metadata: { foo: 'bar' },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
"
|
|
148
|
+
`);
|
|
149
149
|
});
|
|
150
150
|
it('should include given events in the spec file when provided', async () => {
|
|
151
151
|
const spec: SpecsSchema = {
|
|
@@ -93,6 +93,7 @@ describe('decide.ts.ejs', () => {
|
|
|
93
93
|
* - Validate the command input fields
|
|
94
94
|
* - Inspect the current domain \`state\` to determine if the command is allowed
|
|
95
95
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
96
|
+
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
96
97
|
* - If valid, return one or more events with the correct structure
|
|
97
98
|
*
|
|
98
99
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -218,6 +219,7 @@ describe('decide.ts.ejs', () => {
|
|
|
218
219
|
* - Validate the command input fields
|
|
219
220
|
* - Inspect the current domain \`state\` to determine if the command is allowed
|
|
220
221
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
222
|
+
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
221
223
|
* - If valid, return one or more events with the correct structure
|
|
222
224
|
*
|
|
223
225
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -358,6 +360,7 @@ describe('decide.ts.ejs', () => {
|
|
|
358
360
|
* - Validate the command input fields
|
|
359
361
|
* - Inspect the current domain \`state\` to determine if the command is allowed
|
|
360
362
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
363
|
+
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
361
364
|
* - If valid, return one or more events with the correct structure
|
|
362
365
|
*
|
|
363
366
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -545,6 +548,7 @@ describe('decide.ts.ejs', () => {
|
|
|
545
548
|
* - Inspect the current domain \`state\` to determine if the command is allowed
|
|
546
549
|
* - Use \`products\` (integration result) to enrich or filter the output
|
|
547
550
|
* - If invalid, throw one of the following domain errors: \`NotFoundError\`, \`ValidationError\`, or \`IllegalStateError\`
|
|
551
|
+
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
548
552
|
* - If valid, return one or more events with the correct structure
|
|
549
553
|
*
|
|
550
554
|
* ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
|
|
@@ -71,6 +71,7 @@ case '<%= command %>': {
|
|
|
71
71
|
* - Use `<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
|
|
72
72
|
<% } -%>
|
|
73
73
|
* - If invalid, throw one of the following domain errors: `NotFoundError`, `ValidationError`, or `IllegalStateError`
|
|
74
|
+
* ⚠️ Error constructors: NotFoundError takes { id, type, message? }, while IllegalStateError/ValidationError take string
|
|
74
75
|
* - If valid, return one or more events with the correct structure
|
|
75
76
|
*
|
|
76
77
|
* ⚠️ Only read from inputs — never mutate them. `evolve.ts` handles state updates.
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
<%
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
<% } -%>
|
|
14
17
|
<% } -%>
|
|
@@ -323,6 +323,7 @@ describe('mutation.resolver.ts.ejs', () => {
|
|
|
323
323
|
|
|
324
324
|
expect(mutationFile?.contents).toMatchInlineSnapshot(`
|
|
325
325
|
"import { Mutation, Resolver, Arg, Ctx, Field, InputType } from 'type-graphql';
|
|
326
|
+
import { GraphQLJSON } from 'graphql-type-json';
|
|
326
327
|
import { type GraphQLContext, sendCommand, MutationResponse } from '../../../shared';
|
|
327
328
|
|
|
328
329
|
@InputType()
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
<%
|
|
2
|
-
function isInlineObject(ts) {
|
|
3
|
-
return /^\{[\s\S]*\}$/.test((ts ?? '').trim());
|
|
4
|
-
}
|
|
5
|
-
function isInlineObjectArray(ts) {
|
|
6
|
-
const t = (ts ?? '').trim();
|
|
7
|
-
return /^Array<\{[\s\S]*\}>$/.test(t) || /^\{[\s\S]*\}\[\]$/.test(t);
|
|
8
|
-
}
|
|
9
|
-
function baseTs(ts) {
|
|
10
|
-
return (ts ?? 'string').replace(/\s*\|\s*null\b/g, '').trim();
|
|
11
|
-
}
|
|
12
|
-
function fieldUsesDate(ts) {
|
|
13
|
-
const b = baseTs(ts);
|
|
14
|
-
if (b === 'Date') return true;
|
|
15
|
-
if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*Date\b/.test(b);
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
function fieldUsesJSON(ts) {
|
|
19
|
-
const b = baseTs(ts);
|
|
20
|
-
if (b === 'unknown' || b === 'any' || b === 'object') return true;
|
|
21
|
-
if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*(unknown|any|object)\b/.test(b);
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
2
|
const cmd = commands[0];
|
|
25
3
|
const usesDate = cmd.fields.some(f => fieldUsesDate(f.tsType));
|
|
26
4
|
const usesJSON = cmd.fields.some(f => fieldUsesJSON(f.tsType));
|
|
5
|
+
const enumList = collectEnumNames(cmd.fields);
|
|
27
6
|
|
|
28
7
|
const embeddedInputs = [];
|
|
29
8
|
for (const f of cmd.fields) {
|
|
@@ -38,7 +17,7 @@ for (const f of cmd.fields) {
|
|
|
38
17
|
%>
|
|
39
18
|
import { Mutation, Resolver, Arg, Ctx, Field, InputType<% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
40
19
|
<% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
|
|
41
|
-
<% } %>import { type GraphQLContext, sendCommand, MutationResponse } from '../../../shared';
|
|
20
|
+
<% } %>import { type GraphQLContext, sendCommand, MutationResponse<% if (enumList.length > 0) { %>, <%= enumList.join(', ') %><% } %> } from '../../../shared';
|
|
42
21
|
|
|
43
22
|
<% for (const { typeName, tsType } of embeddedInputs) {
|
|
44
23
|
const inner = tsType.trim().startsWith('Array<')
|
|
@@ -107,7 +107,7 @@ describe('generateScaffoldFilePlans', () => {
|
|
|
107
107
|
import type { CreateListing } from './commands';
|
|
108
108
|
|
|
109
109
|
export function register(messageBus: CommandProcessor, eventStore: EventStore) {
|
|
110
|
-
messageBus.handle((command: CreateListing) => handle(eventStore, command), 'CreateListing');
|
|
110
|
+
messageBus.handle<CreateListing>((command: CreateListing) => handle(eventStore, command), 'CreateListing');
|
|
111
111
|
}
|
|
112
112
|
"
|
|
113
113
|
`);
|
|
@@ -4,9 +4,6 @@ import type { <%= commands.map(c => pascalCase(c.type)).join(', ') %> } from './
|
|
|
4
4
|
|
|
5
5
|
export function register(messageBus: CommandProcessor, eventStore: EventStore) {
|
|
6
6
|
<% for (const command of commands) { -%>
|
|
7
|
-
|
|
8
|
-
(command: <%= pascalCase(command.type) %>) => handle(eventStore, command),
|
|
9
|
-
'<%= command.type %>'
|
|
10
|
-
);
|
|
7
|
+
messageBus.handle<<%= pascalCase(command.type) %>>((command: <%= pascalCase(command.type) %>) => handle(eventStore, command), '<%= command.type %>');
|
|
11
8
|
<% } -%>
|
|
12
9
|
}
|
|
@@ -82,54 +82,58 @@ describe('state.ts.ejs', () => {
|
|
|
82
82
|
const stateFile = plans.find((p) => p.outputPath.endsWith('state.ts'));
|
|
83
83
|
|
|
84
84
|
expect(stateFile?.contents).toMatchInlineSnapshot(`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
85
|
+
"/**
|
|
86
|
+
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
87
|
+
*
|
|
88
|
+
* Define the shape of the domain state for the current slice below. This state is used by \`decide.ts\`
|
|
89
|
+
* to determine whether a command is valid.
|
|
90
|
+
*
|
|
91
|
+
* The state is evolved over time by applying domain events (in \`evolve.ts\`).
|
|
92
|
+
* Each event updates the state incrementally based on business rules.
|
|
93
|
+
*
|
|
94
|
+
* Guidelines:
|
|
95
|
+
* - Include only fields that are **read** during command validation.
|
|
96
|
+
* - Use discriminated unions with string literal types (e.g., \`status: 'pending' | 'done'\`) to model state transitions.
|
|
97
|
+
* - IMPORTANT: If an enum exists in domain/shared/types.ts for your field (e.g., Status enum), use the enum constant type instead (e.g., \`status: Status.PENDING\`).
|
|
98
|
+
* - Prefer primitive types: \`string\`, \`boolean\`, \`number\`.
|
|
99
|
+
* - Use objects or maps only when structure is essential for decision logic.
|
|
100
|
+
*
|
|
101
|
+
* Do NOT include:
|
|
102
|
+
* - Redundant data already emitted in events unless required to enforce business rules.
|
|
103
|
+
* - Fields used only for projections, UI, or query purposes.
|
|
104
|
+
*
|
|
105
|
+
* ### Example (for a Task domain):
|
|
106
|
+
*
|
|
107
|
+
* \`\`\`ts
|
|
108
|
+
* export type PendingTask = {
|
|
109
|
+
* status: 'pending';
|
|
110
|
+
* };
|
|
111
|
+
*
|
|
112
|
+
* export type InProgressTask = {
|
|
113
|
+
* status: 'in_progress';
|
|
114
|
+
* startedAt: string;
|
|
115
|
+
* };
|
|
116
|
+
*
|
|
117
|
+
* export type CompletedTask = {
|
|
118
|
+
* status: 'completed';
|
|
119
|
+
* completedAt: string;
|
|
120
|
+
* };
|
|
121
|
+
*
|
|
122
|
+
* export type State = PendingTask | InProgressTask | CompletedTask;
|
|
123
|
+
* \`\`\`
|
|
124
|
+
*
|
|
125
|
+
* Note: Status string literals should match your schema's enum values (usually snake_case).
|
|
126
|
+
* If an enum is defined in domain/shared/types.ts, reference the enum type instead of literals.
|
|
127
|
+
*/
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
// TODO: Replace with a discriminated union of domain states for the current slice
|
|
130
|
+
export type State = {};
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// TODO: Replace the Return with the initial domain state of the current slice
|
|
133
|
+
export const initialState = (): State => {
|
|
134
|
+
return {};
|
|
135
|
+
};
|
|
136
|
+
"
|
|
137
|
+
`);
|
|
134
138
|
});
|
|
135
139
|
});
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Guidelines:
|
|
11
11
|
* - Include only fields that are **read** during command validation.
|
|
12
|
-
* - Use discriminated unions (e.g., `status: '
|
|
12
|
+
* - Use discriminated unions with string literal types (e.g., `status: 'pending' | 'done'`) to model state transitions.
|
|
13
|
+
* - IMPORTANT: If an enum exists in domain/shared/types.ts for your field (e.g., Status enum), use the enum constant type instead (e.g., `status: Status.PENDING`).
|
|
13
14
|
* - Prefer primitive types: `string`, `boolean`, `number`.
|
|
14
15
|
* - Use objects or maps only when structure is essential for decision logic.
|
|
15
16
|
*
|
|
@@ -21,21 +22,24 @@
|
|
|
21
22
|
*
|
|
22
23
|
* ```ts
|
|
23
24
|
* export type PendingTask = {
|
|
24
|
-
* status: '
|
|
25
|
+
* status: 'pending';
|
|
25
26
|
* };
|
|
26
27
|
*
|
|
27
28
|
* export type InProgressTask = {
|
|
28
|
-
* status: '
|
|
29
|
+
* status: 'in_progress';
|
|
29
30
|
* startedAt: string;
|
|
30
31
|
* };
|
|
31
32
|
*
|
|
32
33
|
* export type CompletedTask = {
|
|
33
|
-
* status: '
|
|
34
|
+
* status: 'completed';
|
|
34
35
|
* completedAt: string;
|
|
35
36
|
* };
|
|
36
37
|
*
|
|
37
38
|
* export type State = PendingTask | InProgressTask | CompletedTask;
|
|
38
39
|
* ```
|
|
40
|
+
*
|
|
41
|
+
* Note: Status string literals should match your schema's enum values (usually snake_case).
|
|
42
|
+
* If an enum is defined in domain/shared/types.ts, reference the enum type instead of literals.
|
|
39
43
|
*/
|
|
40
44
|
|
|
41
45
|
// TODO: Replace with a discriminated union of domain states for the current slice
|
|
@@ -227,8 +227,24 @@ describe('projection.ts.ejs', () => {
|
|
|
227
227
|
case 'ListingCreated': {
|
|
228
228
|
/**
|
|
229
229
|
* ## IMPLEMENTATION INSTRUCTIONS ##
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* Implement how this event updates the projection.
|
|
231
|
+
*
|
|
232
|
+
* **IMPORTANT - Internal State Pattern:**
|
|
233
|
+
* If you need to track state beyond the public AvailableListings type (e.g., to calculate
|
|
234
|
+
* aggregations, track previous values, etc.), follow this pattern:
|
|
235
|
+
*
|
|
236
|
+
* 1. Define an extended interface BEFORE the projection:
|
|
237
|
+
* interface InternalAvailableListings extends AvailableListings {
|
|
238
|
+
* internalField: SomeType;
|
|
239
|
+
* }
|
|
240
|
+
*
|
|
241
|
+
* 2. Cast document parameter to extended type:
|
|
242
|
+
* const current: InternalAvailableListings = (document as InternalAvailableListings) || { ...defaults };
|
|
243
|
+
*
|
|
244
|
+
* 3. Cast return values to extended type:
|
|
245
|
+
* return { ...allFields, internalField } as InternalAvailableListings;
|
|
246
|
+
*
|
|
247
|
+
* This keeps internal state separate from the public GraphQL schema.
|
|
232
248
|
*/
|
|
233
249
|
return {
|
|
234
250
|
propertyId: /* TODO: map from event.data */ '',
|
|
@@ -329,6 +345,7 @@ describe('projection.ts.ejs', () => {
|
|
|
329
345
|
@Field(() => String)
|
|
330
346
|
items!: string;
|
|
331
347
|
|
|
348
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
332
349
|
[key: string]: unknown;
|
|
333
350
|
}
|
|
334
351
|
|
|
@@ -10,6 +10,30 @@ if (eventImportGroups.length > 0) {
|
|
|
10
10
|
import type { <%= group.eventTypes.join(', ') %> } from '<%= group.importPath %>';
|
|
11
11
|
<%
|
|
12
12
|
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Collect enums used in state fields
|
|
16
|
+
const stateEnums = [];
|
|
17
|
+
const targetName = slice.server?.data?.[0]?.target?.name;
|
|
18
|
+
if (targetName && messages) {
|
|
19
|
+
const targetDef = messages.find(m => m.name === targetName);
|
|
20
|
+
if (targetDef?.fields) {
|
|
21
|
+
for (const field of targetDef.fields) {
|
|
22
|
+
const fieldType = (field.type || '').replace(/\s*\|\s*null/g, '').trim();
|
|
23
|
+
if (fieldType.includes('|') && (fieldType.includes('"') || fieldType.includes("'"))) {
|
|
24
|
+
const enumName = toTsFieldType(field.type);
|
|
25
|
+
if (enumName && enumName !== 'String' && !stateEnums.includes(enumName)) {
|
|
26
|
+
stateEnums.push(enumName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (stateEnums.length > 0) {
|
|
34
|
+
%>
|
|
35
|
+
import { <%= stateEnums.join(', ') %> } from '../../../shared';
|
|
36
|
+
<%
|
|
13
37
|
} -%>
|
|
14
38
|
|
|
15
39
|
type AllEvents = <%= allEventTypes %>;
|
|
@@ -76,8 +100,24 @@ case '<%= event.type %>': {
|
|
|
76
100
|
* - If the intent is to **soft delete**, consider adding a `status` field (e.g., `status: 'removed'`).
|
|
77
101
|
* - Ensure consumers of this projection (e.g., UI) handle the chosen approach appropriately.
|
|
78
102
|
<% } else { -%>
|
|
79
|
-
*
|
|
80
|
-
*
|
|
103
|
+
* Implement how this event updates the projection.
|
|
104
|
+
*
|
|
105
|
+
* **IMPORTANT - Internal State Pattern:**
|
|
106
|
+
* If you need to track state beyond the public <%= pascalCase(targetName || 'State') %> type (e.g., to calculate
|
|
107
|
+
* aggregations, track previous values, etc.), follow this pattern:
|
|
108
|
+
*
|
|
109
|
+
* 1. Define an extended interface BEFORE the projection:
|
|
110
|
+
* interface Internal<%= pascalCase(targetName || 'State') %> extends <%= pascalCase(targetName || 'State') %> {
|
|
111
|
+
* internalField: SomeType;
|
|
112
|
+
* }
|
|
113
|
+
*
|
|
114
|
+
* 2. Cast document parameter to extended type:
|
|
115
|
+
* const current: Internal<%= pascalCase(targetName || 'State') %> = (document as Internal<%= pascalCase(targetName || 'State') %>) || { ...defaults };
|
|
116
|
+
*
|
|
117
|
+
* 3. Cast return values to extended type:
|
|
118
|
+
* return { ...allFields, internalField } as Internal<%= pascalCase(targetName || 'State') %>;
|
|
119
|
+
*
|
|
120
|
+
* This keeps internal state separate from the public GraphQL schema.
|
|
81
121
|
<% } -%>
|
|
82
122
|
*/
|
|
83
123
|
<% if (isRemovalEvent) { -%>
|
|
@@ -94,16 +134,28 @@ case '<%= event.type %>': {
|
|
|
94
134
|
const type = def?.type ?? 'string';
|
|
95
135
|
|
|
96
136
|
let placeholder = 'undefined';
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
137
|
+
const isNullable = type.includes('| null') || type.includes('|null');
|
|
138
|
+
const baseType = isNullable ? type.replace(/\s*\|\s*null/g, '').trim() : type;
|
|
139
|
+
|
|
140
|
+
if (baseType === 'string' || baseType === 'ID') {
|
|
141
|
+
placeholder = isNullable ? "/* TODO: map from event.data */ null" : "/* TODO: map from event.data */ ''";
|
|
142
|
+
} else if (baseType === 'number') {
|
|
143
|
+
placeholder = isNullable ? '/* TODO: map from event.data */ null' : '/* TODO: map from event.data */ 0';
|
|
144
|
+
} else if (baseType === 'boolean') {
|
|
145
|
+
placeholder = isNullable ? '/* TODO: map from event.data */ null' : '/* TODO: map from event.data */ false';
|
|
146
|
+
} else if (baseType === 'Date') {
|
|
147
|
+
placeholder = isNullable ? '/* TODO: map from event.data */ null' : '/* TODO: map from event.data */ new Date()';
|
|
148
|
+
} else if (baseType.startsWith('Array<')) {
|
|
149
|
+
placeholder = isNullable ? '/* TODO: map from event.data */ null' : '/* TODO: map from event.data */ []';
|
|
150
|
+
} else if (baseType.includes('|') && (baseType.includes('"') || baseType.includes("'"))) {
|
|
151
|
+
const enumName = toTsFieldType(type);
|
|
152
|
+
const firstValue = baseType.match(/['"]([^'"]+)['"]/)?.[1] ?? '';
|
|
153
|
+
const enumConstant = firstValue ? firstValue.toUpperCase().replace(/[^A-Z0-9]/g, '_') : '';
|
|
154
|
+
if (enumName && enumName !== 'String') {
|
|
155
|
+
placeholder = `${enumName}.${enumConstant} /* TODO: verify this enum constant is correct */`;
|
|
156
|
+
} else {
|
|
157
|
+
placeholder = `/* TODO: use enum constant from types.ts */ '${firstValue}'`;
|
|
158
|
+
}
|
|
107
159
|
} else {
|
|
108
160
|
placeholder = '/* TODO: map from event.data */ undefined as any';
|
|
109
161
|
}
|
|
@@ -87,6 +87,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
87
87
|
@Field(() => Float)
|
|
88
88
|
maxGuests!: number;
|
|
89
89
|
|
|
90
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
90
91
|
[key: string]: unknown;
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -217,6 +218,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
217
218
|
@Field(() => [SuggestedItemsItems])
|
|
218
219
|
items!: SuggestedItemsItems[];
|
|
219
220
|
|
|
221
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
220
222
|
[key: string]: unknown;
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -360,7 +362,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
360
362
|
expect(queryFile?.contents).toMatchInlineSnapshot(`
|
|
361
363
|
"import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID } from 'type-graphql';
|
|
362
364
|
import { GraphQLJSON } from 'graphql-type-json';
|
|
363
|
-
import { type GraphQLContext, ReadModel } from '../../../shared';
|
|
365
|
+
import { type GraphQLContext, ReadModel, QuestionnaireProgressStatus } from '../../../shared';
|
|
364
366
|
|
|
365
367
|
@ObjectType()
|
|
366
368
|
export class QuestionnaireProgressAnswers {
|
|
@@ -379,8 +381,8 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
379
381
|
@Field(() => String)
|
|
380
382
|
participantId!: string;
|
|
381
383
|
|
|
382
|
-
@Field(() =>
|
|
383
|
-
status!:
|
|
384
|
+
@Field(() => QuestionnaireProgressStatus)
|
|
385
|
+
status!: QuestionnaireProgressStatus;
|
|
384
386
|
|
|
385
387
|
@Field(() => String, { nullable: true })
|
|
386
388
|
currentQuestionId?: string | null;
|
|
@@ -391,6 +393,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
391
393
|
@Field(() => [QuestionnaireProgressAnswers])
|
|
392
394
|
answers!: QuestionnaireProgressAnswers[];
|
|
393
395
|
|
|
396
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
394
397
|
[key: string]: unknown;
|
|
395
398
|
}
|
|
396
399
|
|