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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.turbo/turbo-build.log +5 -6
  2. package/.turbo/turbo-format.log +1 -1
  3. package/.turbo/turbo-lint.log +1 -1
  4. package/.turbo/turbo-test.log +4 -4
  5. package/.turbo/turbo-type-check.log +1 -1
  6. package/CHANGELOG.md +18 -0
  7. package/dist/src/codegen/extract/commands.d.ts +1 -1
  8. package/dist/src/codegen/extract/commands.d.ts.map +1 -1
  9. package/dist/src/codegen/extract/data-sink.d.ts +1 -1
  10. package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
  11. package/dist/src/codegen/extract/events.d.ts +1 -1
  12. package/dist/src/codegen/extract/events.d.ts.map +1 -1
  13. package/dist/src/codegen/extract/gwt.d.ts +1 -1
  14. package/dist/src/codegen/extract/gwt.d.ts.map +1 -1
  15. package/dist/src/codegen/extract/index.d.ts +1 -0
  16. package/dist/src/codegen/extract/index.d.ts.map +1 -1
  17. package/dist/src/codegen/extract/index.js +1 -0
  18. package/dist/src/codegen/extract/index.js.map +1 -1
  19. package/dist/src/codegen/extract/messages.d.ts +1 -1
  20. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  21. package/dist/src/codegen/extract/projection.d.ts +1 -1
  22. package/dist/src/codegen/extract/projection.d.ts.map +1 -1
  23. package/dist/src/codegen/extract/query.d.ts +1 -1
  24. package/dist/src/codegen/extract/query.d.ts.map +1 -1
  25. package/dist/src/codegen/extract/states.d.ts +1 -1
  26. package/dist/src/codegen/extract/states.d.ts.map +1 -1
  27. package/dist/src/codegen/extract/type-helpers.d.ts +13 -0
  28. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -0
  29. package/dist/src/codegen/extract/type-helpers.js +98 -0
  30. package/dist/src/codegen/extract/type-helpers.js.map +1 -0
  31. package/dist/src/codegen/scaffoldFromSchema.d.ts +3 -3
  32. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  33. package/dist/src/codegen/scaffoldFromSchema.js +202 -19
  34. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  35. package/dist/src/codegen/templates/command/commands.specs.ts +3 -3
  36. package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
  37. package/dist/src/codegen/templates/command/decide.specs.specs.ts +54 -54
  38. package/dist/src/codegen/templates/command/decide.specs.ts +13 -9
  39. package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
  40. package/dist/src/codegen/templates/command/events.specs.ts +3 -3
  41. package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
  42. package/dist/src/codegen/templates/command/evolve.specs.ts +3 -3
  43. package/dist/src/codegen/templates/command/handle.specs.ts +10 -5
  44. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
  45. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
  46. package/dist/src/codegen/templates/command/register.specs.ts +4 -4
  47. package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
  48. package/dist/src/codegen/templates/command/state.specs.ts +54 -50
  49. package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
  50. package/dist/src/codegen/templates/query/projection.specs.specs.ts +7 -7
  51. package/dist/src/codegen/templates/query/projection.specs.ts +24 -7
  52. package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
  53. package/dist/src/codegen/templates/query/query.resolver.specs.ts +19 -16
  54. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
  55. package/dist/src/codegen/templates/query/state.specs.ts +3 -3
  56. package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
  57. package/dist/src/codegen/templates/react/react.specs.ts +3 -3
  58. package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
  59. package/dist/src/codegen/templates/react/register.specs.ts +3 -3
  60. package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
  61. package/dist/src/codegen/test-data/specVariant1.d.ts +1 -1
  62. package/dist/src/codegen/test-data/specVariant1.d.ts.map +1 -1
  63. package/dist/src/codegen/test-data/specVariant1.js +1 -1
  64. package/dist/src/codegen/test-data/specVariant1.js.map +1 -1
  65. package/dist/src/codegen/types.d.ts +1 -1
  66. package/dist/src/codegen/types.d.ts.map +1 -1
  67. package/dist/src/commands/generate-server.d.ts +0 -1
  68. package/dist/src/commands/generate-server.d.ts.map +1 -1
  69. package/dist/src/commands/generate-server.js +53 -31
  70. package/dist/src/commands/generate-server.js.map +1 -1
  71. package/dist/src/domain/shared/graphql-types.d.ts +10 -0
  72. package/dist/src/domain/shared/graphql-types.d.ts.map +1 -0
  73. package/dist/src/domain/shared/graphql-types.js +40 -0
  74. package/dist/src/domain/shared/graphql-types.js.map +1 -0
  75. package/dist/src/domain/shared/graphql-types.ts +20 -0
  76. package/dist/src/domain/shared/index.d.ts +1 -0
  77. package/dist/src/domain/shared/index.d.ts.map +1 -1
  78. package/dist/src/domain/shared/index.js +1 -0
  79. package/dist/src/domain/shared/index.js.map +1 -1
  80. package/dist/src/domain/shared/index.ts +1 -0
  81. package/dist/src/domain/shared/sendCommand.d.ts +1 -1
  82. package/dist/src/domain/shared/sendCommand.d.ts.map +1 -1
  83. package/dist/src/domain/shared/sendCommand.ts +1 -1
  84. package/dist/src/domain/shared/types.d.ts +5 -7
  85. package/dist/src/domain/shared/types.d.ts.map +1 -1
  86. package/dist/src/domain/shared/types.js +11 -38
  87. package/dist/src/domain/shared/types.js.map +1 -1
  88. package/dist/src/domain/shared/types.ts +10 -16
  89. package/dist/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +4 -4
  91. package/src/codegen/extract/commands.ts +1 -1
  92. package/src/codegen/extract/data-sink.ts +1 -1
  93. package/src/codegen/extract/events.ts +1 -1
  94. package/src/codegen/extract/gwt.ts +1 -1
  95. package/src/codegen/extract/index.ts +1 -0
  96. package/src/codegen/extract/messages.ts +1 -1
  97. package/src/codegen/extract/projection.ts +1 -1
  98. package/src/codegen/extract/query.ts +1 -1
  99. package/src/codegen/extract/states.ts +1 -1
  100. package/src/codegen/extract/type-helpers.ts +102 -0
  101. package/src/codegen/scaffoldFromSchema.ts +283 -25
  102. package/src/codegen/templates/command/commands.specs.ts +3 -3
  103. package/src/codegen/templates/command/commands.ts.ejs +14 -9
  104. package/src/codegen/templates/command/decide.specs.specs.ts +54 -54
  105. package/src/codegen/templates/command/decide.specs.ts +13 -9
  106. package/src/codegen/templates/command/decide.ts.ejs +1 -0
  107. package/src/codegen/templates/command/events.specs.ts +3 -3
  108. package/src/codegen/templates/command/events.ts.ejs +16 -13
  109. package/src/codegen/templates/command/evolve.specs.ts +3 -3
  110. package/src/codegen/templates/command/handle.specs.ts +10 -5
  111. package/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
  112. package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
  113. package/src/codegen/templates/command/register.specs.ts +4 -4
  114. package/src/codegen/templates/command/register.ts.ejs +1 -4
  115. package/src/codegen/templates/command/state.specs.ts +54 -50
  116. package/src/codegen/templates/command/state.ts.ejs +8 -4
  117. package/src/codegen/templates/query/projection.specs.specs.ts +7 -7
  118. package/src/codegen/templates/query/projection.specs.ts +24 -7
  119. package/src/codegen/templates/query/projection.ts.ejs +64 -12
  120. package/src/codegen/templates/query/query.resolver.specs.ts +19 -16
  121. package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
  122. package/src/codegen/templates/query/state.specs.ts +3 -3
  123. package/src/codegen/templates/react/react.specs.specs.ts +3 -3
  124. package/src/codegen/templates/react/react.specs.ts +3 -3
  125. package/src/codegen/templates/react/react.ts.ejs +0 -1
  126. package/src/codegen/templates/react/register.specs.ts +3 -3
  127. package/src/codegen/templates/react/register.ts.ejs +0 -1
  128. package/src/codegen/test-data/specVariant1.ts +2 -2
  129. package/src/codegen/types.ts +1 -1
  130. package/src/commands/generate-server.ts +63 -34
  131. package/src/domain/shared/graphql-types.ts +20 -0
  132. package/src/domain/shared/index.ts +1 -0
  133. package/src/domain/shared/sendCommand.ts +1 -1
  134. package/src/domain/shared/types.ts +10 -16
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/narrative": "0.11.11",
35
+ "@auto-engineer/message-bus": "0.11.11"
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.11"
47
47
  },
