@auto-engineer/server-generator-apollo-emmett 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-format.log +5 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +22 -0
  5. package/.turbo/turbo-type-check.log +5 -0
  6. package/CHANGELOG.md +20 -0
  7. package/dist/src/codegen/extract/events.d.ts +2 -2
  8. package/dist/src/codegen/extract/events.d.ts.map +1 -1
  9. package/dist/src/codegen/extract/events.js +16 -6
  10. package/dist/src/codegen/extract/events.js.map +1 -1
  11. package/dist/src/codegen/extract/gwt.js +7 -22
  12. package/dist/src/codegen/extract/gwt.js.map +1 -1
  13. package/dist/src/codegen/extract/imports.d.ts +29 -0
  14. package/dist/src/codegen/extract/imports.d.ts.map +1 -0
  15. package/dist/src/codegen/extract/imports.js +55 -0
  16. package/dist/src/codegen/extract/imports.js.map +1 -0
  17. package/dist/src/codegen/extract/index.d.ts +1 -0
  18. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  19. package/dist/src/codegen/extract/index.js +1 -0
  20. package/dist/src/codegen/extract/index.js.map +1 -1
  21. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  22. package/dist/src/codegen/extract/messages.js +33 -7
  23. package/dist/src/codegen/extract/messages.js.map +1 -1
  24. package/dist/src/codegen/extract/query.d.ts +3 -1
  25. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  26. package/dist/src/codegen/extract/query.js +12 -12
  27. package/dist/src/codegen/extract/query.js.map +1 -1
  28. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  29. package/dist/src/codegen/scaffoldFromSchema.js +9 -1
  30. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  31. package/dist/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  32. package/dist/src/codegen/templates/command/decide.specs.ts +8 -8
  33. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  34. package/dist/src/codegen/templates/command/decide.ts.ejs +2 -2
  35. package/dist/src/codegen/templates/command/events.ts.ejs +2 -2
  36. package/dist/src/codegen/templates/command/evolve.ts.ejs +3 -3
  37. package/dist/src/codegen/templates/command/handle.specs.ts +6 -6
  38. package/dist/src/codegen/templates/command/handle.ts.ejs +3 -3
  39. package/dist/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  40. package/dist/src/codegen/templates/query/projection.specs.ts +1 -1
  41. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  42. package/dist/src/codegen/templates/query/projection.ts.ejs +30 -29
  43. package/dist/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  44. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  45. package/dist/src/codegen/templates/react/react.specs.specs.ts +8 -5
  46. package/dist/src/codegen/templates/react/react.specs.ts +4 -4
  47. package/dist/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  48. package/dist/src/codegen/templates/react/react.ts.ejs +4 -4
  49. package/dist/src/codegen/templates/react/register.specs.ts +2 -2
  50. package/dist/src/codegen/templates/react/register.ts.ejs +2 -2
  51. package/dist/src/codegen/types.d.ts +2 -0
  52. package/dist/src/codegen/types.d.ts.map +1 -1
  53. package/dist/src/commands/generate-server.d.ts.map +1 -1
  54. package/dist/src/commands/generate-server.js +3 -0
  55. package/dist/src/commands/generate-server.js.map +1 -1
  56. package/dist/src/domain/shared/ReadModel.d.ts +2 -2
  57. package/dist/src/domain/shared/ReadModel.d.ts.map +1 -1
  58. package/dist/src/domain/shared/ReadModel.js +2 -2
  59. package/dist/src/domain/shared/ReadModel.js.map +1 -1
  60. package/dist/src/domain/shared/ReadModel.ts +3 -3
  61. package/dist/src/domain/shared/types.d.ts +5 -3
  62. package/dist/src/domain/shared/types.d.ts.map +1 -1
  63. package/dist/src/domain/shared/types.js.map +1 -1
  64. package/dist/src/domain/shared/types.ts +5 -3
  65. package/dist/src/server.js +54 -7
  66. package/dist/src/server.js.map +1 -1
  67. package/dist/src/server.ts +53 -15
  68. package/dist/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +8 -5
  70. package/src/codegen/extract/events.ts +20 -3
  71. package/src/codegen/extract/gwt.ts +10 -26
  72. package/src/codegen/extract/imports.ts +71 -0
  73. package/src/codegen/extract/index.ts +1 -0
  74. package/src/codegen/extract/messages.ts +34 -7
  75. package/src/codegen/extract/query.ts +17 -19
  76. package/src/codegen/scaffoldFromSchema.ts +13 -0
  77. package/src/codegen/templates/command/decide.specs.specs.ts +235 -8
  78. package/src/codegen/templates/command/decide.specs.ts +8 -8
  79. package/src/codegen/templates/command/decide.specs.ts.ejs +95 -30
  80. package/src/codegen/templates/command/decide.ts.ejs +2 -2
  81. package/src/codegen/templates/command/events.ts.ejs +2 -2
  82. package/src/codegen/templates/command/evolve.ts.ejs +3 -3
  83. package/src/codegen/templates/command/handle.specs.ts +6 -6
  84. package/src/codegen/templates/command/handle.ts.ejs +3 -3
  85. package/src/codegen/templates/query/projection.specs.specs.ts +623 -0
  86. package/src/codegen/templates/query/projection.specs.ts +1 -1
  87. package/src/codegen/templates/query/projection.specs.ts.ejs +176 -52
  88. package/src/codegen/templates/query/projection.ts.ejs +30 -29
  89. package/src/codegen/templates/query/query.resolver.specs.ts +190 -5
  90. package/src/codegen/templates/query/query.resolver.ts.ejs +31 -9
  91. package/src/codegen/templates/react/react.specs.specs.ts +8 -5
  92. package/src/codegen/templates/react/react.specs.ts +4 -4
  93. package/src/codegen/templates/react/react.specs.ts.ejs +118 -67
  94. package/src/codegen/templates/react/react.ts.ejs +4 -4
  95. package/src/codegen/templates/react/register.specs.ts +2 -2
  96. package/src/codegen/templates/react/register.ts.ejs +2 -2
  97. package/src/codegen/types.ts +2 -0
  98. package/src/commands/generate-server.ts +3 -0
  99. package/src/domain/shared/ReadModel.ts +3 -3
  100. package/src/domain/shared/types.ts +5 -3
  101. package/src/server.ts +53 -15
  102. package/dist/src/codegen/templates/query/projection.specs.specs..ts +0 -296
  103. package/src/codegen/scaffoldFromSchema.query-slice-register.specs.ts +0 -179
  104. package/src/codegen/templates/query/projection.specs.specs..ts +0 -296
