@auto-engineer/server-generator-apollo-emmett 0.11.9 → 0.11.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/src/codegen/extract/index.d.ts +1 -0
  3. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  4. package/dist/src/codegen/extract/index.js +1 -0
  5. package/dist/src/codegen/extract/index.js.map +1 -1
  6. package/dist/src/codegen/extract/type-helpers.d.ts +13 -0
  7. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -0
  8. package/dist/src/codegen/extract/type-helpers.js +98 -0
  9. package/dist/src/codegen/extract/type-helpers.js.map +1 -0
  10. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  11. package/dist/src/codegen/scaffoldFromSchema.js +202 -19
  12. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  13. package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
  14. package/dist/src/codegen/templates/command/decide.specs.specs.ts +47 -47
  15. package/dist/src/codegen/templates/command/decide.specs.ts +4 -0
  16. package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
  17. package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
  18. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
  19. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
  20. package/dist/src/codegen/templates/command/register.specs.ts +1 -1
  21. package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
  22. package/dist/src/codegen/templates/command/state.specs.ts +51 -47
  23. package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
  24. package/dist/src/codegen/templates/query/projection.specs.ts +19 -2
  25. package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
  26. package/dist/src/codegen/templates/query/query.resolver.specs.ts +6 -3
  27. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
  28. package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
  29. package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
  30. package/dist/src/commands/generate-server.d.ts +0 -1
  31. package/dist/src/commands/generate-server.d.ts.map +1 -1
  32. package/dist/src/commands/generate-server.js +42 -23
  33. package/dist/src/commands/generate-server.js.map +1 -1
  34. package/dist/src/domain/shared/graphql-types.d.ts +10 -0
  35. package/dist/src/domain/shared/graphql-types.d.ts.map +1 -0
  36. package/dist/src/domain/shared/graphql-types.js +40 -0
  37. package/dist/src/domain/shared/graphql-types.js.map +1 -0
  38. package/dist/src/domain/shared/graphql-types.ts +20 -0
  39. package/dist/src/domain/shared/index.d.ts +1 -0
  40. package/dist/src/domain/shared/index.d.ts.map +1 -1
  41. package/dist/src/domain/shared/index.js +1 -0
  42. package/dist/src/domain/shared/index.js.map +1 -1
  43. package/dist/src/domain/shared/index.ts +1 -0
  44. package/dist/src/domain/shared/sendCommand.d.ts +1 -1
  45. package/dist/src/domain/shared/sendCommand.d.ts.map +1 -1
  46. package/dist/src/domain/shared/sendCommand.ts +1 -1
  47. package/dist/src/domain/shared/types.d.ts +5 -7
  48. package/dist/src/domain/shared/types.d.ts.map +1 -1
  49. package/dist/src/domain/shared/types.js +11 -38
  50. package/dist/src/domain/shared/types.js.map +1 -1
  51. package/dist/src/domain/shared/types.ts +10 -16
  52. package/dist/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +4 -4
  54. package/src/codegen/extract/index.ts +1 -0
  55. package/src/codegen/extract/type-helpers.ts +102 -0
  56. package/src/codegen/scaffoldFromSchema.ts +273 -15
  57. package/src/codegen/templates/command/commands.ts.ejs +14 -9
  58. package/src/codegen/templates/command/decide.specs.specs.ts +47 -47
  59. package/src/codegen/templates/command/decide.specs.ts +4 -0
  60. package/src/codegen/templates/command/decide.ts.ejs +1 -0
  61. package/src/codegen/templates/command/events.ts.ejs +16 -13
  62. package/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
  63. package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
  64. package/src/codegen/templates/command/register.specs.ts +1 -1
  65. package/src/codegen/templates/command/register.ts.ejs +1 -4
  66. package/src/codegen/templates/command/state.specs.ts +51 -47
  67. package/src/codegen/templates/command/state.ts.ejs +8 -4
  68. package/src/codegen/templates/query/projection.specs.ts +19 -2
  69. package/src/codegen/templates/query/projection.ts.ejs +64 -12
  70. package/src/codegen/templates/query/query.resolver.specs.ts +6 -3
  71. package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
  72. package/src/codegen/templates/react/react.ts.ejs +0 -1
  73. package/src/codegen/templates/react/register.ts.ejs +0 -1
  74. package/src/commands/generate-server.ts +50 -25
  75. package/src/domain/shared/graphql-types.ts +20 -0
  76. package/src/domain/shared/index.ts +1 -0
  77. package/src/domain/shared/sendCommand.ts +1 -1
  78. package/src/domain/shared/types.ts +10 -16
  79. package/.turbo/turbo-build.log +0 -6
  80. package/.turbo/turbo-format.log +0 -4
  81. package/.turbo/turbo-lint.log +0 -4
  82. package/.turbo/turbo-test.log +0 -14
  83. package/.turbo/turbo-type-check.log +0 -4
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/flow": "0.11.9",
35
- "@auto-engineer/message-bus": "0.11.9"
34
+ "@auto-engineer/flow": "0.11.10",
35
+ "@auto-engineer/message-bus": "0.11.10"
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.9"
46
+ "@auto-engineer/cli": "0.11.10"
47
47
  },