48
- "version": "0.11.9",
48
+ "version": "0.11.11",
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",
@@ -1,4 +1,4 @@
1
- import { CommandExample, EventExample } from '@auto-engineer/flow';
1
+ import { CommandExample, EventExample } from '@auto-engineer/narrative';
2
2
  import { Message, MessageDefinition } from '../types';
3
3
  import { extractFieldsFromMessage } from './fields';
4
4
 
@@ -1,4 +1,4 @@
1
- import { CommandExample, Slice, type Example } from '@auto-engineer/flow';
1
+ import { CommandExample, Slice, type Example } from '@auto-engineer/narrative';
2
2
 
3
3
  function resolveStreamId(stream: string, exampleData: Record<string, unknown>): string {
4
4
  return stream.replace(/\$\{([^}]+)\}/g, (_, key: string) => String(exampleData?.[key] ?? 'unknown'));
@@ -1,4 +1,4 @@
1
- import { EventExample } from '@auto-engineer/flow';
1
+ import { EventExample } from '@auto-engineer/narrative';
2
2
  import { Message, MessageDefinition } from '../types';
3
3
  import { extractFieldsFromMessage } from './fields';
4
4
  import { ReactGwtSpec } from './messages';
@@ -1,4 +1,4 @@
1
- import { Slice, CommandExample, EventExample, StateExample, Example } from '@auto-engineer/flow';
1
+ import { Slice, CommandExample, EventExample, StateExample, Example } from '@auto-engineer/narrative';
2
2
  import { GwtCondition } from '../types';
3
3
 
4
4
  export function buildCommandGwtMapping(slice: Slice): Record<string, (GwtCondition & { failingFields?: string[] })[]> {
@@ -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';
@@ -1,5 +1,5 @@
1
1
  import { extractCommandsFromGwt, extractCommandsFromThen } from './commands';
2
- import { CommandExample, ErrorExample, EventExample, Slice, StateExample } from '@auto-engineer/flow';
2
+ import { CommandExample, ErrorExample, EventExample, Slice, StateExample } 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';
@@ -1,4 +1,4 @@
1
- import { Slice } from '@auto-engineer/flow';
1
+ import { Slice } from '@auto-engineer/narrative';
2
2
 
3
3
  interface ProjectionOrigin {
4
4
  type: 'projection';
@@ -1,4 +1,4 @@
1
- import { EventExample, Slice } from '@auto-engineer/flow';
1
+ import { EventExample, Slice } from '@auto-engineer/narrative';
2
2
 
3
3
  interface QueryGwtCondition {
4
4
  description: string;
@@ -1,4 +1,4 @@
1
- import { Slice } from '@auto-engineer/flow';
1
+ import { Slice } from '@auto-engineer/narrative';
2
2
  import { Message, MessageDefinition } from '../types';
3
3
  import { extractFieldsFromMessage } from './fields';
4
4
 
@@ -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
+ }
@@ -6,7 +6,7 @@ import ejs from 'ejs';
6
6
  import { ensureDirExists, ensureDirPath, toKebabCase } from './utils/path';
7
7
  import { camelCase, pascalCase } from 'change-case';
8
8
  import prettier from 'prettier';
9
- import { Flow, Slice, Model } from '@auto-engineer/flow';
9
+ import { Narrative, Slice, Model } from '@auto-engineer/narrative';
10
10
  import createDebug from 'debug';
11
11
 
12
12
  const debug = createDebug('auto:server-generator-apollo-emmett:scaffold');
@@ -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...');
@@ -241,7 +484,7 @@ function extractUsedErrors(gwtMapping: Record<string, (GwtCondition & { failingF
241
484
 
242
485
  async function prepareTemplateData(
243
486
  slice: Slice,
244
- flow: Flow,
487
+ flow: Narrative,
245
488
  commands: Message[],
246
489
  events: Message[],
247
490
  states: Message[],
@@ -313,7 +556,7 @@ async function prepareTemplateData(
313
556
 
314
557
  function annotateEventSources(
315
558
  events: Message[],
316
- flows: Flow[],
559
+ flows: Narrative[],
317
560
  fallbackFlowName: string,
318
561
  fallbackSliceName: string,
319
562
  ): void {
@@ -339,7 +582,7 @@ function canSliceProduceEvent(slice: Slice): boolean {
339
582
  return ['command', 'react'].includes(slice.type) && 'server' in slice && Boolean(slice.server?.specs);
340
583
  }
341
584
 
342
- function findEventSource(flows: Flow[], eventType: string): { flowName: string; sliceName: string } | null {
585
+ function findEventSource(flows: Narrative[], eventType: string): { flowName: string; sliceName: string } | null {
343
586
  debugSlice('Finding source for event: %s', eventType);
344
587
 
345
588
  for (const flow of flows) {
@@ -360,7 +603,7 @@ function findEventSource(flows: Flow[], eventType: string): { flowName: string;
360
603
 
361
604
  function annotateCommandSources(
362
605
  commands: Message[],
363
- flows: Flow[],
606
+ flows: Narrative[],
364
607
  fallbackFlowName: string,
365
608
  fallbackSliceName: string,
366
609
  ): void {
@@ -373,7 +616,7 @@ function annotateCommandSources(
373
616
  }
374
617
  }
375
618
 
376
- function findCommandSource(flows: Flow[], commandType: string): { flowName: string; sliceName: string } | null {
619
+ function findCommandSource(flows: Narrative[], commandType: string): { flowName: string; sliceName: string } | null {
377
620
  debugSlice('Finding source for command: %s', commandType);
378
621
  for (const flow of flows) {
379
622
  for (const slice of flow.slices) {
@@ -403,10 +646,11 @@ function findCommandSource(flows: Flow[], commandType: string): { flowName: stri
403
646
 
404
647
  async function generateFilesForSlice(
405
648
  slice: Slice,
406
- flow: Flow,
649
+ flow: Narrative,
407
650
  sliceDir: string,
408
651
  messages: MessageDefinition[],
409
- flows: Flow[],
652
+ flows: Narrative[],
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,14 +691,14 @@ 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;
454
698
  }
455
699
 
456
700
  export async function generateScaffoldFilePlans(
457
- flows: Flow[],
701
+ flows: Narrative[],
458
702
  messages: Model['messages'],
459
703
  integrations?: Model['integrations'],
460
704
  baseDir = 'src/domain/flows',
@@ -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
  }
@@ -508,7 +766,7 @@ export async function writeScaffoldFilePlans(plans: FilePlan[]) {
508
766
  }
509
767
 
510
768
  export async function scaffoldFromSchema(
511
- flows: Flow[],
769
+ flows: Narrative[],
512
770
  messages: Model['messages'],
513
771
  baseDir = 'src/domain/flows',
514
772
  ): Promise<void> {
@@ -1,12 +1,12 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { Model as SpecsSchema } from '@auto-engineer/flow';
2
+ import { Model as SpecsSchema } from '@auto-engineer/narrative';
3
3
  import { generateScaffoldFilePlans } from '../../scaffoldFromSchema';
4
4
 
5
5
  describe('commands.ts.ejs', () => {
6
6
  it('should generate correct command file', async () => {
7
7
  const spec: SpecsSchema = {
8
8
  variant: 'specs',
9
- flows: [
9
+ narratives: [
10
10
  {
11
11
  name: 'Host creates a listing',
12
12
  slices: [
@@ -72,7 +72,7 @@ describe('commands.ts.ejs', () => {
72
72
  ],
73
73
  };
74
74
 
75
- const plans = await generateScaffoldFilePlans(spec.flows, spec.messages, undefined, 'src/domain/flows');
75
+ const plans = await generateScaffoldFilePlans(spec.narratives, spec.messages, undefined, 'src/domain/flows');
76
76
  const commandFile = plans.find((p) => p.outputPath.endsWith('commands.ts'));
77
77
 
78
78
  expect(commandFile?.contents).toMatchInlineSnapshot(`