@@ -1,71 +1,195 @@
1
1
  <%_
2
+ const targetName = slice?.server?.data?.[0]?.target?.name || 'UnknownState';
3
+ const TargetType = pascalCase(targetName);
4
+ const projName = projectionName || "UnknownProjection";
2
5
  const idField = projectionIdField ?? 'id';
3
- const projectionType = states[0]?.type ?? 'UnknownState';
4
- const projectionCollection = projectionName ?? 'unknown-collection';
5
- const testCases = queryGwtMapping ?? [];
6
- const eventTypes = Array.from(new Set(events.map(e => e.type)));
6
+ const uniqueEventTypes = allEventTypesArray;
7
+
8
+ const ruleGroups = new Map();
9
+ const rules = slice?.server?.specs?.rules || [];
10
+ for (const rule of rules) {
11
+ const ruleDescription = rule.description || `${flowName} | ${sliceName}`;
12
+ if (!ruleGroups.has(ruleDescription)) {
13
+ ruleGroups.set(ruleDescription, []);
14
+ }
15
+
16
+ for (const example of rule.examples || []) {
17
+ ruleGroups.get(ruleDescription).push({
18
+ description: example.description || 'should handle events correctly',
19
+ given: example.given || [],
20
+ when: example.when || [],
21
+ then: example.then || []
22
+ });
23
+ }
24
+ }
7
25
  _%>
8
26
 
9
27
  import { describe, it, beforeEach, expect } from 'vitest';
10
- import {
11
- InMemoryProjectionSpec,
12
- } from '@event-driven-io/emmett';
28
+ import { InMemoryProjectionSpec } from '@event-driven-io/emmett';
13
29
  import { projection } from './projection';
