@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.
- package/CHANGELOG.md +8 -0
- package/dist/src/codegen/extract/index.d.ts +1 -0
- package/dist/src/codegen/extract/index.d.ts.map +1 -1
- package/dist/src/codegen/extract/index.js +1 -0
- package/dist/src/codegen/extract/index.js.map +1 -1
- package/dist/src/codegen/extract/type-helpers.d.ts +13 -0
- package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -0
- package/dist/src/codegen/extract/type-helpers.js +98 -0
- package/dist/src/codegen/extract/type-helpers.js.map +1 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +202 -19
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +47 -47
- package/dist/src/codegen/templates/command/decide.specs.ts +4 -0
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/dist/src/codegen/templates/command/register.specs.ts +1 -1
- package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
- package/dist/src/codegen/templates/command/state.specs.ts +51 -47
- package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
- package/dist/src/codegen/templates/query/projection.specs.ts +19 -2
- package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +6 -3
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
- package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
- package/dist/src/commands/generate-server.d.ts +0 -1
- package/dist/src/commands/generate-server.d.ts.map +1 -1
- package/dist/src/commands/generate-server.js +42 -23
- package/dist/src/commands/generate-server.js.map +1 -1
- package/dist/src/domain/shared/graphql-types.d.ts +10 -0
- package/dist/src/domain/shared/graphql-types.d.ts.map +1 -0
- package/dist/src/domain/shared/graphql-types.js +40 -0
- package/dist/src/domain/shared/graphql-types.js.map +1 -0
- package/dist/src/domain/shared/graphql-types.ts +20 -0
- package/dist/src/domain/shared/index.d.ts +1 -0
- package/dist/src/domain/shared/index.d.ts.map +1 -1
- package/dist/src/domain/shared/index.js +1 -0
- package/dist/src/domain/shared/index.js.map +1 -1
- package/dist/src/domain/shared/index.ts +1 -0
- package/dist/src/domain/shared/sendCommand.d.ts +1 -1
- package/dist/src/domain/shared/sendCommand.d.ts.map +1 -1
- package/dist/src/domain/shared/sendCommand.ts +1 -1
- package/dist/src/domain/shared/types.d.ts +5 -7
- package/dist/src/domain/shared/types.d.ts.map +1 -1
- package/dist/src/domain/shared/types.js +11 -38
- package/dist/src/domain/shared/types.js.map +1 -1
- package/dist/src/domain/shared/types.ts +10 -16
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/codegen/extract/index.ts +1 -0
- package/src/codegen/extract/type-helpers.ts +102 -0
- package/src/codegen/scaffoldFromSchema.ts +273 -15
- package/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/src/codegen/templates/command/decide.specs.specs.ts +47 -47
- package/src/codegen/templates/command/decide.specs.ts +4 -0
- package/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/src/codegen/templates/command/events.ts.ejs +16 -13
- package/src/codegen/templates/command/mutation.resolver.specs.ts +1 -0
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/src/codegen/templates/command/register.specs.ts +1 -1
- package/src/codegen/templates/command/register.ts.ejs +1 -4
- package/src/codegen/templates/command/state.specs.ts +51 -47
- package/src/codegen/templates/command/state.ts.ejs +8 -4
- package/src/codegen/templates/query/projection.specs.ts +19 -2
- package/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/src/codegen/templates/query/query.resolver.specs.ts +6 -3
- package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/src/codegen/templates/react/react.ts.ejs +0 -1
- package/src/codegen/templates/react/register.ts.ejs +0 -1
- package/src/commands/generate-server.ts +50 -25
- package/src/domain/shared/graphql-types.ts +20 -0
- package/src/domain/shared/index.ts +1 -0
- package/src/domain/shared/sendCommand.ts +1 -1
- package/src/domain/shared/types.ts +10 -16
- package/.turbo/turbo-build.log +0 -6
- package/.turbo/turbo-format.log +0 -4
- package/.turbo/turbo-lint.log +0 -4
- package/.turbo/turbo-test.log +0 -14
- 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.
|
|
35
|
-
"@auto-engineer/message-bus": "0.11.
|
|
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.
|
|
46
|
+
"@auto-engineer/cli": "0.11.10"
|
|
47
47
|
},
|
|
48
|
-
"version": "0.11.
|
|
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",
|
|
@@ -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(
|
|
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 =
|
|
82
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
117
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
106
|
+
describe('Should create listing successfully', () => {
|
|
107
|
+
type Events = ListingCreated;
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const given = DeciderSpecification.for<CreateListing, Events, State>({
|
|
110
|
+
decide,
|
|
111
|
+
evolve,
|
|
112
|
+
initialState,
|
|
113
|
+
});
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 = {
|