48
- "version": "0.11.9",
48
+ "version": "0.11.10",
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",
@@ -6,3 +6,4 @@ export * from './messages';
6
6
  export * from './query';
7
7
  export * from './projection';
8
8
  export * from './imports';
9
+ export * from './type-helpers';
@@ -0,0 +1,102 @@
1
+ export function isInlineObject(ts: string): boolean {
2
+ return /^\{[\s\S]*\}$/.test((ts ?? '').trim());
3
+ }
4
+
5
+ export function isInlineObjectArray(ts: string): boolean {
6
+ const t = (ts ?? '').trim();
7
+ return /^Array<\{[\s\S]*\}>$/.test(t) || /^\{[\s\S]*\}\[\]$/.test(t);
8
+ }
9
+
10
+ export function baseTs(ts: string): string {
11
+ return (ts ?? 'string').replace(/\s*\|\s*null\b/g, '').trim();
12
+ }
13
+
14
+ export function createIsEnumType(toTsFieldType: (ts: string) => string) {
15
+ return (tsType: string): boolean => {
16
+ const converted = toTsFieldType(tsType);
17
+ const base = converted
18
+ .replace(/\s*\|\s*null\b/g, '')
19
+ .replace(/\[\]$/, '')
20
+ .trim();
21
+ return (
22
+ /^[A-Z][a-zA-Z0-9]*$/.test(base) &&
23
+ ![
24
+ 'String',
25
+ 'Number',
26
+ 'Boolean',
27
+ 'Date',
28
+ 'ID',
29
+ 'Int',
30
+ 'Float',
31
+ 'GraphQLISODateTime',
32
+ 'GraphQLJSON',
33
+ 'JSON',
34
+ ].includes(base)
35
+ );
36
+ };
37
+ }
38
+
39
+ export function createFieldUsesDate(graphqlType: (ts: string) => string) {
40
+ return (ts: string): boolean => {
41
+ const b = baseTs(ts);
42
+ const gqlType = graphqlType(b);
43
+ if (gqlType.includes('GraphQLISODateTime')) return true;
44
+ if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*Date\b/.test(b);
45
+ return false;
46
+ };
47
+ }
48
+
49
+ export function createFieldUsesJSON(graphqlType: (ts: string) => string) {
50
+ return (ts: string): boolean => {
51
+ const b = baseTs(ts);
52
+ const gqlType = graphqlType(b);
53
+ if (gqlType.includes('GraphQLJSON') || gqlType.includes('JSON')) return true;
54
+ if (isInlineObject(b) || isInlineObjectArray(b)) return /:\s*(unknown|any|object)\b/.test(b);
55
+ return false;
56
+ };
57
+ }
58
+
59
+ export function createFieldUsesFloat(graphqlType: (ts: string) => string) {
60
+ return (ts: string): boolean => {
61
+ const b = baseTs(ts);
62
+ const gqlType = graphqlType(b);
63
+ if (gqlType.includes('Float')) return true;
64
+ if (isInlineObject(b) || isInlineObjectArray(b)) {
65
+ const inner = b.trim().startsWith('Array<')
66
+ ? b
67
+ .trim()
68
+ .replace(/^Array<\{/, '{')
69
+ .replace(/}>$/, '}')
70
+ : b.trim().replace(/\[\]$/, '');
71
+ const match = inner.match(/^\{([\s\S]*)\}$/);
72
+ const body = match ? match[1] : '';
73
+ const rawFields = body.split(/[,;]\s*/).filter(Boolean);
74
+ return rawFields.some((f) => {
75
+ const parts = f.split(':');
76
+ const type = parts.slice(1).join(':').trim();
77
+ return type.length > 0 && graphqlType(type).includes('Float');
78
+ });
79
+ }
80
+ return false;
81
+ };
82
+ }
83
+
84
+ export function extractEnumName(tsType: string, toTsFieldType: (ts: string) => string): string {
85
+ return toTsFieldType(tsType)
86
+ .replace(/\s*\|\s*null\b/g, '')
87
+ .replace(/\[\]$/, '')
88
+ .trim();
89
+ }
90
+
91
+ export function createCollectEnumNames(isEnumType: (tsType: string) => boolean, toTsFieldType: (ts: string) => string) {
92
+ return (fields: Array<{ type?: string; tsType?: string }>): string[] => {
93
+ const enumNames = new Set<string>();
94
+ for (const field of fields) {
95
+ const fieldType = field.type ?? field.tsType;
96
+ if (fieldType !== undefined && fieldType !== null && fieldType.length > 0 && isEnumType(fieldType)) {
97
+ enumNames.add(extractEnumName(fieldType, toTsFieldType));
98
+ }
99
+ }
100
+ return Array.from(enumNames).sort();
101
+ };
102
+ }
@@ -25,6 +25,14 @@ import {
25
25
  getAllEventTypes,
26
26
  getLocalEvents,
27
27
  createEventUnionType,
28
+ isInlineObject as isInlineObjectHelper,
29
+ isInlineObjectArray as isInlineObjectArrayHelper,
30
+ baseTs,
31
+ createIsEnumType,
32
+ createFieldUsesDate,
33
+ createFieldUsesJSON,
34
+ createFieldUsesFloat,
35
+ createCollectEnumNames,
28
36
  } from './extract';
29
37
 
30
38
  function extractGwtSpecs(slice: Slice) {
@@ -61,12 +69,214 @@ const defaultFilesByType: Record<string, string[]> = {
61
69
  react: ['react.ts.ejs', 'react.specs.ts.ejs', 'register.ts.ejs'],
62
70
  };
63
71
 
72
+ interface EnumDefinition {
73
+ name: string;
74
+ values: string[];
75
+ unionString: string;
76
+ }
77
+
78
+ interface EnumContext {
79
+ enums: EnumDefinition[];
80
+ unionToEnumName: Map<string, string>;
81
+ }
82
+
83
+ function isStringLiteralUnion(s: string): boolean {
84
+ return /^"[^"]+"(\s*\|\s*"[^"]+")+$/.test(s.trim()) || /^'[^']+'(\s*\|\s*'[^']+)+$/.test(s.trim());
85
+ }
86
+
87
+ function extractStringLiteralValues(unionString: string): string[] {
88
+ const doubleQuoted = unionString.match(/"([^"]+)"/g);
89
+ const singleQuoted = unionString.match(/'([^']+)'/g);
90
+ const matches = doubleQuoted ?? singleQuoted;
91
+ if (matches === null) return [];
92
+ return matches.map((m) => m.slice(1, -1));
93
+ }
94
+
95
+ function normalizeUnionString(values: string[]): string {
96
+ return values
97
+ .slice()
98
+ .sort()
99
+ .map((v) => `'${v}'`)
100
+ .join(' | ');
101
+ }
102
+
103
+ function generateEnumName(fieldName: string, existingNames: Set<string>): string {
104
+ const baseName = pascalCase(fieldName);
105
+ if (!existingNames.has(baseName)) {
106
+ return baseName;
107
+ }
108
+ let counter = 2;
109
+ while (existingNames.has(`${baseName}${counter}`)) {
110
+ counter++;
111
+ }
112
+ return `${baseName}${counter}`;
113
+ }
114
+
115
+ function processFieldForEnum(
116
+ field: { name: string; type: string },
117
+ messageName: string,
118
+ unionToEnumName: Map<string, string>,
119
+ existingEnumNames: Set<string>,
120
+ ): EnumDefinition | null {
121
+ const tsType = field.type;
122
+ debug(' Field: %s, type: %O', field.name, tsType);
123
+ if (tsType === null || tsType === undefined) return null;
124
+
125
+ const cleanType = tsType.replace(/\s*\|\s*null\b/g, '').trim();
126
+ const isUnion = isStringLiteralUnion(cleanType);
127
+ debug(' cleanType: %O, isStringLiteralUnion: %s', cleanType, isUnion);
128
+ if (!isUnion) return null;
129
+
130
+ const values = extractStringLiteralValues(cleanType);
131
+ debug(' extracted values: %O', values);
132
+ if (values.length === 0) return null;
133
+
134
+ const normalized = normalizeUnionString(values);
135
+
136
+ if (unionToEnumName.has(normalized)) {
137
+ debug(' already has enum for this union, skipping');
138
+ return null;
139
+ }
140
+
141
+ const enumName = generateEnumName(`${messageName}${pascalCase(field.name)}`, existingEnumNames);
142
+ existingEnumNames.add(enumName);
143
+ debug(' ✓ Creating enum: %s with values %O', enumName, values);
144
+
145
+ const enumDef: EnumDefinition = {
146
+ name: enumName,
147
+ values,
148
+ unionString: normalized,
149
+ };
150
+
151
+ unionToEnumName.set(normalized, enumName);
152
+ return enumDef;
153
+ }
154
+
155
+ function extractEnumsFromMessages(messages: MessageDefinition[]): EnumContext {
156
+ const unionToEnumName = new Map<string, string>();
157
+ const enums: EnumDefinition[] = [];
158
+ const existingEnumNames = new Set<string>();
159
+
160
+ debug('extractEnumsFromMessages: processing %d messages', messages.length);
161
+
162
+ for (const message of messages) {
163
+ debug(' Message: %s, has fields: %s', message.name, message.fields !== undefined && message.fields !== null);
164
+ if (message.fields === undefined || message.fields === null) continue;
165
+
166
+ for (const field of message.fields) {
167
+ const enumDef = processFieldForEnum(field, message.name, unionToEnumName, existingEnumNames);
168
+ if (enumDef !== null) {
169
+ enums.push(enumDef);
170
+ }
171
+ }
172
+ }
173
+
174
+ debug('extractEnumsFromMessages: found %d enums total', enums.length);
175
+ debug(
176
+ ' Enum names: %O',
177
+ enums.map((e) => e.name),
178
+ );
179
+ debug(' unionToEnumName map size: %d', unionToEnumName.size);
180
+ for (const [union, enumName] of unionToEnumName.entries()) {
181
+ debug(' "%s" -> %s', union, enumName);
182
+ }
183
+ return { enums, unionToEnumName };
184
+ }
185
+
186
+ function generateEnumTypeScript(enumDef: EnumDefinition): string {
187
+ const entries = enumDef.values.map((val) => {
188
+ const key = val.toUpperCase().replace(/[^A-Z0-9]/g, '_');
189
+ return ` ${key} = '${val}',`;
190
+ });
191
+
192
+ return `export enum ${enumDef.name} {
193
+ ${entries.join('\n')}
194
+ }
195
+
196
+ registerEnumType(${enumDef.name}, {
197
+ name: '${enumDef.name}',
198
+ });`;
199
+ }
200
+
201
+ async function appendEnumsToSharedTypes(baseDir: string, enums: EnumDefinition[]): Promise<void> {
202
+ if (enums.length === 0) return;
203
+
204
+ const sharedTypesPath = path.join(baseDir, 'shared', 'types.ts');
205
+
206
+ let existingContent = '';
207
+ try {
208
+ existingContent = await fs.readFile(sharedTypesPath, 'utf8');
209
+ } catch {
210
+ debug('Types file does not exist yet at %s, will create it', sharedTypesPath);
211
+ }
212
+
213
+ const enumsToAdd = enums.filter((e) => {
214
+ const enumPattern = new RegExp(`export\\s+enum\\s+${e.name}\\s*\\{`, 'm');
215
+ return !enumPattern.test(existingContent);
216
+ });
217
+
218
+ if (enumsToAdd.length === 0) {
219
+ debug('All enums already exist in %s, skipping', sharedTypesPath);
220
+ return;
221
+ }
222
+
223
+ debug('Adding %d new enums to %s', enumsToAdd.length, sharedTypesPath);
224
+
225
+ const hasRegisterEnumImport = existingContent.includes('registerEnumType');
226
+ const enumCode = enumsToAdd.map((e) => generateEnumTypeScript(e)).join('\n\n');
227
+
228
+ let newContent: string;
229
+ if (hasRegisterEnumImport) {
230
+ newContent = `${existingContent.trimEnd()}\n\n${enumCode}\n`;
231
+ } else {
232
+ const importMatch = existingContent.match(/^([\s\S]*?)(import.*from\s+['"]type-graphql['"];?\s*\n)/m);
233
+ if (importMatch !== null) {
234
+ const beforeImport = importMatch[1];
235
+ const typeGraphqlImport = importMatch[2];
236
+ const afterImport = existingContent.slice(beforeImport.length + typeGraphqlImport.length);
237
+
238
+ const updatedImport = typeGraphqlImport.replace(
239
+ /^import\s*\{([^}]*)\}\s*from\s*['"]type-graphql['"];?\s*$/m,
240
+ (_match: string, imports: string): string => {
241
+ const importsList = imports
242
+ .split(',')
243
+ .map((s) => s.trim())
244
+ .filter((item) => item.length > 0);
245
+ if (!importsList.includes('registerEnumType')) {
246
+ importsList.push('registerEnumType');
247
+ }
248
+ return `import { ${importsList.join(', ')} } from 'type-graphql';`;
249
+ },
250
+ );
251
+
252
+ newContent = `${beforeImport}${updatedImport}${afterImport.trimEnd()}\n\n${enumCode}\n`;
253
+ } else {
254
+ newContent = `import { registerEnumType } from 'type-graphql';\n\n${existingContent.trimEnd()}\n\n${enumCode}\n`;
255
+ }
256
+ }
257
+
258
+ const prettierConfig = await prettier.resolveConfig(sharedTypesPath);
259
+ const formatted = await prettier.format(newContent, {
260
+ ...prettierConfig,
261
+ parser: 'typescript',
262
+ filepath: sharedTypesPath,
263
+ });
264
+
265
+ await fs.mkdir(path.dirname(sharedTypesPath), { recursive: true });
266
+ await fs.writeFile(sharedTypesPath, formatted, 'utf8');
267
+ debug('Appended %d enums to %s', enums.length, sharedTypesPath);
268
+ }
269
+
64
270
  export interface FilePlan {
65
271
  outputPath: string;
66
272
  contents: string;
67
273
  }
68
274
 
69
- async function renderTemplate(templatePath: string, data: Record<string, unknown>): Promise<string> {
275
+ async function renderTemplate(
276
+ templatePath: string,
277
+ data: Record<string, unknown>,
278
+ unionToEnumName: Map<string, string> = new Map(),
279
+ ): Promise<string> {
70
280
  debugTemplate('Rendering template: %s', templatePath);
71
281
  debugTemplate('Data keys: %o', Object.keys(data));
72
282
 
@@ -78,14 +288,13 @@ async function renderTemplate(templatePath: string, data: Record<string, unknown
78
288
  });
79
289
  debugTemplate('Template compiled successfully');
80
290
 
81
- const isInlineObject = (s: string) => /^\{[\s\S]*\}$/.test(s.trim());
82
- const isStringLiteralUnion = (s: string) => /^"(?:[^"]+)"\s*(\|\s*"(?:[^"]+)")+$/.test(s.trim());
291
+ const isInlineObject = isInlineObjectHelper;
292
+ const isInlineObjectArray = isInlineObjectArrayHelper;
293
+
83
294
  const convertPrimitiveType = (base: string): string => {
84
- // GraphQL-native scalars
85
295
  if (base === 'ID') return 'ID';
86
296
  if (base === 'Int') return 'Int';
87
297
  if (base === 'Float') return 'Float';
88
- // TS primitives
89
298
  if (base === 'string') return 'String';
90
299
  if (base === 'number') return 'Float';
91
300
  if (base === 'boolean') return 'Boolean';
@@ -93,33 +302,58 @@ async function renderTemplate(templatePath: string, data: Record<string, unknown
93
302
  return 'String';
94
303
  };
95
304
 
305
+ const resolveEnumOrString = (base: string): string => {
306
+ if (!isStringLiteralUnion(base)) return 'String';
307
+ const values = extractStringLiteralValues(base);
308
+ const normalized = normalizeUnionString(values);
309
+ const enumName = unionToEnumName.get(normalized);
310
+ return enumName ?? 'String';
311
+ };
312
+
96
313
  const graphqlType = (rawTs: string): string => {
97
314
  const t = (rawTs ?? '').trim();
98
315
  if (!t) return 'String';
99
316
  const base = t.replace(/\s*\|\s*null\b/g, '').trim();
100
- // arrays
317
+
101
318
  const arr1 = base.match(/^Array<(.*)>$/);
102
319
  const arr2 = base.match(/^(.*)\[\]$/);
103
- if (arr1) return `[${graphqlType(arr1[1].trim())}]`;
104
- if (arr2) return `[${graphqlType(arr2[1].trim())}]`;
105
- // JSON
320
+ if (arr1 !== null) return `[${graphqlType(arr1[1].trim())}]`;
321
+ if (arr2 !== null) return `[${graphqlType(arr2[1].trim())}]`;
322
+
106
323
  if (base === 'unknown' || base === 'any') return 'GraphQLJSON';
107
324
  if (base === 'object') return 'JSON';
108
325
  if (isInlineObject(base)) return 'JSON';
109
- if (isStringLiteralUnion(base)) return 'String';
326
+ if (isStringLiteralUnion(base)) return resolveEnumOrString(base);
327
+
110
328
  return convertPrimitiveType(base);
111
329
  };
112
330
 
113
331
  const toTsFieldType = (ts: string): string => {
114
332
  if (!ts) return 'string';
115
333
  const t = ts.trim();
116
- const arr = t.match(/^Array<(.*)>$/);
117
- if (arr) return `${arr[1].trim()}[]`;
334
+ const cleanType = t.replace(/\s*\|\s*null\b/g, '').trim();
335
+
336
+ const arr = cleanType.match(/^Array<(.*)>$/);
337
+ if (arr !== null) return `${toTsFieldType(arr[1].trim())}[]`;
338
+
339
+ if (isStringLiteralUnion(cleanType)) {
340
+ const values = extractStringLiteralValues(cleanType);
341
+ const normalized = normalizeUnionString(values);
342
+ const enumName = unionToEnumName.get(normalized);
343
+ if (enumName !== undefined) return enumName;
344
+ }
345
+
118
346
  return t;
119
347
  };
120
348
 
121
349
  const isNullable = (rawTs: string): boolean => /\|\s*null\b/.test(rawTs);
122
350
 
351
+ const isEnumType = createIsEnumType(toTsFieldType);
352
+ const fieldUsesDate = createFieldUsesDate(graphqlType);
353
+ const fieldUsesJSON = createFieldUsesJSON(graphqlType);
354
+ const fieldUsesFloat = createFieldUsesFloat(graphqlType);
355
+ const collectEnumNames = createCollectEnumNames(isEnumType, toTsFieldType);
356
+
123
357
  const result = await template({
124
358
  ...data,
125
359
  pascalCase,
@@ -132,6 +366,14 @@ async function renderTemplate(templatePath: string, data: Record<string, unknown
132
366
  formatDataObject,
133
367
  messages: data.messages,
134
368
  message: data.message,
369
+ isInlineObject,
370
+ isInlineObjectArray,
371
+ baseTs,
372
+ isEnumType,
373
+ fieldUsesDate,
374
+ fieldUsesJSON,
375
+ fieldUsesFloat,
376
+ collectEnumNames,
135
377
  });
136
378
 
137
379
  debugTemplate('Template rendered, output size: %d bytes', result.length);
@@ -188,6 +430,7 @@ async function generateFileForTemplate(
188
430
  slice: Slice,
189
431
  sliceDir: string,
190
432
  templateData: Record<string, unknown>,
433
+ unionToEnumName: Map<string, string> = new Map(),
191
434
  ): Promise<FilePlan> {
192
435
  debugFiles('Generating file from template: %s', templateFile);
193
436
  debugFiles(' Slice type: %s', slice.type);
@@ -201,7 +444,7 @@ async function generateFileForTemplate(
201
444
  debugFiles(' Template path: %s', templatePath);
202
445
  debugFiles(' Output path: %s', outputPath);
203
446
 
204
- const contents = await renderTemplate(templatePath, templateData);
447
+ const contents = await renderTemplate(templatePath, templateData, unionToEnumName);
205
448
  debugFiles(' Rendered content size: %d bytes', contents.length);
206
449
 
207
450
  debugFiles(' Formatting with Prettier...');
@@ -407,6 +650,7 @@ async function generateFilesForSlice(
407
650
  sliceDir: string,
408
651
  messages: MessageDefinition[],
409
652
  flows: Flow[],
653
+ unionToEnumName: Map<string, string>,
410
654
  integrations?: Model['integrations'],
411
655
  ): Promise<FilePlan[]> {
412
656
  debugSlice('Generating files for slice: %s (type: %s)', slice.name, slice.type);
@@ -447,7 +691,7 @@ async function generateFilesForSlice(
447
691
 
448
692
  debugSlice(' Generating %d files from templates', templates.length);
449
693
  const plans = await Promise.all(
450
- templates.map((template) => generateFileForTemplate(template, slice, sliceDir, templateData)),
694
+ templates.map((template) => generateFileForTemplate(template, slice, sliceDir, templateData, unionToEnumName)),
451
695
  );
452
696
  debugSlice(' Generated %d file plans for slice: %s', plans.length, slice.name);
453
697
  return plans;
@@ -464,6 +708,12 @@ export async function generateScaffoldFilePlans(
464
708
  debug(' Number of messages: %d', messages?.length ?? 0);
465
709
  debug(' Base directory: %s', baseDir);
466
710
 
711
+ const { enums, unionToEnumName } = extractEnumsFromMessages(messages ?? []);
712
+ debug(' Extracted %d enums from messages', enums.length);
713
+
714
+ const domainBaseDir = baseDir.replace(/\/flows$/, '');
715
+ await appendEnumsToSharedTypes(domainBaseDir, enums);
716
+
467
717
  const allPlans: FilePlan[] = [];
468
718
 
469
719
  for (const flow of flows) {
@@ -476,7 +726,15 @@ export async function generateScaffoldFilePlans(
476
726
  debugFlow(' Processing slice: %s (type: %s)', slice.name, slice.type);
477
727
  const sliceDir = ensureDirPath(flowDir, toKebabCase(slice.name));
478
728
  debugFlow(' Slice directory: %s', sliceDir);
479
- const plans = await generateFilesForSlice(slice, flow, sliceDir, messages, flows, integrations);
729
+ const plans = await generateFilesForSlice(
730
+ slice,
731
+ flow,
732
+ sliceDir,
733
+ messages ?? [],
734
+ flows,
735
+ unionToEnumName,
736
+ integrations,
737
+ );
480
738
  debugFlow(' Generated %d plans for slice', plans.length);
481
739
  allPlans.push(...plans);
482
740
  }
@@ -1,11 +1,16 @@
1
- import { Command } from "@event-driven-io/emmett";
1
+ <%
2
+ const enumList = collectEnumNames(commands.flatMap(c => c.fields));
3
+ %>import { Command } from "@event-driven-io/emmett";
4
+ <% if (enumList.length > 0) { -%>
5
+ import { <%= enumList.join(', ') %> } from '../../../shared';
6
+ <% } -%>
2
7
  <% for (const command of commands) { -%>
3
- export type <%= pascalCase(command.type) %> = Command<
4
- '<%= command.type %>',
5
- {
6
- <% for (const field of command.fields) { -%>
7
- <%- field.name %>: <%- field.tsType %>;
8
- <% } -%>
9
- }
10
- >;
8
+ export type <%= pascalCase(command.type) %> = Command<
9
+ '<%= command.type %>',
10
+ {
11
+ <% for (const field of command.fields) { -%>
12
+ <%- field.name %>: <%- toTsFieldType(field.tsType) %>;
13
+ <% } -%>
14
+ }
15
+ >;
11
16
  <% } -%>
@@ -95,57 +95,57 @@ describe('spec.ts.ejs', () => {
95
95
  const specFile = plans.find((p) => p.outputPath.endsWith('specs.ts'));
96
96
 
97
97
  expect(specFile?.contents).toMatchInlineSnapshot(`
98
- "import { describe, it } from 'vitest';
99
- import { DeciderSpecification } from '@event-driven-io/emmett';
100
- import { decide } from './decide';
101
- import { evolve } from './evolve';
102
- import { initialState, State } from './state';
103
- import type { ListingCreated } from './events';
104
- import type { CreateListing } from './commands';
98
+ "import { describe, it } from 'vitest';
99
+ import { DeciderSpecification } from '@event-driven-io/emmett';
100
+ import { decide } from './decide';
101
+ import { evolve } from './evolve';
102
+ import { initialState, State } from './state';
103
+ import type { ListingCreated } from './events';
104
+ import type { CreateListing } from './commands';
105
105
 
106
- describe('Should create listing successfully', () => {
107
- type Events = ListingCreated;
106
+ describe('Should create listing successfully', () => {
107
+ type Events = ListingCreated;
108
108
 
109
- const given = DeciderSpecification.for<CreateListing, Events, State>({
110
- decide,
111
- evolve,
112
- initialState,
113
- });
109
+ const given = DeciderSpecification.for<CreateListing, Events, State>({
110
+ decide,
111
+ evolve,
112
+ initialState,
113
+ });
114
114
 
115
- it('User creates listing with valid data', () => {
116
- given([])
117
- .when({
118
- type: 'CreateListing',
119
- data: {
120
- propertyId: 'listing_123',
121
- title: 'blah',
122
- pricePerNight: 250,
123
- maxGuests: 4,
124
- amenities: ['wifi', 'kitchen'],
125
- available: true,
126
- tags: ['some tag'],
127
- rating: 4.8,
128
- metadata: { foo: 'bar' },
129
- listedAt: new Date('2024-01-15T10:00:00Z'),
130
- },
131
- metadata: { now: new Date() },
132
- })
115
+ it('User creates listing with valid data', () => {
116
+ given([])
117
+ .when({
118
+ type: 'CreateListing',
119
+ data: {
120
+ propertyId: 'listing_123',
121
+ title: 'blah',
122
+ pricePerNight: 250,
123
+ maxGuests: 4,
124
+ amenities: ['wifi', 'kitchen'],
125
+ available: true,
126
+ tags: ['some tag'],
127
+ rating: 4.8,
128
+ metadata: { foo: 'bar' },
129
+ listedAt: new Date('2024-01-15T10:00:00Z'),
130
+ },
131
+ metadata: { now: new Date() },
132
+ })
133
133
 
134
- .then([
135
- {
136
- type: 'ListingCreated',
137
- data: {
138
- propertyId: 'listing_123',
139
- listedAt: new Date('2024-01-15T10:00:00Z'),
140
- rating: 4.8,
141
- metadata: { foo: 'bar' },
142
- },
143
- },
144
- ]);
145
- });
146
- });
147
- "
148
- `);
134
+ .then([
135
+ {
136
+ type: 'ListingCreated',
137
+ data: {
138
+ propertyId: 'listing_123',
139
+ listedAt: new Date('2024-01-15T10:00:00Z'),
140
+ rating: 4.8,
141
+ metadata: { foo: 'bar' },
142
+ },
143
+ },
144
+ ]);
145
+ });
146
+ });
147
+ "
148
+ `);
149
149
  });
150
150
  it('should include given events in the spec file when provided', async () => {
151
151
  const spec: SpecsSchema = {