14
- import type { <%= eventTypes.join(', ') %> } from '../<%= toKebabCase(events[0]?.sourceSliceName || 'unknown') %>/events';
15
- import { <%= projectionType %> } from './state';
16
-
17
- <% if (eventTypes.length > 0) { -%>
18
- type ProjectionEvent = <%= eventTypes.join(' | ') %>;
19
- <% } else { -%>
20
- type ProjectionEvent = never;
30
+ <% for (const group of eventImportGroups) { -%>
31
+ import type { <%= group.eventTypes.join(', ') %> } from '<%= group.importPath %>';
21
32
  <% } -%>
33
+ import { <%= TargetType %> } from './state';
22
34
 
23
- describe('<%= projectionName %> Projection', () => {
24
- let given: InMemoryProjectionSpec<ProjectionEvent>;
35
+ type ProjectionEvent = <%= uniqueEventTypes.length ? uniqueEventTypes.join(' | ') : 'never' %>;
25
36
 
26
- beforeEach(() => {
37
+ <% for (const [ruleDescription, ruleTests] of ruleGroups.entries()) { %>
38
+ describe('<%= ruleDescription %>', () => {
39
+ let given: InMemoryProjectionSpec<ProjectionEvent>;
40
+
41
+ beforeEach(() => {
27
42
  given = InMemoryProjectionSpec.for({ projection });
28
- });
43
+ });
29
44
 
30
- <%_ for (const [index, gwt] of testCases.entries()) {
31
- const isRemoval = gwt.then[0]?.exampleData && Object.keys(gwt.then[0].exampleData).length === 0;
32
- const expectedIdValue = JSON.stringify(gwt.then[0]?.exampleData?.[idField] ?? 'unknown-id');
33
- _%>
34
- it('<%= isRemoval ? 'removes' : 'creates or updates' %> <%= projectionType %> document - case <%= index + 1 %>', () =>
35
- given([
36
- <%_ for (const event of gwt.given) { _%>
37
- {
38
- type: '<%= event.eventRef %>',
45
+ <% for (const testCase of ruleTests) {
46
+ const givenEvents = Array.isArray(testCase.given) ? testCase.given : [];
47
+ const whenEvents = Array.isArray(testCase.when) ? testCase.when : (testCase.when ? [testCase.when] : []);
48
+ const thenStates = Array.isArray(testCase.then) ? testCase.then : [];
49
+ const allTestEvents = [...givenEvents, ...whenEvents].filter(e => e.eventRef && e.eventRef !== '');
50
+
51
+ if (thenStates.length > 0) {
52
+ const expectedState = thenStates.find(t => t.stateRef === targetName);
53
+ if (!expectedState) continue;
54
+
55
+ const description = testCase.description || 'should handle events correctly';
56
+ _%>
57
+
58
+ it('<%= description %>', () =>
59
+ given([<% if (givenEvents.length > 0) {
60
+ for (const evt of givenEvents) {
61
+ if (!evt.eventRef || evt.eventRef === '') continue;
62
+ const eventMessage = messages.find(m => m.name === evt.eventRef);
63
+ const streamNameValue = slice.stream ? `'${slice.stream}'` : "'test-stream'";
64
+ _%>
65
+ {
66
+ type: '<%= evt.eventRef %>',
39
67
  data: {
40
- <%_ for (const [key, val] of Object.entries(event.exampleData || {})) { _%>
41
- <%= key %>: <%- formatTsValue(val, (events.find(e => e.type === event.eventRef)?.fields.find(f => f.name === key)?.tsType || 'string')) %>,
42
- <%_ } _%>
68
+ <% const dataKeys = Object.keys(evt.exampleData || {});
69
+ for (let i = 0; i < dataKeys.length; i++) {
70
+ const key = dataKeys[i];
71
+ const value = evt.exampleData[key];
72
+ const isLast = i === dataKeys.length - 1;
73
+ const field = eventMessage?.fields?.find(f => f.name === key);
74
+ const tsType = field?.tsType || field?.type || 'string';
75
+
76
+ let formattedValue;
77
+ if (value === null || value === undefined) {
78
+ formattedValue = 'null';
79
+ } else if (tsType === 'string' || tsType === 'ID') {
80
+ formattedValue = `'${value}'`;
81
+ } else if (tsType === 'number' || tsType === 'boolean') {
82
+ formattedValue = String(value);
83
+ } else if (tsType === 'Date') {
84
+ formattedValue = `new Date('${value}')`;
85
+ } else if (Array.isArray(value)) {
86
+ formattedValue = JSON.stringify(value);
87
+ } else {
88
+ formattedValue = `'${value}'`;
89
+ }
90
+ -%>
91
+ <%= key %>: <%= formattedValue %><%= isLast ? '' : ',' %>
92
+ <% } -%>
43
93
  },
44
94
  metadata: {
45
- streamName: 'ignored-stream',
46
- streamPosition: 1n,
47
- globalPosition: 1n,
48
- },
95
+ streamName: <%= streamNameValue %>,
96
+ streamPosition: 1n,
97
+ globalPosition: 1n,
49
98
  },
50
- <%_ } _%>
51
- ])
52
- .when([])
53
- .then(async (state) => {
54
- const document = await state.database
55
- .collection<<%= projectionType %>>('<%= projectionCollection %>')
56
- .findOne((doc) => doc.<%= idField %> === <%- expectedIdValue %>);
99
+ },<% } } -%>])
100
+ .when([<% if (whenEvents.length > 0) {
101
+ for (const evt of whenEvents) {
102
+ if (!evt.eventRef || evt.eventRef === '') continue;
103
+ const eventMessage = messages.find(m => m.name === evt.eventRef);
104
+ const streamNameValue = slice.stream ? `'${slice.stream}'` : "'test-stream'";
105
+ _%>
106
+ {
107
+ type: '<%= evt.eventRef %>',
108
+ data: {
109
+ <% const dataKeys = Object.keys(evt.exampleData || {});
110
+ for (let i = 0; i < dataKeys.length; i++) {
111
+ const key = dataKeys[i];
112
+ const value = evt.exampleData[key];
113
+ const isLast = i === dataKeys.length - 1;
114
+ const field = eventMessage?.fields?.find(f => f.name === key);
115
+ const tsType = field?.tsType || field?.type || 'string';
57
116
 
58
- <%_ if (isRemoval) { _%>
59
- expect(document).toBeNull();
60
- <%_ } else { _%>
61
- const expected: <%= projectionType %> = {
62
- <%_ for (const [key, val] of Object.entries(gwt.then[0].exampleData || {})) { _%>
63
- <%= key %>: <%- formatTsValue(val, (states[0]?.fields.find(f => f.name === key)?.tsType || 'string')) %>,
64
- <%_ } _%>
117
+ let formattedValue;
118
+ if (value === null || value === undefined) {
119
+ formattedValue = 'null';
120
+ } else if (tsType === 'string' || tsType === 'ID') {
121
+ formattedValue = `'${value}'`;
122
+ } else if (tsType === 'number' || tsType === 'boolean') {
123
+ formattedValue = String(value);
124
+ } else if (tsType === 'Date') {
125
+ formattedValue = `new Date('${value}')`;
126
+ } else if (Array.isArray(value)) {
127
+ formattedValue = JSON.stringify(value);
128
+ } else {
129
+ formattedValue = `'${value}'`;
130
+ }
131
+ -%>
132
+ <%= key %>: <%= formattedValue %><%= isLast ? '' : ',' %>
133
+ <% } -%>
134
+ },
135
+ metadata: {
136
+ streamName: <%= streamNameValue %>,
137
+ streamPosition: <%= givenEvents.length + whenEvents.indexOf(evt) + 1 %>n,
138
+ globalPosition: <%= givenEvents.length + whenEvents.indexOf(evt) + 1 %>n,
139
+ },
140
+ },<% } } -%>])
141
+ .then(async (state) => {
142
+ const document = await state.database
143
+ .collection<<%= TargetType %>>('<%= projName %>')
144
+ .findOne((doc) => <%
145
+ const idField = projectionIdField ?? 'id';
146
+ if (idField.includes('-')) {
147
+ // Handle composite keys
148
+ const parts = idField.split('-');
149
+ const conditions = parts.map(part => {
150
+ const value = expectedState.exampleData?.[part];
151
+ const valueStr = typeof value === 'string' ? `'${value}'` : value || "'test-value'";
152
+ return `doc.${part} === ${valueStr}`;
153
+ }).join(' && ');
154
+ %><%= conditions %><%
155
+ } else {
156
+ const value = expectedState.exampleData?.[idField];
157
+ const valueStr = typeof value === 'string' ? `'${value}'` : value || "'test-id'";
158
+ %>doc.<%= idField %> === <%= valueStr %><%
159
+ }
160
+ %>);
161
+
162
+ const expected: <%= TargetType %> = {
163
+ <% const stateKeys = Object.keys(expectedState.exampleData || {});
164
+ for (let i = 0; i < stateKeys.length; i++) {
165
+ const key = stateKeys[i];
166
+ const value = expectedState.exampleData[key];
167
+ const isLast = i === stateKeys.length - 1;
168
+ const stateMessage = messages.find(m => m.name === targetName);
169
+ const field = stateMessage?.fields?.find(f => f.name === key);
170
+ const tsType = field?.tsType || field?.type || 'string';
171
+
172
+ let formattedValue;
173
+ if (value === null || value === undefined) {
174
+ formattedValue = 'null';
175
+ } else if (tsType === 'string' || tsType === 'ID') {
176
+ formattedValue = `'${value}'`;
177
+ } else if (tsType === 'number' || tsType === 'boolean') {
178
+ formattedValue = String(value);
179
+ } else if (tsType === 'Date') {
180
+ formattedValue = `new Date('${value}')`;
181
+ } else if (Array.isArray(value)) {
182
+ formattedValue = JSON.stringify(value);
183
+ } else {
184
+ formattedValue = `'${value}'`;
185
+ }
186
+ -%>
187
+ <%= key %>: <%= formattedValue %><%= isLast ? '' : ',' %>
188
+ <% } -%>
65
189
  };
66
190
 
67
191
  expect(document).toMatchObject(expected);
68
- <%_ } _%>
69
- }));
70
- <%_ } _%>
71
- });
192
+ }));
193
+ <% } } %>
194
+ });
195
+ <% } -%>
@@ -4,28 +4,15 @@ type ReadEvent,
4
4
  type InMemoryReadEventMetadata,
5
5
  } from '@event-driven-io/emmett';
6
6
  import type { <%= pascalCase(slice.server?.data?.[0]?.target?.name || 'UnknownState') %> } from './state';<%
7
- if (events.length > 0) {
8
- const importGroups = new Map();
9
- for (const event of events) {
10
- const fromSameFlow = event.sourceFlowName === flowName;
11
- const basePath = fromSameFlow
12
- ? `../${toKebabCase(event.sourceSliceName ?? 'unknown')}/events`
13
- : `../${toKebabCase(event.sourceFlowName ?? 'unknown')}/${toKebabCase(event.sourceSliceName ?? 'unknown')}/events`;
14
-
15
- if (!importGroups.has(basePath)) {
16
- importGroups.set(basePath, []);
17
- }
18
- importGroups.get(basePath).push(event.type);
19
- }
20
-
21
- for (const [importPath, typeNames] of importGroups.entries()) {
7
+ if (eventImportGroups.length > 0) {
8
+ for (const group of eventImportGroups) {
22
9
  %>
23
- import type { <%= typeNames.join(', ') %> } from '<%= importPath %>';
10
+ import type { <%= group.eventTypes.join(', ') %> } from '<%= group.importPath %>';
24
11
  <%
25
- }
12
+ }
26
13
  } -%>
27
14
 
28
- type AllEvents = <%= events.length > 0 ? events.map(e => e.type).join(' | ') : 'never' %>;
15
+ type AllEvents = <%= allEventTypes %>;
29
16
 
30
17
  export const projection = inMemorySingleStreamProjection<
31
18
  <%= pascalCase(slice.server?.data?.[0]?.target?.name || 'UnknownState') %>,
@@ -33,7 +20,19 @@ AllEvents
33
20
  >({
34
21
  collectionName: '<%= pascalCase(slice.server?.data?.[0]?.origin?.name || "unknown-collection") %>',
35
22
  canHandle: [<%- events.map(e => `'${e.type}'`).join(', ') %>],
36
- getDocumentId: (event) => event.data.<%= slice.server?.data?.[0]?.origin?.idField ?? 'id' %>,
23
+ getDocumentId: (event) => <%
24
+ const idField = slice.server?.data?.[0]?.origin?.idField ?? 'id';
25
+ // Check if idField contains hyphen-separated composite keys
26
+ if (idField.includes('-')) {
27
+ const parts = idField.split('-');
28
+ const template = parts.map((part, index) =>
29
+ index === 0 ? `\${event.data.${part}}` : `-\${event.data.${part}}`
30
+ ).join('');
31
+ %>`<%= template %>`<%
32
+ } else {
33
+ %>event.data.<%= idField %><%
34
+ }
35
+ %>,
37
36
  evolve: (
38
37
  document: <%= pascalCase(slice.server?.data?.[0]?.target?.name || 'UnknownState') %> | null,
39
38
  event: ReadEvent<AllEvents, InMemoryReadEventMetadata>
@@ -42,9 +41,11 @@ switch (event.type) {
42
41
  <% for (const event of events) {
43
42
  const targetName = slice.server?.data?.[0]?.target?.name;
44
43
  const queryGwt = slice.type === 'query'
45
- ? queryGwtMapping.find(gwt =>
46
- gwt.given.some(g => g.eventRef === event.type)
47
- )
44
+ ? queryGwtMapping.find(gwt => {
45
+ const inGiven = gwt.given && gwt.given.some(g => g.eventRef === event.type);
46
+ const inWhen = gwt.when.some(g => g.eventRef === event.type);
47
+ return inGiven || inWhen;
48
+ })
48
49
  : undefined;
49
50
  const example = slice.type === 'query'
50
51
  ? queryGwt?.then.find(t => t.stateRef === targetName)?.exampleData
@@ -94,19 +95,19 @@ case '<%= event.type %>': {
94
95
 
95
96
  let placeholder = 'undefined';
96
97
  if (type === 'string' || type === 'ID') {
97
- placeholder = "''";
98
+ placeholder = "/* TODO: map from event.data */ ''";
98
99
  } else if (type === 'number') {
99
- placeholder = '0';
100
+ placeholder = '/* TODO: map from event.data */ 0';
100
101
  } else if (type === 'boolean') {
101
- placeholder = 'false';
102
+ placeholder = '/* TODO: map from event.data */ false';
102
103
  } else if (type === 'Date') {
103
- placeholder = 'new Date()';
104
+ placeholder = '/* TODO: map from event.data */ new Date()';
104
105
  } else if (type.startsWith('Array<')) {
105
- placeholder = '[]';
106
+ placeholder = '/* TODO: map from event.data */ []';
106
107
  } else {
107
- placeholder = '{} as any';
108
+ placeholder = '/* TODO: map from event.data */ undefined as any';
108
109
  }
109
- %> <%= field %>: /* TODO: map from event.data */ <%- placeholder %><%= isLast ? '' : ',' %>
110
+ %> <%= field %>: <%- placeholder %><%= isLast ? '' : ',' %>
110
111
  <% } -%>
111
112
  };
112
113
  <% } -%>
@@ -67,7 +67,7 @@ describe('query.resolver.ts.ejs', () => {
67
67
  const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
68
68
 
69
69
  expect(resolverFile?.contents).toMatchInlineSnapshot(`
70
- "import { Query, Resolver, Arg, Ctx, ObjectType, Field } from 'type-graphql';
70
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, Float } from 'type-graphql';
71
71
  import { type GraphQLContext, ReadModel } from '../../../shared';
72
72
 
73
73
  @ObjectType()
@@ -99,7 +99,7 @@ describe('query.resolver.ts.ejs', () => {
99
99
  @Arg('maxPrice', () => Float, { nullable: true }) maxPrice?: number,
100
100
  @Arg('minGuests', () => Float, { nullable: true }) minGuests?: number,
101
101
  ): Promise<AvailableListings[]> {
102
- const model = new ReadModel<AvailableListings>(ctx.eventStore, 'AvailablePropertiesProjection');
102
+ const model = new ReadModel<AvailableListings>(ctx.database, 'AvailablePropertiesProjection');
103
103
 
104
104
  // ## IMPLEMENTATION INSTRUCTIONS ##
105
105
  // You can query the projection using the ReadModel API:
@@ -190,7 +190,8 @@ describe('query.resolver.ts.ejs', () => {
190
190
  const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
191
191
 
192
192
  expect(resolverFile?.contents).toMatchInlineSnapshot(`
193
- "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID } from 'type-graphql';
193
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID, Float } from 'type-graphql';
194
+ import { GraphQLJSON } from 'graphql-type-json';
194
195
  import { type GraphQLContext, ReadModel } from '../../../shared';
195
196
 
196
197
  @ObjectType()
@@ -226,7 +227,7 @@ describe('query.resolver.ts.ejs', () => {
226
227
  @Ctx() ctx: GraphQLContext,
227
228
  @Arg('sessionId', () => ID, { nullable: true }) sessionId?: string,
228
229
  ): Promise<SuggestedItems[]> {
229
- const model = new ReadModel<SuggestedItems>(ctx.eventStore, 'SuggestedItemsProjection');
230
+ const model = new ReadModel<SuggestedItems>(ctx.database, 'SuggestedItemsProjection');
230
231
 
231
232
  // ## IMPLEMENTATION INSTRUCTIONS ##
232
233
  // You can query the projection using the ReadModel API:
@@ -400,7 +401,7 @@ describe('query.resolver.ts.ejs', () => {
400
401
  @Ctx() ctx: GraphQLContext,
401
402
  @Arg('participantId', () => ID, { nullable: true }) participantId?: string,
402
403
  ): Promise<QuestionnaireProgress[]> {
403
- const model = new ReadModel<QuestionnaireProgress>(ctx.eventStore, 'Questionnaires');
404
+ const model = new ReadModel<QuestionnaireProgress>(ctx.database, 'Questionnaires');
404
405
 
405
406
  // ## IMPLEMENTATION INSTRUCTIONS ##
406
407
  // You can query the projection using the ReadModel API:
@@ -422,4 +423,188 @@ describe('query.resolver.ts.ejs', () => {
422
423
  "
423
424
  `);
424
425
  });
426
+ it('should import Float when Float fields are used', async () => {
427
+ const spec: SpecsSchema = {
428
+ variant: 'specs',
429
+ flows: [
430
+ {
431
+ name: 'product-flow',
432
+ slices: [
433
+ {
434
+ type: 'query',
435
+ name: 'get-product-price',
436
+ request: `
437
+ query GetProductPrice($productId: ID!) {
438
+ productPrice(productId: $productId) {
439
+ productId
440
+ price
441
+ discount
442
+ }
443
+ }
444
+ `,
445
+ client: {
446
+ description: '',
447
+ },
448
+ server: {
449
+ description: '',
450
+ data: [
451
+ {
452
+ origin: {
453
+ type: 'projection',
454
+ idField: 'productId',
455
+ name: 'ProductPricesProjection',
456
+ },
457
+ target: {
458
+ type: 'State',
459
+ name: 'ProductPrice',
460
+ },
461
+ },
462
+ ],
463
+ specs: { name: '', rules: [] },
464
+ },
465
+ },
466
+ ],
467
+ },
468
+ ],
469
+ messages: [
470
+ {
471
+ type: 'state',
472
+ name: 'ProductPrice',
473
+ fields: [
474
+ { name: 'productId', type: 'string', required: true },
475
+ { name: 'price', type: 'number', required: true },
476
+ { name: 'discount', type: 'number', required: true },
477
+ ],
478
+ },
479
+ ],
480
+ };
481
+
482
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
483
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
484
+
485
+ expect(resolverFile?.contents).toContain(
486
+ "import { Query, Resolver, Arg, Ctx, ObjectType, Field, ID, Float } from 'type-graphql';",
487
+ );
488
+ expect(resolverFile?.contents).toContain('@Field(() => Float)');
489
+ });
490
+ it('should import Float when array of numbers is used', async () => {
491
+ const spec: SpecsSchema = {
492
+ variant: 'specs',
493
+ flows: [
494
+ {
495
+ name: 'stats-flow',
496
+ slices: [
497
+ {
498
+ type: 'query',
499
+ name: 'get-stats',
500
+ request: `
501
+ query GetStats($userId: ID!) {
502
+ stats(userId: $userId) {
503
+ userId
504
+ scores
505
+ }
506
+ }
507
+ `,
508
+ client: {
509
+ description: '',
510
+ },
511
+ server: {
512
+ description: '',
513
+ data: [
514
+ {
515
+ origin: {
516
+ type: 'projection',
517
+ idField: 'userId',
518
+ name: 'StatsProjection',
519
+ },
520
+ target: {
521
+ type: 'State',
522
+ name: 'Stats',
523
+ },
524
+ },
525
+ ],
526
+ specs: { name: '', rules: [] },
527
+ },
528
+ },
529
+ ],
530
+ },
531
+ ],
532
+ messages: [
533
+ {
534
+ type: 'state',
535
+ name: 'Stats',
536
+ fields: [
537
+ { name: 'userId', type: 'string', required: true },
538
+ { name: 'scores', type: 'Array<number>', required: true },
539
+ ],
540
+ },
541
+ ],
542
+ };
543
+
544
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
545
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
546
+
547
+ expect(resolverFile?.contents).toContain('Float');
548
+ expect(resolverFile?.contents).toContain('@Field(() => [Float])');
549
+ });
550
+ it('should import Float when Float query arg is used', async () => {
551
+ const spec: SpecsSchema = {
552
+ variant: 'specs',
553
+ flows: [
554
+ {
555
+ name: 'search-flow',
556
+ slices: [
557
+ {
558
+ type: 'query',
559
+ name: 'search-products',
560
+ request: `
561
+ query SearchProducts($minPrice: Float, $maxPrice: Float) {
562
+ searchProducts(minPrice: $minPrice, maxPrice: $maxPrice) {
563
+ productId
564
+ name
565
+ }
566
+ }
567
+ `,
568
+ client: {
569
+ description: '',
570
+ },
571
+ server: {
572
+ description: '',
573
+ data: [
574
+ {
575
+ origin: {
576
+ type: 'projection',
577
+ idField: 'productId',
578
+ name: 'ProductsProjection',
579
+ },
580
+ target: {
581
+ type: 'State',
582
+ name: 'Product',
583
+ },
584
+ },
585
+ ],
586
+ specs: { name: '', rules: [] },
587
+ },
588
+ },
589
+ ],
590
+ },
591
+ ],
592
+ messages: [
593
+ {
594
+ type: 'state',
595
+ name: 'Product',
596
+ fields: [
597
+ { name: 'productId', type: 'string', required: true },
598
+ { name: 'name', type: 'string', required: true },
599
+ ],
600
+ },
601
+ ],
602
+ };
603
+
604
+ const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
605
+ const resolverFile = plans.find((p) => p.outputPath.endsWith('query.resolver.ts'));
606
+
607
+ expect(resolverFile?.contents).toContain('Float');
608
+ expect(resolverFile?.contents).toContain("@Arg('minPrice', () => Float");
609
+ });
425
610
  });
@@ -20,23 +20,46 @@ function baseTs(ts) {
20
20
  }
21
21
  function fieldUsesDate(ts) {
22
22
  const b = baseTs(ts);
23
- if (b === 'Date') return true;
23
+ const gqlType = graphqlType(b);
24
+ if (gqlType.includes('GraphQLISODateTime')) return true;
24
25
  if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*Date\b/.test(b);
25
26
  return false;
26
27
  }
27
28
  function fieldUsesJSON(ts) {
28
29
  const b = baseTs(ts);
29
- if (b === 'unknown' || b === 'any' || b === 'object') return true;
30
+ const gqlType = graphqlType(b);
31
+ if (gqlType.includes('GraphQLJSON') || gqlType.includes('JSON')) return true;
30
32
  if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*(unknown|any|object)\b/.test(b);
31
33
  return false;
32
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
+ }
33
54
 
34
55
  const messageFields = message?.fields ?? [];
35
56
  const usesDate = messageFields.some(f => fieldUsesDate(f.type)) ||
36
- (parsedRequest?.args ?? []).some(a => baseTs(a.tsType) === 'Date');
37
- const usesJSON = messageFields.some(f => fieldUsesJSON(f.type));
57
+ (parsedRequest?.args ?? []).some(a => fieldUsesDate(a.tsType));
58
+ const usesJSON = messageFields.some(f => fieldUsesJSON(f.type)) ||
59
+ (parsedRequest?.args ?? []).some(a => fieldUsesJSON(a.tsType));
60
+ const usesFloat = messageFields.some(f => fieldUsesFloat(f.type)) ||
61
+ (parsedRequest?.args ?? []).some(a => fieldUsesFloat(a.tsType));
38
62
 
39
- // Collect embedded types up-front so we can emit them before the parent
40
63
  const embeddedTypes = [];
41
64
  for (const field of messageFields) {
42
65
  const tsType = field.type ?? 'string';
@@ -48,13 +71,12 @@ for (const field of messageFields) {
48
71
  }
49
72
  }
50
73
  %>
51
- import { Query, Resolver, Arg, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
74
+ import { Query, Resolver, Arg, Ctx, ObjectType, Field<% if (usesID) { %>, ID<% } %><% if (usesFloat) { %>, Float<% } %><% if (usesDate) { %>, GraphQLISODateTime<% } %> } from 'type-graphql';
52
75
  <% if (usesJSON) { %>import { GraphQLJSON } from 'graphql-type-json';
53
76
  <% } %>import { type GraphQLContext, ReadModel } from '../../../shared';
54
77
 
55
- <% // Emit embedded types FIRST — this matches your snapshot order
78
+ <%
56
79
  for (const { typeName, tsType } of embeddedTypes) {
57
- // Extract inner "{ ... }" whether Array<{...}> or "{...}[]"
58
80
  const inner = tsType.trim().startsWith('Array<')
59
81
  ? tsType.trim().replace(/^Array<\{/, '{').replace(/}>$/, '}')
60
82
  : tsType.trim().replace(/\[\]$/, '');
@@ -114,7 +136,7 @@ async <%= queryName %>(
114
136
  %> @Arg('<%= arg.name %>', () => <%= gqlType %>, { nullable: true }) <%= arg.name %>?: <%= tsType %><%= i < parsedRequest.args.length - 1 ? ',' : '' %>
115
137
  <% } } %>
116
138
  ): Promise<<%= viewType %>[]> {
117
- const model = new ReadModel<<%= viewType %>>(ctx.eventStore, '<%= projectionType %>');
139
+ const model = new ReadModel<<%= viewType %>>(ctx.database, '<%= projectionType %>');
118
140
 
119
141
  // ## IMPLEMENTATION INSTRUCTIONS ##
120
142
  // You can query the projection using the ReadModel API: