@auto-engineer/server-generator-apollo-emmett 0.11.14 → 0.11.15

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/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/message-bus": "0.11.14",
35
- "@auto-engineer/narrative": "0.11.14"
34
+ "@auto-engineer/narrative": "0.11.15",
35
+ "@auto-engineer/message-bus": "0.11.15"
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.14"
46
+ "@auto-engineer/cli": "0.11.15"
47
47
  },
48
- "version": "0.11.14",
48
+ "version": "0.11.15",
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
  );
@@ -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 = (document as InternalAvailableListings) || { ...defaults };
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 = (document as InternalTodoSummary) || { ...initialState, _entities: {} };
552
+ * const current: InternalTodoSummary = document ?? { ...initialState, _entities: {} };
553
553
  *
554
554
  * 2. Track entity changes:
555
- * const entityId = event.data.todoId; // or relevant ID field
556
- * const prevStatus = current._entities?.[entityId]?.status;
557
- * current._entities[entityId] = { status: 'new_status', ...otherData };
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((doc) => <%
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 = (document as Internal<%= pascalCase(targetName || 'State') %>) || { ...initialState, _entities: {} };
117
+ * const current: Internal<%= pascalCase(targetName || 'State') %> = document ?? { ...initialState, _entities: {} };
118
118
  *
119
119
  * 2. Track entity changes:
120
- * const entityId = event.data.todoId; // or relevant ID field
121
- * const prevStatus = current._entities?.[entityId]?.status;
122
- * current._entities[entityId] = { status: 'new_status', ...otherData };
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') %> = (document as Internal<%= pascalCase(targetName || 'State') %>) || { ...defaults };
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') %>;