@auto-engineer/server-generator-apollo-emmett 0.11.14 → 0.11.16
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/CHANGELOG.md +18 -0
- package/dist/src/codegen/extract/messages.d.ts +1 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +4 -1
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +1 -0
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.js +12 -0
- package/dist/src/codegen/extract/projection.js.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +3 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +66 -2
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +6 -5
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +1 -3
- package/dist/src/codegen/templates/query/projection.specs.ts +9 -5
- package/dist/src/codegen/templates/query/projection.specs.ts.ejs +6 -2
- package/dist/src/codegen/templates/query/projection.ts.ejs +9 -5
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +79 -0
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +45 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/messages.ts +6 -1
- package/src/codegen/extract/projection.ts +14 -0
- package/src/codegen/scaffoldFromSchema.ts +3 -0
- package/src/codegen/templates/command/mutation.resolver.specs.ts +66 -2
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +6 -5
- package/src/codegen/templates/query/projection.specs.specs.ts +1 -3
- package/src/codegen/templates/query/projection.specs.ts +9 -5
- package/src/codegen/templates/query/projection.specs.ts.ejs +6 -2
- package/src/codegen/templates/query/projection.ts.ejs +9 -5
- package/src/codegen/templates/query/query.resolver.specs.ts +79 -0
- package/src/codegen/templates/query/query.resolver.ts.ejs +45 -1
package/package.json
CHANGED
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"graphql-type-json": "^0.3.2",
|
|
32
32
|
"uuid": "^11.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
|
-
"@auto-engineer/
|
|
35
|
-
"@auto-engineer/
|
|
34
|
+
"@auto-engineer/narrative": "0.11.16",
|
|
35
|
+
"@auto-engineer/message-bus": "0.11.16"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"typescript": "^5.8.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
45
|
"tsx": "^4.19.2",
|
|
46
|
-
"@auto-engineer/cli": "0.11.
|
|
46
|
+
"@auto-engineer/cli": "0.11.16"
|
|
47
47
|
},
|
|
48
|
-
"version": "0.11.
|
|
48
|
+
"version": "0.11.16",
|
|
49
49
|
"scripts": {
|
|
50
50
|
"generate:server": "tsx src/cli/index.ts",
|
|
51
51
|
"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",
|
|
@@ -3,7 +3,7 @@ import { CommandExample, EventExample, Slice } from '@auto-engineer/narrative';
|
|
|
3
3
|
import { Message, MessageDefinition } from '../types';
|
|
4
4
|
import { extractEventsFromGiven, extractEventsFromThen, extractEventsFromWhen } from './events';
|
|
5
5
|
import { extractFieldsFromMessage } from './fields';
|
|
6
|
-
import { extractProjectionIdField } from './projection';
|
|
6
|
+
import { extractProjectionIdField, extractProjectionSingleton } from './projection';
|
|
7
7
|
import { extractStatesFromData, extractStatesFromTarget } from './states';
|
|
8
8
|
import createDebug from 'debug';
|
|
9
9
|
|
|
@@ -19,6 +19,7 @@ export interface ExtractedMessages {
|
|
|
19
19
|
states: Message[];
|
|
20
20
|
commandSchemasByName: Record<string, Message>;
|
|
21
21
|
projectionIdField?: string;
|
|
22
|
+
projectionSingleton?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface ReactGwtSpec {
|
|
@@ -122,6 +123,9 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
|
|
|
122
123
|
const projectionIdField = extractProjectionIdField(slice);
|
|
123
124
|
debugQuery(' Projection ID field: %s', projectionIdField ?? 'none');
|
|
124
125
|
|
|
126
|
+
const projectionSingleton = extractProjectionSingleton(slice);
|
|
127
|
+
debugQuery(' Projection singleton: %s', projectionSingleton);
|
|
128
|
+
|
|
125
129
|
const events: Message[] = gwtSpecs.flatMap((gwt) => {
|
|
126
130
|
const eventsFromGiven = Array.isArray(gwt.given)
|
|
127
131
|
? gwt.given.filter((item): item is EventExample => 'eventRef' in item)
|
|
@@ -166,6 +170,7 @@ function extractMessagesForQuery(slice: Slice, allMessages: MessageDefinition[])
|
|
|
166
170
|
states,
|
|
167
171
|
commandSchemasByName: {},
|
|
168
172
|
projectionIdField,
|
|
173
|
+
projectionSingleton,
|
|
169
174
|
};
|
|
170
175
|
|
|
171
176
|
debugQuery(' Final result: %d events, %d states', result.events.length, result.states.length);
|
|
@@ -4,6 +4,7 @@ interface ProjectionOrigin {
|
|
|
4
4
|
type: 'projection';
|
|
5
5
|
idField?: string;
|
|
6
6
|
name?: string;
|
|
7
|
+
singleton?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
interface HasOrigin {
|
|
@@ -46,3 +47,16 @@ export function extractProjectionIdField(slice: Slice): string | undefined {
|
|
|
46
47
|
export function extractProjectionName(slice: Slice): string | undefined {
|
|
47
48
|
return extractProjectionField(slice, 'name');
|
|
48
49
|
}
|
|
50
|
+
|
|
51
|
+
export function extractProjectionSingleton(slice: Slice): boolean {
|
|
52
|
+
if (!('server' in slice)) return false;
|
|
53
|
+
const dataSource = slice.server?.data?.[0];
|
|
54
|
+
if (!hasOrigin(dataSource)) return false;
|
|
55
|
+
|
|
56
|
+
const origin = dataSource.origin;
|
|
57
|
+
if (isProjectionOrigin(origin)) {
|
|
58
|
+
return origin.singleton === true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
@@ -490,6 +490,7 @@ async function prepareTemplateData(
|
|
|
490
490
|
states: Message[],
|
|
491
491
|
commandSchemasByName: Record<string, Message>,
|
|
492
492
|
projectionIdField: string | undefined,
|
|
493
|
+
projectionSingleton: boolean | undefined,
|
|
493
494
|
allMessages?: MessageDefinition[],
|
|
494
495
|
integrations?: Model['integrations'],
|
|
495
496
|
): Promise<Record<string, unknown>> {
|
|
@@ -538,6 +539,7 @@ async function prepareTemplateData(
|
|
|
538
539
|
usedErrors,
|
|
539
540
|
commandSchemasByName,
|
|
540
541
|
projectionIdField,
|
|
542
|
+
projectionSingleton,
|
|
541
543
|
projectionName,
|
|
542
544
|
projectionType: projectionName != null ? pascalCase(projectionName) : undefined,
|
|
543
545
|
parsedRequest: slice.type === 'query' && slice.request != null ? parseGraphQlRequest(slice.request) : undefined,
|
|
@@ -693,6 +695,7 @@ async function generateFilesForSlice(
|
|
|
693
695
|
extracted.states,
|
|
694
696
|
extracted.commandSchemasByName,
|
|
695
697
|
extracted.projectionIdField,
|
|
698
|
+
extracted.projectionSingleton,
|
|
696
699
|
messages,
|
|
697
700
|
integrations,
|
|
698
701
|
);
|
|
@@ -76,7 +76,7 @@ describe('mutation.resolver.ts.ejs', () => {
|
|
|
76
76
|
const mutationFile = plans.find((p) => p.outputPath.endsWith('mutation.resolver.ts'));
|
|
77
77
|
|
|
78
78
|
expect(mutationFile?.contents).toMatchInlineSnapshot(`
|
|
79
|
-
"import { Mutation, Resolver, Arg, Ctx, Field, InputType, GraphQLISODateTime } from 'type-graphql';
|
|
79
|
+
"import { Mutation, Resolver, Arg, Ctx, Field, InputType, Float, GraphQLISODateTime } from 'type-graphql';
|
|
80
80
|
import { GraphQLJSON } from 'graphql-type-json';
|
|
81
81
|
import { type GraphQLContext, sendCommand, MutationResponse } from '../../../shared';
|
|
82
82
|
|
|
@@ -322,7 +322,7 @@ describe('mutation.resolver.ts.ejs', () => {
|
|
|
322
322
|
);
|
|
323
323
|
|
|
324
324
|
expect(mutationFile?.contents).toMatchInlineSnapshot(`
|
|
325
|
-
"import { Mutation, Resolver, Arg, Ctx, Field, InputType } from 'type-graphql';
|
|
325
|
+
"import { Mutation, Resolver, Arg, Ctx, Field, InputType, Float } from 'type-graphql';
|
|
326
326
|
import { GraphQLJSON } from 'graphql-type-json';
|
|
327
327
|
import { type GraphQLContext, sendCommand, MutationResponse } from '../../../shared';
|
|
328
328
|
|
|
@@ -361,4 +361,68 @@ export class AddItemsToCartResolver {
|
|
|
361
361
|
"
|
|
362
362
|
`);
|
|
363
363
|
});
|
|
364
|
+
|
|
365
|
+
it('should import Float when Float fields are used', async () => {
|
|
366
|
+
const spec: SpecsSchema = {
|
|
367
|
+
variant: 'specs',
|
|
368
|
+
narratives: [
|
|
369
|
+
{
|
|
370
|
+
name: 'product-flow',
|
|
371
|
+
slices: [
|
|
372
|
+
{
|
|
373
|
+
type: 'command',
|
|
374
|
+
name: 'update-product-price',
|
|
375
|
+
client: {
|
|
376
|
+
description: '',
|
|
377
|
+
},
|
|
378
|
+
server: {
|
|
379
|
+
description: '',
|
|
380
|
+
specs: {
|
|
381
|
+
name: '',
|
|
382
|
+
rules: [
|
|
383
|
+
{
|
|
384
|
+
description: 'update price',
|
|
385
|
+
examples: [
|
|
386
|
+
{
|
|
387
|
+
description: 'happy path',
|
|
388
|
+
when: {
|
|
389
|
+
commandRef: 'UpdateProductPrice',
|
|
390
|
+
exampleData: {
|
|
391
|
+
productId: 'p-1',
|
|
392
|
+
price: 99.99,
|
|
393
|
+
discount: 10.5,
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
then: [],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
messages: [
|
|
408
|
+
{
|
|
409
|
+
type: 'command',
|
|
410
|
+
name: 'UpdateProductPrice',
|
|
411
|
+
fields: [
|
|
412
|
+
{ name: 'productId', type: 'string', required: true },
|
|
413
|
+
{ name: 'price', type: 'number', required: true },
|
|
414
|
+
{ name: 'discount', type: 'number', required: true },
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
421
|
+
const mutationFile = plans.find((p) => p.outputPath.endsWith('mutation.resolver.ts'));
|
|
422
|
+
|
|
423
|
+
expect(mutationFile?.contents).toContain(
|
|
424
|
+
"import { Mutation, Resolver, Arg, Ctx, Field, InputType, Float } from 'type-graphql';",
|
|
425
|
+
);
|
|
426
|
+
expect(mutationFile?.contents).toContain('@Field(() => Float)');
|
|
427
|
+
});
|
|
364
428
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<%
|
|
2
2
|
const cmd = commands[0];
|
|
3
|
-
const usesDate = cmd.fields.some(f => fieldUsesDate(f.tsType));
|
|
4
|
-
const usesJSON = cmd.fields.some(f => fieldUsesJSON(f.tsType));
|
|
3
|
+
const usesDate = cmd.fields.some(f => fieldUsesDate(f.type ?? f.tsType));
|
|
4
|
+
const usesJSON = cmd.fields.some(f => fieldUsesJSON(f.type ?? f.tsType));
|
|
5
|
+
const usesFloat = cmd.fields.some(f => fieldUsesFloat(f.type ?? f.tsType));
|
|
5
6
|
const enumList = collectEnumNames(cmd.fields);
|
|
6
7
|
|
|
7
8
|
const embeddedInputs = [];
|
|
8
9
|
for (const f of cmd.fields) {
|
|
9
|
-
const tsType = f.tsType ?? 'string';
|
|
10
|
+
const tsType = f.type ?? f.tsType ?? 'string';
|
|
10
11
|
if (isInlineObjectArray(tsType) || isInlineObject(tsType)) {
|
|
11
12
|
embeddedInputs.push({
|
|
12
13
|
typeName: `${pascalCase(cmd.type)}${pascalCase(f.name)}Input`,
|
|
@@ -15,7 +16,7 @@ for (const f of cmd.fields) {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
%>
|
|
18
|
-
import { Mutation, Resolver, Arg, Ctx, Field, InputType<% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
19
|
+
import { Mutation, Resolver, Arg, Ctx, Field, InputType<% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
19
20
|
<% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
|
|
20
21
|
<% } %>import { type GraphQLContext, sendCommand, MutationResponse<% if (enumList.length > 0) { %>, <%= enumList.join(', ') %><% } %> } from '../../../shared';
|
|
21
22
|
|
|
@@ -46,7 +47,7 @@ export class <%= typeName %> {
|
|
|
46
47
|
@InputType()
|
|
47
48
|
export class <%= pascalCase(cmd.type) %>Input {
|
|
48
49
|
<% for (const field of cmd.fields) {
|
|
49
|
-
const tsType = field.tsType ?? 'string';
|
|
50
|
+
const tsType = field.type ?? field.tsType ?? 'string';
|
|
50
51
|
const gqlType = graphqlType(tsType);
|
|
51
52
|
const nestedName = `${pascalCase(cmd.type)}${pascalCase(field.name)}Input`;
|
|
52
53
|
|
|
@@ -783,9 +783,7 @@ describe('projection.specs.ts.ejs', () => {
|
|
|
783
783
|
},
|
|
784
784
|
])
|
|
785
785
|
.then(async (state) => {
|
|
786
|
-
const document = await state.database
|
|
787
|
-
.collection<TodoSummary>('TodoSummaryProjection')
|
|
788
|
-
.findOne((doc) => doc.id === 'test-id');
|
|
786
|
+
const document = await state.database.collection<TodoSummary>('TodoSummaryProjection').findOne();
|
|
789
787
|
|
|
790
788
|
const expected: TodoSummary = {
|
|
791
789
|
totalCount: 1,
|
|
@@ -239,7 +239,7 @@ describe('projection.ts.ejs', () => {
|
|
|
239
239
|
* }
|
|
240
240
|
*
|
|
241
241
|
* 2. Cast document parameter to extended type:
|
|
242
|
-
* const current: InternalAvailableListings =
|
|
242
|
+
* const current: InternalAvailableListings = document ?? { ...defaults };
|
|
243
243
|
*
|
|
244
244
|
* 3. Cast return values to extended type:
|
|
245
245
|
* return { ...allFields, internalField } as InternalAvailableListings;
|
|
@@ -549,12 +549,16 @@ describe('projection.ts.ejs', () => {
|
|
|
549
549
|
* CRITICAL: Use internal state to track individual entity information:
|
|
550
550
|
*
|
|
551
551
|
* 1. Access current state:
|
|
552
|
-
* const current =
|
|
552
|
+
* const current: InternalTodoSummary = document ?? { ...initialState, _entities: {} };
|
|
553
553
|
*
|
|
554
554
|
* 2. Track entity changes:
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
555
|
+
* // a) Extract the unique identifier that distinguishes this entity
|
|
556
|
+
* // Examine event.data to find the ID field (often 'id' or '<entity>Id')
|
|
557
|
+
* const entityId = event.data.[ENTITY_ID_FIELD];
|
|
558
|
+
*
|
|
559
|
+
* // b) Store/update entity state with relevant properties from event.data
|
|
560
|
+
* // Include only fields needed for aggregation calculations
|
|
561
|
+
* current._entities[entityId] = { [field]: value, ... };
|
|
558
562
|
*
|
|
559
563
|
* 3. Calculate aggregates from entity states:
|
|
560
564
|
* const counts = Object.values(current._entities).reduce((acc, entity) => {
|
|
@@ -141,7 +141,9 @@ _%>
|
|
|
141
141
|
.then(async (state) => {
|
|
142
142
|
const document = await state.database
|
|
143
143
|
.collection<<%= TargetType %>>('<%= projName %>')
|
|
144
|
-
.findOne((
|
|
144
|
+
.findOne(<% if (projectionSingleton) { %>);<%
|
|
145
|
+
} else {
|
|
146
|
+
%>(doc) => <%
|
|
145
147
|
const idField = projectionIdField ?? 'id';
|
|
146
148
|
if (idField.includes('-')) {
|
|
147
149
|
// Handle composite keys
|
|
@@ -157,7 +159,9 @@ if (idField.includes('-')) {
|
|
|
157
159
|
const valueStr = typeof value === 'string' ? `'${value}'` : value || "'test-id'";
|
|
158
160
|
%>doc.<%= idField %> === <%= valueStr %><%
|
|
159
161
|
}
|
|
160
|
-
%>)
|
|
162
|
+
%>);<%
|
|
163
|
+
}
|
|
164
|
+
%>
|
|
161
165
|
|
|
162
166
|
const expected: <%= TargetType %> = {
|
|
163
167
|
<% const stateKeys = Object.keys(expectedState.exampleData || {});
|
|
@@ -114,12 +114,16 @@ case '<%= event.type %>': {
|
|
|
114
114
|
* CRITICAL: Use internal state to track individual entity information:
|
|
115
115
|
*
|
|
116
116
|
* 1. Access current state:
|
|
117
|
-
* const current
|
|
117
|
+
* const current: Internal<%= pascalCase(targetName || 'State') %> = document ?? { ...initialState, _entities: {} };
|
|
118
118
|
*
|
|
119
119
|
* 2. Track entity changes:
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
120
|
+
* // a) Extract the unique identifier that distinguishes this entity
|
|
121
|
+
* // Examine event.data to find the ID field (often 'id' or '<entity>Id')
|
|
122
|
+
* const entityId = event.data.[ENTITY_ID_FIELD];
|
|
123
|
+
*
|
|
124
|
+
* // b) Store/update entity state with relevant properties from event.data
|
|
125
|
+
* // Include only fields needed for aggregation calculations
|
|
126
|
+
* current._entities[entityId] = { [field]: value, ... };
|
|
123
127
|
*
|
|
124
128
|
* 3. Calculate aggregates from entity states:
|
|
125
129
|
* const counts = Object.values(current._entities).reduce((acc, entity) => {
|
|
@@ -165,7 +169,7 @@ case '<%= event.type %>': {
|
|
|
165
169
|
* }
|
|
166
170
|
*
|
|
167
171
|
* 2. Cast document parameter to extended type:
|
|
168
|
-
* const current: Internal<%= pascalCase(targetName || 'State') %> =
|
|
172
|
+
* const current: Internal<%= pascalCase(targetName || 'State') %> = document ?? { ...defaults };
|
|
169
173
|
*
|
|
170
174
|
* 3. Cast return values to extended type:
|
|
171
175
|
* return { ...allFields, internalField } as Internal<%= pascalCase(targetName || 'State') %>;
|
|
@@ -610,4 +610,83 @@ describe('query.resolver.ts.ejs', () => {
|
|
|
610
610
|
expect(resolverFile?.contents).toContain('Float');
|
|
611
611
|
expect(resolverFile?.contents).toContain("@Arg('minPrice', () => Float");
|
|
612
612
|
});
|
|
613
|
+
|
|
614
|
+
it('should generate a singleton query resolver that returns a single object', async () => {
|
|
615
|
+
const spec: SpecsSchema = {
|
|
616
|
+
variant: 'specs',
|
|
617
|
+
narratives: [
|
|
618
|
+
{
|
|
619
|
+
name: 'todo-list-flow',
|
|
620
|
+
slices: [
|
|
621
|
+
{
|
|
622
|
+
type: 'query',
|
|
623
|
+
name: 'views-completion-summary',
|
|
624
|
+
request: `
|
|
625
|
+
query TodoListSummary {
|
|
626
|
+
todoListSummary {
|
|
627
|
+
totalTodos
|
|
628
|
+
pendingCount
|
|
629
|
+
inProgressCount
|
|
630
|
+
completedCount
|
|
631
|
+
completionPercentage
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
`,
|
|
635
|
+
client: {
|
|
636
|
+
description: '',
|
|
637
|
+
},
|
|
638
|
+
server: {
|
|
639
|
+
description: '',
|
|
640
|
+
data: [
|
|
641
|
+
{
|
|
642
|
+
origin: {
|
|
643
|
+
type: 'projection',
|
|
644
|
+
name: 'TodoSummary',
|
|
645
|
+
singleton: true,
|
|
646
|
+
},
|
|
647
|
+
target: {
|
|
648
|
+
type: 'State',
|
|
649
|
+
name: 'TodoListSummary',
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
specs: { name: '', rules: [] },
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
messages: [
|
|
660
|
+
{
|
|
661
|
+
type: 'state',
|
|
662
|
+
name: 'TodoListSummary',
|
|
663
|
+
fields: [
|
|
664
|
+
{ name: 'totalTodos', type: 'number', required: true },
|
|
665
|
+
{ name: 'pendingCount', type: 'number', required: true },
|
|
666
|
+
{ name: 'inProgressCount', type: 'number', required: true },
|
|
667
|
+
{ name: 'completedCount', type: 'number', required: true },
|
|
668
|
+
{ name: 'completionPercentage', type: 'number', required: true },
|
|
669
|
+
],
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
|
|
675
|
+
const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
|
|
676
|
+
|
|
677
|
+
// Should return single object, not array
|
|
678
|
+
expect(resolverFile?.contents).toContain('@Query(() => TodoListSummary)');
|
|
679
|
+
expect(resolverFile?.contents).not.toContain('@Query(() => [TodoListSummary])');
|
|
680
|
+
|
|
681
|
+
// Should use collection.findOne() pattern
|
|
682
|
+
expect(resolverFile?.contents).toContain("ctx.database.collection<TodoListSummary>('TodoSummary').findOne()");
|
|
683
|
+
|
|
684
|
+
expect(resolverFile?.contents).toContain('if (!result)');
|
|
685
|
+
expect(resolverFile?.contents).toContain('totalTodos: 0');
|
|
686
|
+
expect(resolverFile?.contents).toContain('pendingCount: 0');
|
|
687
|
+
|
|
688
|
+
// Should NOT import ReadModel (unused for singletons)
|
|
689
|
+
expect(resolverFile?.contents).toContain("import { type GraphQLContext } from '../../../shared'");
|
|
690
|
+
expect(resolverFile?.contents).not.toContain(', ReadModel');
|
|
691
|
+
});
|
|
613
692
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%
|
|
2
2
|
const target = slice?.server?.data?.[0]?.target;
|
|
3
3
|
const projection = slice?.server?.data?.[0]?.origin;
|
|
4
|
+
const isSingleton = projection?.singleton === true;
|
|
4
5
|
const queryName = parsedRequest?.queryName ?? camelCase(sliceName);
|
|
5
6
|
const viewType = target?.name ? pascalCase(target.name) : 'UnknownView';
|
|
6
7
|
const projectionType = projection?.name ? pascalCase(projection.name) : 'UnknownProjection';
|
|
@@ -32,7 +33,7 @@ const hasArgs = parsedRequest?.args?.length > 0;
|
|
|
32
33
|
%>
|
|
33
34
|
import { Query, Resolver<% if (hasArgs) { %>, Arg<% } %>, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
|
|
34
35
|
<% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
|
|
35
|
-
<% } %>import { type GraphQLContext
|
|
36
|
+
<% } %>import { type GraphQLContext<% if (!isSingleton) { %>, ReadModel<% } %><% if (enumList.length > 0) { %>, <%= enumList.join(', ') %><% } %> } from '../../../shared';
|
|
36
37
|
|
|
37
38
|
<%
|
|
38
39
|
for (const { typeName, tsType } of embeddedTypes) {
|
|
@@ -88,6 +89,48 @@ export class <%= viewType %> {
|
|
|
88
89
|
|
|
89
90
|
@Resolver()
|
|
90
91
|
export class <%= resolverClassName %> {
|
|
92
|
+
<% if (isSingleton) { %>
|
|
93
|
+
@Query(() => <%= viewType %>)
|
|
94
|
+
async <%= queryName %>(
|
|
95
|
+
@Ctx() ctx: GraphQLContext<% if (parsedRequest?.args?.length) { %>,
|
|
96
|
+
<% for (let i = 0; i < parsedRequest.args.length; i++) {
|
|
97
|
+
const arg = parsedRequest.args[i];
|
|
98
|
+
const gqlType = graphqlType(arg.tsType);
|
|
99
|
+
const tsType = arg.tsType === 'ID' ? 'string' : arg.tsType;
|
|
100
|
+
%> @Arg('<%= arg.name %>', () => <%= gqlType %>, { nullable: true }) <%= arg.name %>?: <%= tsType %><%= i < parsedRequest.args.length - 1 ? ',' : '' %>
|
|
101
|
+
<% } } %>
|
|
102
|
+
): Promise<<%= viewType %>> {
|
|
103
|
+
const result = await ctx.database.collection<<%= viewType %>>('<%= projectionType %>').findOne();
|
|
104
|
+
|
|
105
|
+
if (!result) {
|
|
106
|
+
return {
|
|
107
|
+
<% for (let i = 0; i < messageFields.length; i++) {
|
|
108
|
+
const field = messageFields[i];
|
|
109
|
+
const tsType = field.type ?? 'string';
|
|
110
|
+
const baseType = tsType.replace(/\s*\|\s*null/g, '').trim();
|
|
111
|
+
let defaultValue = '0';
|
|
112
|
+
|
|
113
|
+
if (baseType === 'string' || baseType === 'ID') {
|
|
114
|
+
defaultValue = "''";
|
|
115
|
+
} else if (baseType === 'number') {
|
|
116
|
+
defaultValue = '0';
|
|
117
|
+
} else if (baseType === 'boolean') {
|
|
118
|
+
defaultValue = 'false';
|
|
119
|
+
} else if (baseType === 'Date') {
|
|
120
|
+
defaultValue = 'new Date()';
|
|
121
|
+
} else if (baseType.startsWith('Array<')) {
|
|
122
|
+
defaultValue = '[]';
|
|
123
|
+
} else if (baseType.includes('|') && (baseType.includes('"') || baseType.includes("'"))) {
|
|
124
|
+
const firstValue = baseType.match(/['"]([^'"]+)['"]/)?.[1] ?? '';
|
|
125
|
+
defaultValue = `'${firstValue}'`;
|
|
126
|
+
}
|
|
127
|
+
%> <%= field.name %>: <%= defaultValue %>,
|
|
128
|
+
<% } %> };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
<% } else { %>
|
|
91
134
|
@Query(() => [<%= viewType %>])
|
|
92
135
|
async <%= queryName %>(
|
|
93
136
|
@Ctx() ctx: GraphQLContext<% if (parsedRequest?.args?.length) { %>,
|
|
@@ -118,4 +161,5 @@ return model.find((<%= hasArgs ? 'item' : '_item' %>) => {
|
|
|
118
161
|
return true;
|
|
119
162
|
});
|
|
120
163
|
}
|
|
164
|
+
<% } %>
|
|
121
165
|
}
|