@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.
- package/.turbo/turbo-build.log +5 -6
- package/.turbo/turbo-format.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +4 -4
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/src/codegen/extract/commands.d.ts +1 -1
- package/dist/src/codegen/extract/commands.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.d.ts +1 -1
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/events.d.ts +1 -1
- package/dist/src/codegen/extract/events.d.ts.map +1 -1
- package/dist/src/codegen/extract/gwt.d.ts +1 -1
- package/dist/src/codegen/extract/gwt.d.ts.map +1 -1
- 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/messages.d.ts +1 -1
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/projection.d.ts +1 -1
- package/dist/src/codegen/extract/projection.d.ts.map +1 -1
- package/dist/src/codegen/extract/query.d.ts +1 -1
- package/dist/src/codegen/extract/query.d.ts.map +1 -1
- package/dist/src/codegen/extract/states.d.ts +1 -1
- package/dist/src/codegen/extract/states.d.ts.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 +3 -3
- 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.specs.ts +3 -3
- package/dist/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +54 -54
- package/dist/src/codegen/templates/command/decide.specs.ts +13 -9
- package/dist/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/dist/src/codegen/templates/command/events.specs.ts +3 -3
- package/dist/src/codegen/templates/command/events.ts.ejs +16 -13
- package/dist/src/codegen/templates/command/evolve.specs.ts +3 -3
- package/dist/src/codegen/templates/command/handle.specs.ts +10 -5
- package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
- package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/dist/src/codegen/templates/command/register.specs.ts +4 -4
- package/dist/src/codegen/templates/command/register.ts.ejs +1 -4
- package/dist/src/codegen/templates/command/state.specs.ts +54 -50
- package/dist/src/codegen/templates/command/state.ts.ejs +8 -4
- package/dist/src/codegen/templates/query/projection.specs.specs.ts +7 -7
- package/dist/src/codegen/templates/query/projection.specs.ts +24 -7
- package/dist/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/dist/src/codegen/templates/query/query.resolver.specs.ts +19 -16
- package/dist/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/dist/src/codegen/templates/query/state.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.specs.ts +3 -3
- package/dist/src/codegen/templates/react/react.ts.ejs +0 -1
- package/dist/src/codegen/templates/react/register.specs.ts +3 -3
- package/dist/src/codegen/templates/react/register.ts.ejs +0 -1
- package/dist/src/codegen/test-data/specVariant1.d.ts +1 -1
- package/dist/src/codegen/test-data/specVariant1.d.ts.map +1 -1
- package/dist/src/codegen/test-data/specVariant1.js +1 -1
- package/dist/src/codegen/test-data/specVariant1.js.map +1 -1
- package/dist/src/codegen/types.d.ts +1 -1
- package/dist/src/codegen/types.d.ts.map +1 -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 +53 -31
- 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/commands.ts +1 -1
- package/src/codegen/extract/data-sink.ts +1 -1
- package/src/codegen/extract/events.ts +1 -1
- package/src/codegen/extract/gwt.ts +1 -1
- package/src/codegen/extract/index.ts +1 -0
- package/src/codegen/extract/messages.ts +1 -1
- package/src/codegen/extract/projection.ts +1 -1
- package/src/codegen/extract/query.ts +1 -1
- package/src/codegen/extract/states.ts +1 -1
- package/src/codegen/extract/type-helpers.ts +102 -0
- package/src/codegen/scaffoldFromSchema.ts +283 -25
- package/src/codegen/templates/command/commands.specs.ts +3 -3
- package/src/codegen/templates/command/commands.ts.ejs +14 -9
- package/src/codegen/templates/command/decide.specs.specs.ts +54 -54
- package/src/codegen/templates/command/decide.specs.ts +13 -9
- package/src/codegen/templates/command/decide.ts.ejs +1 -0
- package/src/codegen/templates/command/events.specs.ts +3 -3
- package/src/codegen/templates/command/events.ts.ejs +16 -13
- package/src/codegen/templates/command/evolve.specs.ts +3 -3
- package/src/codegen/templates/command/handle.specs.ts +10 -5
- package/src/codegen/templates/command/mutation.resolver.specs.ts +8 -7
- package/src/codegen/templates/command/mutation.resolver.ts.ejs +2 -23
- package/src/codegen/templates/command/register.specs.ts +4 -4
- package/src/codegen/templates/command/register.ts.ejs +1 -4
- package/src/codegen/templates/command/state.specs.ts +54 -50
- package/src/codegen/templates/command/state.ts.ejs +8 -4
- package/src/codegen/templates/query/projection.specs.specs.ts +7 -7
- package/src/codegen/templates/query/projection.specs.ts +24 -7
- package/src/codegen/templates/query/projection.ts.ejs +64 -12
- package/src/codegen/templates/query/query.resolver.specs.ts +19 -16
- package/src/codegen/templates/query/query.resolver.ts.ejs +11 -49
- package/src/codegen/templates/query/state.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.specs.ts +3 -3
- package/src/codegen/templates/react/react.specs.ts +3 -3
- package/src/codegen/templates/react/react.ts.ejs +0 -1
- package/src/codegen/templates/react/register.specs.ts +3 -3
- package/src/codegen/templates/react/register.ts.ejs +0 -1
- package/src/codegen/test-data/specVariant1.ts +2 -2
- package/src/codegen/types.ts +1 -1
- package/src/commands/generate-server.ts +63 -34
- 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/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/
|
|
35
|
-
"@auto-engineer/message-bus": "0.11.
|
|
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.
|
|
46
|
+
"@auto-engineer/cli": "0.11.11"
|
|
47
47
|
},
|
|
48
|
-
"version": "0.11.
|
|
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, Slice, type Example } from '@auto-engineer/
|
|
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 { Slice, CommandExample, EventExample, StateExample, Example } from '@auto-engineer/
|
|
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[] })[]> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractCommandsFromGwt, extractCommandsFromThen } from './commands';
|
|
2
|
-
import { CommandExample, ErrorExample, EventExample, Slice, StateExample } from '@auto-engineer/
|
|
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';
|
|
@@ -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 {
|
|
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(
|
|
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...');
|
|
@@ -241,7 +484,7 @@ function extractUsedErrors(gwtMapping: Record<string, (GwtCondition & { failingF
|
|
|
241
484
|
|
|
242
485
|
async function prepareTemplateData(
|
|
243
486
|
slice: Slice,
|
|
244
|
-
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
649
|
+
flow: Narrative,
|
|
407
650
|
sliceDir: string,
|
|
408
651
|
messages: MessageDefinition[],
|
|
409
|
-
flows:
|
|
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:
|
|
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(
|
|
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:
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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(`
|