@auto-engineer/server-generator-apollo-emmett 0.11.9 → 0.11.11
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 +5 -6
- package/.turbo/turbo-format.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +4 -4
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/src/codegen/extract/commands.d.ts +1 -1
- package/dist/src/codegen/extract/commands.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.d.ts +1 -1
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/events.d.ts +1 -1
- package/dist/src/codegen/extract/events.d.ts.map +1 -1
- package/dist/src/codegen/extract/gwt.d.ts +1 -1
- package/dist/src/codegen/extract/gwt.d.ts.map +1 -1
- 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/messages.d.ts +1 -1
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +1 -1
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/query.d.ts +1 -1
- package/dist/src/codegen/extract/query.d.ts.map +1 -1
- package/dist/src/codegen/extract/states.d.ts +1 -1
- package/dist/src/codegen/extract/states.d.ts.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 +3 -3
- 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.specs.ts +3 -3
- package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +54 -54
- package/dist/src/codegen/templates/command/decide.specs.ts +13 -9
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/dist/src/codegen/templates/command/events.specs.ts +3 -3
- package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
- package/dist/src/codegen/templates/command/evolve.specs.ts +3 -3
- package/dist/src/codegen/templates/command/handle.specs.ts +10 -5
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/dist/src/codegen/templates/command/register.specs.ts +4 -4
- package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
- package/dist/src/codegen/templates/command/state.specs.ts +54 -50
- package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +7 -7
- package/dist/src/codegen/templates/query/projection.specs.ts +24 -7
- package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +19 -16
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/dist/src/codegen/templates/query/state.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
- package/dist/src/codegen/templates/react/register.specs.ts +3 -3
- package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
- package/dist/src/codegen/test-data/specVariant1.d.ts +1 -1
- package/dist/src/codegen/test-data/specVariant1.d.ts.map +1 -1
- package/dist/src/codegen/test-data/specVariant1.js +1 -1
- package/dist/src/codegen/test-data/specVariant1.js.map +1 -1
- package/dist/src/codegen/types.d.ts +1 -1
- package/dist/src/codegen/types.d.ts.map +1 -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 +53 -31
- 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/commands.ts +1 -1
- package/src/codegen/extract/data-sink.ts +1 -1
- package/src/codegen/extract/events.ts +1 -1
- package/src/codegen/extract/gwt.ts +1 -1
- package/src/codegen/extract/index.ts +1 -0
- package/src/codegen/extract/messages.ts +1 -1
- package/src/codegen/extract/projection.ts +1 -1
- package/src/codegen/extract/query.ts +1 -1
- package/src/codegen/extract/states.ts +1 -1
- package/src/codegen/extract/type-helpers.ts +102 -0
- package/src/codegen/scaffoldFromSchema.ts +283 -25
- package/src/codegen/templates/command/commands.specs.ts +3 -3
- package/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/src/codegen/templates/command/decide.specs.specs.ts +54 -54
- package/src/codegen/templates/command/decide.specs.ts +13 -9
- package/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/src/codegen/templates/command/events.specs.ts +3 -3
- package/src/codegen/templates/command/events.ts.ejs +16 -13
- package/src/codegen/templates/command/evolve.specs.ts +3 -3
- package/src/codegen/templates/command/handle.specs.ts +10 -5
- package/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/src/codegen/templates/command/register.specs.ts +4 -4
- package/src/codegen/templates/command/register.ts.ejs +1 -4
- package/src/codegen/templates/command/state.specs.ts +54 -50
- package/src/codegen/templates/command/state.ts.ejs +8 -4
- package/src/codegen/templates/query/projection.specs.specs.ts +7 -7
- package/src/codegen/templates/query/projection.specs.ts +24 -7
- package/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/src/codegen/templates/query/query.resolver.specs.ts +19 -16
- package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/src/codegen/templates/query/state.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.ts +3 -3
- package/src/codegen/templates/react/react.ts.ejs +0 -1
- package/src/codegen/templates/react/register.specs.ts +3 -3
- package/src/codegen/templates/react/register.ts.ejs +0 -1
- package/src/codegen/test-data/specVariant1.ts +2 -2
- package/src/codegen/types.ts +1 -1
- package/src/commands/generate-server.ts +63 -34
- 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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
3
|
-
import { Model as SpecsSchema } from '@auto-engineer/
|
|
3
|
+
import { Model as SpecsSchema } from '@auto-engineer/narrative';
|
|
4
4
|
|
|
5
5
|
describe('state.ts.ejs', () => {
|
|
6
6
|
it('should generate an initial state', async () => {
|
|
7
7
|
const spec: SpecsSchema = {
|
|
8
8
|
variant: 'specs',
|
|
9
|
-
|
|
9
|
+
narratives: [
|
|
10
10
|
{
|
|
11
11
|
name: 'Host creates a listing',
|
|
12
12
|
slices: [
|
|
@@ -78,58 +78,62 @@ describe('state.ts.ejs', () => {
|
|
|
78
78
|
],
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
81
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
3
|
-
import { Model as SpecsSchema } from '@auto-engineer/
|
|
3
|
+
import { Model as SpecsSchema } from '@auto-engineer/narrative';
|
|
4
4
|
|
|
5
5
|
describe('projection.specs.ts.ejs', () => {
|
|
6
6
|
it('should generate a valid test spec for a query slice projection', async () => {
|
|
7
7
|
const spec: SpecsSchema = {
|
|
8
8
|
variant: 'specs',
|
|
9
|
-
|
|
9
|
+
narratives: [
|
|
10
10
|
{
|
|
11
11
|
name: 'listing-flow',
|
|
12
12
|
slices: [
|
|
@@ -180,7 +180,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
180
180
|
],
|
|
181
181
|
} as SpecsSchema;
|
|
182
182
|
|
|
183
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
183
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
184
184
|
const specFile = plans.find((p) => p.outputPath.endsWith('projection.specs.ts'));
|
|
185
185
|
|
|
186
186
|
expect(specFile?.contents).toMatchInlineSnapshot(`
|
|
@@ -241,7 +241,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
241
241
|
it('should generate a valid test spec for a model with given/when/then pattern', async () => {
|
|
242
242
|
const questionnaireSpec: SpecsSchema = {
|
|
243
243
|
variant: 'specs',
|
|
244
|
-
|
|
244
|
+
narratives: [
|
|
245
245
|
{
|
|
246
246
|
name: 'Questionnaires',
|
|
247
247
|
slices: [
|
|
@@ -363,7 +363,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
363
363
|
} as SpecsSchema;
|
|
364
364
|
|
|
365
365
|
const plans = await generateScaffoldFilePlans(
|
|
366
|
-
questionnaireSpec.
|
|
366
|
+
questionnaireSpec.narratives,
|
|
367
367
|
questionnaireSpec.messages,
|
|
368
368
|
undefined,
|
|
369
369
|
'src/domain/flows',
|
|
@@ -380,7 +380,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
380
380
|
it('should include all events from both given and when clauses in projection imports and types', async () => {
|
|
381
381
|
const spec: SpecsSchema = {
|
|
382
382
|
variant: 'specs',
|
|
383
|
-
|
|
383
|
+
narratives: [
|
|
384
384
|
{
|
|
385
385
|
name: 'questionnaires',
|
|
386
386
|
slices: [
|
|
@@ -569,7 +569,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
569
569
|
} as SpecsSchema;
|
|
570
570
|
|
|
571
571
|
const plans = await generateScaffoldFilePlans(
|
|
572
|
-
spec.
|
|
572
|
+
spec.narratives,
|
|
573
573
|
[
|
|
574
574
|
{
|
|
575
575
|
type: 'command',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
3
|
-
import { Model } from '@auto-engineer/
|
|
3
|
+
import { Model } from '@auto-engineer/narrative';
|
|
4
4
|
|
|
5
5
|
describe('projection.ts.ejs', () => {
|
|
6
6
|
it('should generate a valid projection file with correct relative event import path from producing slice', async () => {
|
|
7
7
|
const flows: Model = {
|
|
8
8
|
variant: 'specs',
|
|
9
|
-
|
|
9
|
+
narratives: [
|
|
10
10
|
{
|
|
11
11
|
name: 'listing-flow',
|
|
12
12
|
slices: [
|
|
@@ -201,7 +201,7 @@ describe('projection.ts.ejs', () => {
|
|
|
201
201
|
],
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
-
const plans = await generateScaffoldFilePlans(flows.
|
|
204
|
+
const plans = await generateScaffoldFilePlans(flows.narratives, flows.messages, undefined, 'src/domain/flows');
|
|
205
205
|
const projectionFile = plans.find((p) => p.outputPath.endsWith('projection.ts'));
|
|
206
206
|
|
|
207
207
|
expect(projectionFile?.contents).toMatchInlineSnapshot(`
|
|
@@ -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 */ '',
|
|
@@ -263,7 +279,7 @@ describe('projection.ts.ejs', () => {
|
|
|
263
279
|
it('should generate a valid query resolver using ID type', async () => {
|
|
264
280
|
const spec: Model = {
|
|
265
281
|
variant: 'specs',
|
|
266
|
-
|
|
282
|
+
narratives: [
|
|
267
283
|
{
|
|
268
284
|
name: 'wishlist-flow',
|
|
269
285
|
slices: [
|
|
@@ -314,7 +330,7 @@ describe('projection.ts.ejs', () => {
|
|
|
314
330
|
],
|
|
315
331
|
};
|
|
316
332
|
|
|
317
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
333
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
318
334
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
319
335
|
|
|
320
336
|
expect(resolverFile?.contents).toMatchInlineSnapshot(`
|
|
@@ -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
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
|
|
3
|
-
import { Model as SpecsSchema } from '@auto-engineer/
|
|
3
|
+
import { Model as SpecsSchema } from '@auto-engineer/narrative';
|
|
4
4
|
|
|
5
5
|
describe('query.resolver.ts.ejs', () => {
|
|
6
6
|
it('should generate a valid query resolver from request field', async () => {
|
|
7
7
|
const spec: SpecsSchema = {
|
|
8
8
|
variant: 'specs',
|
|
9
|
-
|
|
9
|
+
narratives: [
|
|
10
10
|
{
|
|
11
11
|
name: 'listing-flow',
|
|
12
12
|
slices: [
|
|
@@ -63,7 +63,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
63
63
|
],
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
66
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
67
67
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
68
68
|
|
|
69
69
|
expect(resolverFile?.contents).toMatchInlineSnapshot(`
|
|
@@ -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
|
|
|
@@ -128,7 +129,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
128
129
|
it('should generate a valid query resolver with array of inline object field', async () => {
|
|
129
130
|
const spec: SpecsSchema = {
|
|
130
131
|
variant: 'specs',
|
|
131
|
-
|
|
132
|
+
narratives: [
|
|
132
133
|
{
|
|
133
134
|
name: 'assistant-flow',
|
|
134
135
|
slices: [
|
|
@@ -186,7 +187,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
186
187
|
],
|
|
187
188
|
};
|
|
188
189
|
|
|
189
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
190
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
190
191
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
191
192
|
|
|
192
193
|
expect(resolverFile?.contents).toMatchInlineSnapshot(`
|
|
@@ -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
|
|
|
@@ -252,7 +254,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
252
254
|
it('should generate the query resolver for "views the questionnaire"', async () => {
|
|
253
255
|
const spec: SpecsSchema = {
|
|
254
256
|
variant: 'specs',
|
|
255
|
-
|
|
257
|
+
narratives: [
|
|
256
258
|
{
|
|
257
259
|
name: 'Questionnaires',
|
|
258
260
|
slices: [
|
|
@@ -352,7 +354,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
352
354
|
integrations: [],
|
|
353
355
|
};
|
|
354
356
|
|
|
355
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
357
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
356
358
|
const queryFile = plans.find(
|
|
357
359
|
(p) => p.outputPath.endsWith('query.resolver.ts') && p.contents.includes('ViewsTheQuestionnaireQueryResolver'),
|
|
358
360
|
);
|
|
@@ -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
|
|
|
@@ -426,7 +429,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
426
429
|
it('should import Float when Float fields are used', async () => {
|
|
427
430
|
const spec: SpecsSchema = {
|
|
428
431
|
variant: 'specs',
|
|
429
|
-
|
|
432
|
+
narratives: [
|
|
430
433
|
{
|
|
431
434
|
name: 'product-flow',
|
|
432
435
|
slices: [
|
|
@@ -479,7 +482,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
479
482
|
],
|
|
480
483
|
};
|
|
481
484
|
|
|
482
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
485
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
483
486
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
484
487
|
|
|
485
488
|
expect(resolverFile?.contents).toContain(
|
|
@@ -490,7 +493,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
490
493
|
it('should import Float when array of numbers is used', async () => {
|
|
491
494
|
const spec: SpecsSchema = {
|
|
492
495
|
variant: 'specs',
|
|
493
|
-
|
|
496
|
+
narratives: [
|
|
494
497
|
{
|
|
495
498
|
name: 'stats-flow',
|
|
496
499
|
slices: [
|
|
@@ -541,7 +544,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
541
544
|
],
|
|
542
545
|
};
|
|
543
546
|
|
|
544
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
547
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
545
548
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
546
549
|
|
|
547
550
|
expect(resolverFile?.contents).toContain('Float');
|
|
@@ -550,7 +553,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
550
553
|
it('should import Float when Float query arg is used', async () => {
|
|
551
554
|
const spec: SpecsSchema = {
|
|
552
555
|
variant: 'specs',
|
|
553
|
-
|
|
556
|
+
narratives: [
|
|
554
557
|
{
|
|
555
558
|
name: 'search-flow',
|
|
556
559
|
slices: [
|
|
@@ -601,7 +604,7 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
601
604
|
],
|
|
602
605
|
};
|
|
603
606
|
|
|
604
|
-
const plans = await generateScaffoldFilePlans(spec.
|
|
607
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
605
608
|
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
606
609
|
|
|
607
610
|
expect(resolverFile?.contents).toContain('Float');
|
|
@@ -8,50 +8,6 @@ const message = messages?.find(m => m.name === viewType);
|
|
|
8
8
|
const resolverClassName = `${pascalCase(slice.name)}QueryResolver`;
|
|
9
9
|
const usesID = parsedRequest?.args?.some(arg => graphqlType(arg.tsType) === 'ID');
|
|
10
10
|
|
|
11
|
-
function isInlineObject(ts) {
|
|
12
|
-
return /^\{[\s\S]*\}$/.test((ts ?? '').trim());
|
|
13
|
-
}
|
|
14
|
-
function isInlineObjectArray(ts) {
|
|
15
|
-
const t = (ts ?? '').trim();
|
|
16
|
-
return /^Array<\{[\s\S]*\}>$/.test(t) || /^\{[\s\S]*\}\[\]$/.test(t);
|
|
17
|
-
}
|
|
18
|
-
function baseTs(ts) {
|
|
19
|
-
return (ts ?? 'string').replace(/\s*\|\s*null\b/g, '').trim();
|
|
20
|
-
}
|
|
21
|
-
function fieldUsesDate(ts) {
|
|
22
|
-
const b = baseTs(ts);
|
|
23
|
-
const gqlType = graphqlType(b);
|
|
24
|
-
if (gqlType.includes('GraphQLISODateTime')) return true;
|
|
25
|
-
if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*Date\b/.test(b);
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
function fieldUsesJSON(ts) {
|
|
29
|
-
const b = baseTs(ts);
|
|
30
|
-
const gqlType = graphqlType(b);
|
|
31
|
-
if (gqlType.includes('GraphQLJSON') || gqlType.includes('JSON')) return true;
|
|
32
|
-
if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*(unknown|any|object)\b/.test(b);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
function fieldUsesFloat(ts) {
|
|
36
|
-
const b = baseTs(ts);
|
|
37
|
-
const gqlType = graphqlType(b);
|
|
38
|
-
if (gqlType.includes('Float')) return true;
|
|
39
|
-
if (isInlineObject(b) || isInlineObjectArray(b)) {
|
|
40
|
-
const inner = b.trim().startsWith('Array<')
|
|
41
|
-
? b.trim().replace(/^Array<\{/, '{').replace(/}>$/, '}')
|
|
42
|
-
: b.trim().replace(/\[\]$/, '');
|
|
43
|
-
const match = inner.match(/^\{([\s\S]*)\}$/);
|
|
44
|
-
const body = match ? match[1] : '';
|
|
45
|
-
const rawFields = body.split(/[,;]\s*/).filter(Boolean);
|
|
46
|
-
return rawFields.some(f => {
|
|
47
|
-
const parts = f.split(':');
|
|
48
|
-
const type = parts.slice(1).join(':').trim();
|
|
49
|
-
return type && graphqlType(type).includes('Float');
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
11
|
const messageFields = message?.fields ?? [];
|
|
56
12
|
const usesDate = messageFields.some(f => fieldUsesDate(f.type)) ||
|
|
57
13
|
(parsedRequest?.args ?? []).some(a => fieldUsesDate(a.tsType));
|
|
@@ -60,6 +16,8 @@ const usesJSON = messageFields.some(f => fieldUsesJSON(f.type)) ||
|
|
|
60
16
|
const usesFloat = messageFields.some(f => fieldUsesFloat(f.type)) ||
|
|
61
17
|
(parsedRequest?.args ?? []).some(a => fieldUsesFloat(a.tsType));
|
|
62
18
|
|
|
19
|
+
const enumList = collectEnumNames([...messageFields, ...(parsedRequest?.args ?? [])]);
|
|
20
|
+
|
|
63
21
|
const embeddedTypes = [];
|
|
64
22
|
for (const field of messageFields) {
|
|
65
23
|
const tsType = field.type ?? 'string';
|
|
@@ -70,10 +28,11 @@ for (const field of messageFields) {
|
|
|
70
28
|
});
|
|
71
29
|
}
|
|
72
30
|
}
|
|
31
|
+
const hasArgs = parsedRequest?.args?.length > 0;
|
|
73
32
|
%>
|
|
74
|
-
import { Query, Resolver
|
|
33
|
+
import { Query, Resolver<% if (hasArgs) { %>, Arg<% } %>, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
75
34
|
<% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
|
|
76
|
-
<% } %>import { type GraphQLContext, ReadModel } from '../../../shared';
|
|
35
|
+
<% } %>import { type GraphQLContext, ReadModel<% if (enumList.length > 0) { %>, <%= enumList.join(', ') %><% } %> } from '../../../shared';
|
|
77
36
|
|
|
78
37
|
<%
|
|
79
38
|
for (const { typeName, tsType } of embeddedTypes) {
|
|
@@ -118,9 +77,12 @@ export class <%= viewType %> {
|
|
|
118
77
|
@Field(() => <%= gqlType %><%= isNullable(tsType) ? ', { nullable: true }' : '' %>)
|
|
119
78
|
<%= field.name %><%= isNullable(tsType) ? '?' : '!' %>: <%= toTsFieldType(tsType) %>;
|
|
120
79
|
<% } } %>
|
|
121
|
-
|
|
80
|
+
|
|
81
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
82
|
+
[key: string]: unknown;
|
|
122
83
|
<% } else { %>
|
|
123
|
-
|
|
84
|
+
// IMPORTANT: Index signature required for ReadModel<T extends Record<string, unknown>> compatibility
|
|
85
|
+
[key: string]: unknown;
|
|
124
86
|
<% } %>
|
|
125
87
|
}
|
|
126
88
|
|
|
@@ -148,7 +110,7 @@ const model = new ReadModel<<%= viewType %>>(ctx.database, '<%= projectionType %
|
|
|
148
110
|
// Example below uses \`.find()\` to filter
|
|
149
111
|
// change the logic for the query as needed to meet the requirements for the current slice.
|
|
150
112
|
|
|
151
|
-
return model.find((item) => {
|
|
113
|
+
return model.find((<%= hasArgs ? 'item' : '_item' %>) => {
|
|
152
114
|
<% if (parsedRequest?.args?.length) {
|
|
153
115
|
for (const arg of parsedRequest.args) { %>
|
|
154
116
|
if (<%= arg.name %> !== undefined && item.<%= arg.name %> !== <%= arg.name %>) return false;
|