@constructive-io/graphql-codegen 4.1.3 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/codegen/cli/custom-command-generator.js +58 -19
- package/core/codegen/cli/executor-generator.d.ts +6 -2
- package/core/codegen/cli/executor-generator.js +48 -12
- package/core/codegen/cli/index.d.ts +7 -1
- package/core/codegen/cli/index.js +20 -3
- package/core/codegen/cli/table-command-generator.d.ts +3 -1
- package/core/codegen/cli/table-command-generator.js +50 -5
- package/core/codegen/cli/utils-generator.d.ts +8 -0
- package/core/codegen/cli/utils-generator.js +14 -0
- package/core/codegen/orm/client-generator.d.ts +3 -1
- package/core/codegen/orm/client-generator.js +7 -1
- package/core/codegen/orm/index.js +1 -1
- package/core/codegen/templates/cli-utils.ts +59 -2
- package/core/codegen/templates/node-fetch.ts +162 -0
- package/core/generate.js +20 -2
- package/esm/core/codegen/cli/custom-command-generator.js +58 -19
- package/esm/core/codegen/cli/executor-generator.d.ts +6 -2
- package/esm/core/codegen/cli/executor-generator.js +48 -12
- package/esm/core/codegen/cli/index.d.ts +7 -1
- package/esm/core/codegen/cli/index.js +21 -4
- package/esm/core/codegen/cli/table-command-generator.d.ts +3 -1
- package/esm/core/codegen/cli/table-command-generator.js +50 -5
- package/esm/core/codegen/cli/utils-generator.d.ts +8 -0
- package/esm/core/codegen/cli/utils-generator.js +13 -0
- package/esm/core/codegen/orm/client-generator.d.ts +3 -1
- package/esm/core/codegen/orm/client-generator.js +7 -1
- package/esm/core/codegen/orm/index.js +1 -1
- package/esm/core/generate.js +20 -2
- package/esm/types/config.d.ts +18 -0
- package/package.json +10 -10
- package/types/config.d.ts +18 -0
|
@@ -79,30 +79,46 @@ function unwrapType(ref) {
|
|
|
79
79
|
return ref;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
* If the return type has known fields, generates { field1: true, field2: true, ... }.
|
|
84
|
-
* Falls back to { clientMutationId: true } for mutations without known fields.
|
|
82
|
+
* Check if the return type (after unwrapping) is an OBJECT type.
|
|
85
83
|
*/
|
|
86
|
-
function
|
|
84
|
+
function hasObjectReturnType(returnType) {
|
|
85
|
+
const base = unwrapType(returnType);
|
|
86
|
+
return base.kind === 'OBJECT';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a default select string from the return type's top-level scalar fields.
|
|
90
|
+
* For OBJECT return types with known fields, generates a comma-separated list
|
|
91
|
+
* of all top-level field names (e.g. 'clientMutationId,result').
|
|
92
|
+
* Falls back to 'clientMutationId' for mutations without known fields.
|
|
93
|
+
*/
|
|
94
|
+
function buildDefaultSelectString(returnType, isMutation) {
|
|
87
95
|
const base = unwrapType(returnType);
|
|
88
96
|
if (base.fields && base.fields.length > 0) {
|
|
89
|
-
return
|
|
97
|
+
return base.fields.map((f) => f.name).join(',');
|
|
90
98
|
}
|
|
91
|
-
// Fallback: all PostGraphile mutation payloads have clientMutationId
|
|
92
99
|
if (isMutation) {
|
|
93
|
-
return
|
|
94
|
-
t.objectProperty(t.identifier('clientMutationId'), t.booleanLiteral(true)),
|
|
95
|
-
]);
|
|
100
|
+
return 'clientMutationId';
|
|
96
101
|
}
|
|
97
|
-
return
|
|
102
|
+
return '';
|
|
98
103
|
}
|
|
99
|
-
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr, hasArgs = true) {
|
|
105
|
+
const callArgs = [];
|
|
106
|
+
if (hasArgs) {
|
|
107
|
+
// Operation has arguments: pass args as first param, select as second
|
|
108
|
+
callArgs.push(argsExpr);
|
|
109
|
+
if (selectExpr) {
|
|
110
|
+
callArgs.push(t.objectExpression([
|
|
111
|
+
t.objectProperty(t.identifier('select'), selectExpr),
|
|
112
|
+
]));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (selectExpr) {
|
|
116
|
+
// No arguments: pass { select } as the only param (ORM signature)
|
|
117
|
+
callArgs.push(t.objectExpression([
|
|
103
118
|
t.objectProperty(t.identifier('select'), selectExpr),
|
|
104
|
-
])
|
|
105
|
-
|
|
119
|
+
]));
|
|
120
|
+
}
|
|
121
|
+
return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), callArgs), t.identifier('execute')), []);
|
|
106
122
|
}
|
|
107
123
|
function generateCustomCommand(op, options) {
|
|
108
124
|
const commandName = (0, komoji_1.toKebabCase)(op.name);
|
|
@@ -118,13 +134,23 @@ function generateCustomCommand(op, options) {
|
|
|
118
134
|
const base = unwrapType(arg.type);
|
|
119
135
|
return base.kind === 'INPUT_OBJECT';
|
|
120
136
|
});
|
|
137
|
+
// Check if return type is OBJECT (needs --select flag)
|
|
138
|
+
const isObjectReturn = hasObjectReturnType(op.returnType);
|
|
121
139
|
const utilsPath = options?.executorImportPath
|
|
122
140
|
? options.executorImportPath.replace(/\/executor$/, '/utils')
|
|
123
141
|
: '../utils';
|
|
124
142
|
statements.push(createImportDeclaration('inquirerer', ['CLIOptions', 'Inquirerer']));
|
|
125
143
|
statements.push(createImportDeclaration(executorPath, imports));
|
|
144
|
+
// Build the list of utils imports needed
|
|
145
|
+
const utilsImports = [];
|
|
126
146
|
if (hasInputObjectArg) {
|
|
127
|
-
|
|
147
|
+
utilsImports.push('parseMutationInput');
|
|
148
|
+
}
|
|
149
|
+
if (isObjectReturn) {
|
|
150
|
+
utilsImports.push('buildSelectFromPaths');
|
|
151
|
+
}
|
|
152
|
+
if (utilsImports.length > 0) {
|
|
153
|
+
statements.push(createImportDeclaration(utilsPath, utilsImports));
|
|
128
154
|
}
|
|
129
155
|
const questionsArray = op.args.length > 0
|
|
130
156
|
? (0, arg_mapper_1.buildQuestionsArray)(op.args)
|
|
@@ -168,9 +194,22 @@ function generateCustomCommand(op, options) {
|
|
|
168
194
|
? t.identifier('parsedAnswers')
|
|
169
195
|
: t.identifier('answers'))
|
|
170
196
|
: t.objectExpression([]);
|
|
171
|
-
|
|
197
|
+
// For OBJECT return types, generate runtime select from --select flag
|
|
198
|
+
// For scalar return types, no select is needed
|
|
199
|
+
let selectExpr;
|
|
200
|
+
if (isObjectReturn) {
|
|
201
|
+
const defaultSelect = buildDefaultSelectString(op.returnType, op.kind === 'mutation');
|
|
202
|
+
// Generate: const selectFields = buildSelectFromPaths(argv.select ?? 'defaultFields')
|
|
203
|
+
bodyStatements.push(t.variableDeclaration('const', [
|
|
204
|
+
t.variableDeclarator(t.identifier('selectFields'), t.callExpression(t.identifier('buildSelectFromPaths'), [
|
|
205
|
+
t.logicalExpression('??', t.memberExpression(t.identifier('argv'), t.identifier('select')), t.stringLiteral(defaultSelect)),
|
|
206
|
+
])),
|
|
207
|
+
]));
|
|
208
|
+
selectExpr = t.identifier('selectFields');
|
|
209
|
+
}
|
|
210
|
+
const hasArgs = op.args.length > 0;
|
|
172
211
|
bodyStatements.push(t.variableDeclaration('const', [
|
|
173
|
-
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr))),
|
|
212
|
+
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs))),
|
|
174
213
|
]));
|
|
175
214
|
if (options?.saveToken) {
|
|
176
215
|
bodyStatements.push(t.ifStatement(t.logicalExpression('&&', t.memberExpression(t.identifier('argv'), t.identifier('saveToken')), t.identifier('result')), t.blockStatement([
|
|
@@ -7,5 +7,9 @@ export interface MultiTargetExecutorInput {
|
|
|
7
7
|
endpoint: string;
|
|
8
8
|
ormImportPath: string;
|
|
9
9
|
}
|
|
10
|
-
export
|
|
11
|
-
|
|
10
|
+
export interface ExecutorOptions {
|
|
11
|
+
/** Enable NodeHttpAdapter for *.localhost subdomain routing */
|
|
12
|
+
nodeHttpAdapter?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile;
|
|
15
|
+
export declare function generateMultiTargetExecutorFile(toolName: string, targets: MultiTargetExecutorInput[], options?: ExecutorOptions): GeneratedFile;
|
|
@@ -44,8 +44,12 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
|
|
|
44
44
|
decl.importKind = typeOnly ? 'type' : 'value';
|
|
45
45
|
return decl;
|
|
46
46
|
}
|
|
47
|
-
function generateExecutorFile(toolName) {
|
|
47
|
+
function generateExecutorFile(toolName, options) {
|
|
48
48
|
const statements = [];
|
|
49
|
+
// Import NodeHttpAdapter for *.localhost subdomain routing
|
|
50
|
+
if (options?.nodeHttpAdapter) {
|
|
51
|
+
statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
|
|
52
|
+
}
|
|
49
53
|
statements.push(createImportDeclaration('appstash', ['createConfigStore']));
|
|
50
54
|
statements.push(createImportDeclaration('../orm', ['createClient']));
|
|
51
55
|
statements.push(t.variableDeclaration('const', [
|
|
@@ -98,12 +102,26 @@ function generateExecutorFile(toolName) {
|
|
|
98
102
|
]))),
|
|
99
103
|
])),
|
|
100
104
|
])),
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
t.
|
|
105
|
+
// Build createClient config — use NodeHttpAdapter for *.localhost endpoints
|
|
106
|
+
...(options?.nodeHttpAdapter
|
|
107
|
+
? [
|
|
108
|
+
t.returnStatement(t.callExpression(t.identifier('createClient'), [
|
|
109
|
+
t.objectExpression([
|
|
110
|
+
t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
|
|
111
|
+
t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')),
|
|
112
|
+
t.identifier('headers'),
|
|
113
|
+
])),
|
|
114
|
+
]),
|
|
115
|
+
])),
|
|
116
|
+
]
|
|
117
|
+
: [
|
|
118
|
+
t.returnStatement(t.callExpression(t.identifier('createClient'), [
|
|
119
|
+
t.objectExpression([
|
|
120
|
+
t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('ctx'), t.identifier('endpoint'))),
|
|
121
|
+
t.objectProperty(t.identifier('headers'), t.identifier('headers')),
|
|
122
|
+
]),
|
|
123
|
+
])),
|
|
105
124
|
]),
|
|
106
|
-
])),
|
|
107
125
|
]);
|
|
108
126
|
const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [contextNameParam], getClientBody);
|
|
109
127
|
statements.push(t.exportNamedDeclaration(getClientFunc));
|
|
@@ -114,8 +132,12 @@ function generateExecutorFile(toolName) {
|
|
|
114
132
|
content: header + '\n' + code,
|
|
115
133
|
};
|
|
116
134
|
}
|
|
117
|
-
function generateMultiTargetExecutorFile(toolName, targets) {
|
|
135
|
+
function generateMultiTargetExecutorFile(toolName, targets, options) {
|
|
118
136
|
const statements = [];
|
|
137
|
+
// Import NodeHttpAdapter for *.localhost subdomain routing
|
|
138
|
+
if (options?.nodeHttpAdapter) {
|
|
139
|
+
statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
|
|
140
|
+
}
|
|
119
141
|
statements.push(createImportDeclaration('appstash', ['createConfigStore']));
|
|
120
142
|
for (const target of targets) {
|
|
121
143
|
const aliasName = `create${target.name[0].toUpperCase()}${target.name.slice(1)}Client`;
|
|
@@ -199,12 +221,26 @@ function generateMultiTargetExecutorFile(toolName, targets) {
|
|
|
199
221
|
]), t.blockStatement([
|
|
200
222
|
t.expressionStatement(t.assignmentExpression('=', t.identifier('endpoint'), t.logicalExpression('||', t.memberExpression(t.identifier('defaultEndpoints'), t.identifier('targetName'), true), t.stringLiteral('')))),
|
|
201
223
|
])),
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
t.
|
|
224
|
+
// Build createClient config — use NodeHttpAdapter for *.localhost endpoints
|
|
225
|
+
...(options?.nodeHttpAdapter
|
|
226
|
+
? [
|
|
227
|
+
t.returnStatement(t.callExpression(t.identifier('createFn'), [
|
|
228
|
+
t.objectExpression([
|
|
229
|
+
t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
|
|
230
|
+
t.identifier('endpoint'),
|
|
231
|
+
t.identifier('headers'),
|
|
232
|
+
])),
|
|
233
|
+
]),
|
|
234
|
+
])),
|
|
235
|
+
]
|
|
236
|
+
: [
|
|
237
|
+
t.returnStatement(t.callExpression(t.identifier('createFn'), [
|
|
238
|
+
t.objectExpression([
|
|
239
|
+
t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')),
|
|
240
|
+
t.objectProperty(t.identifier('headers'), t.identifier('headers')),
|
|
241
|
+
]),
|
|
242
|
+
])),
|
|
206
243
|
]),
|
|
207
|
-
])),
|
|
208
244
|
]);
|
|
209
245
|
const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [targetNameParam, contextNameParam], getClientBody);
|
|
210
246
|
statements.push(t.exportNamedDeclaration(getClientFunc));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BuiltinNames, GraphQLSDKConfigTarget } from '../../../types/config';
|
|
2
|
-
import type { CleanOperation, CleanTable } from '../../../types/schema';
|
|
2
|
+
import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema';
|
|
3
3
|
import type { GeneratedFile } from './executor-generator';
|
|
4
4
|
export interface GenerateCliOptions {
|
|
5
5
|
tables: CleanTable[];
|
|
@@ -8,6 +8,8 @@ export interface GenerateCliOptions {
|
|
|
8
8
|
mutations: CleanOperation[];
|
|
9
9
|
};
|
|
10
10
|
config: GraphQLSDKConfigTarget;
|
|
11
|
+
/** TypeRegistry from introspection, used to check field defaults */
|
|
12
|
+
typeRegistry?: TypeRegistry;
|
|
11
13
|
}
|
|
12
14
|
export interface GenerateCliResult {
|
|
13
15
|
files: GeneratedFile[];
|
|
@@ -30,11 +32,15 @@ export interface MultiTargetCliTarget {
|
|
|
30
32
|
mutations: CleanOperation[];
|
|
31
33
|
};
|
|
32
34
|
isAuthTarget?: boolean;
|
|
35
|
+
/** TypeRegistry from introspection, used to check field defaults */
|
|
36
|
+
typeRegistry?: TypeRegistry;
|
|
33
37
|
}
|
|
34
38
|
export interface GenerateMultiTargetCliOptions {
|
|
35
39
|
toolName: string;
|
|
36
40
|
builtinNames?: BuiltinNames;
|
|
37
41
|
targets: MultiTargetCliTarget[];
|
|
42
|
+
/** Enable NodeHttpAdapter for *.localhost subdomain routing */
|
|
43
|
+
nodeHttpAdapter?: boolean;
|
|
38
44
|
}
|
|
39
45
|
export declare function resolveBuiltinNames(targetNames: string[], userOverrides?: BuiltinNames): {
|
|
40
46
|
auth: string;
|
|
@@ -17,16 +17,26 @@ function generateCli(options) {
|
|
|
17
17
|
const toolName = typeof cliConfig === 'object' && cliConfig.toolName
|
|
18
18
|
? cliConfig.toolName
|
|
19
19
|
: 'app';
|
|
20
|
-
|
|
20
|
+
// Use top-level nodeHttpAdapter from config (auto-enabled for CLI by generate.ts)
|
|
21
|
+
const useNodeHttpAdapter = !!config.nodeHttpAdapter;
|
|
22
|
+
const executorFile = (0, executor_generator_1.generateExecutorFile)(toolName, {
|
|
23
|
+
nodeHttpAdapter: useNodeHttpAdapter,
|
|
24
|
+
});
|
|
21
25
|
files.push(executorFile);
|
|
22
26
|
const utilsFile = (0, utils_generator_1.generateUtilsFile)();
|
|
23
27
|
files.push(utilsFile);
|
|
28
|
+
// Generate node HTTP adapter if configured (for *.localhost subdomain routing)
|
|
29
|
+
if (useNodeHttpAdapter) {
|
|
30
|
+
files.push((0, utils_generator_1.generateNodeFetchFile)());
|
|
31
|
+
}
|
|
24
32
|
const contextFile = (0, infra_generator_1.generateContextCommand)(toolName);
|
|
25
33
|
files.push(contextFile);
|
|
26
34
|
const authFile = (0, infra_generator_1.generateAuthCommand)(toolName);
|
|
27
35
|
files.push(authFile);
|
|
28
36
|
for (const table of tables) {
|
|
29
|
-
const tableFile = (0, table_command_generator_1.generateTableCommand)(table
|
|
37
|
+
const tableFile = (0, table_command_generator_1.generateTableCommand)(table, {
|
|
38
|
+
typeRegistry: options.typeRegistry,
|
|
39
|
+
});
|
|
30
40
|
files.push(tableFile);
|
|
31
41
|
}
|
|
32
42
|
const allCustomOps = [
|
|
@@ -71,10 +81,16 @@ function generateMultiTargetCli(options) {
|
|
|
71
81
|
endpoint: t.endpoint,
|
|
72
82
|
ormImportPath: t.ormImportPath,
|
|
73
83
|
}));
|
|
74
|
-
const executorFile = (0, executor_generator_1.generateMultiTargetExecutorFile)(toolName, executorInputs
|
|
84
|
+
const executorFile = (0, executor_generator_1.generateMultiTargetExecutorFile)(toolName, executorInputs, {
|
|
85
|
+
nodeHttpAdapter: !!options.nodeHttpAdapter,
|
|
86
|
+
});
|
|
75
87
|
files.push(executorFile);
|
|
76
88
|
const utilsFile = (0, utils_generator_1.generateUtilsFile)();
|
|
77
89
|
files.push(utilsFile);
|
|
90
|
+
// Generate node HTTP adapter if configured (for *.localhost subdomain routing)
|
|
91
|
+
if (options.nodeHttpAdapter) {
|
|
92
|
+
files.push((0, utils_generator_1.generateNodeFetchFile)());
|
|
93
|
+
}
|
|
78
94
|
const contextFile = (0, infra_generator_1.generateMultiTargetContextCommand)(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
|
|
79
95
|
files.push(contextFile);
|
|
80
96
|
const authFile = (0, infra_generator_1.generateAuthCommandWithName)(toolName, builtinNames.auth);
|
|
@@ -92,6 +108,7 @@ function generateMultiTargetCli(options) {
|
|
|
92
108
|
const tableFile = (0, table_command_generator_1.generateTableCommand)(table, {
|
|
93
109
|
targetName: target.name,
|
|
94
110
|
executorImportPath: '../../executor',
|
|
111
|
+
typeRegistry: target.typeRegistry,
|
|
95
112
|
});
|
|
96
113
|
files.push(tableFile);
|
|
97
114
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { CleanTable } from '../../../types/schema';
|
|
1
|
+
import type { CleanTable, TypeRegistry } from '../../../types/schema';
|
|
2
2
|
import type { GeneratedFile } from './executor-generator';
|
|
3
3
|
export interface TableCommandOptions {
|
|
4
4
|
targetName?: string;
|
|
5
5
|
executorImportPath?: string;
|
|
6
|
+
/** TypeRegistry from introspection, used to check field defaults */
|
|
7
|
+
typeRegistry?: TypeRegistry;
|
|
6
8
|
}
|
|
7
9
|
export declare function generateTableCommand(table: CleanTable, options?: TableCommandOptions): GeneratedFile;
|
|
@@ -38,6 +38,7 @@ const t = __importStar(require("@babel/types"));
|
|
|
38
38
|
const komoji_1 = require("komoji");
|
|
39
39
|
const babel_ast_1 = require("../babel-ast");
|
|
40
40
|
const utils_1 = require("../utils");
|
|
41
|
+
const utils_2 = require("../utils");
|
|
41
42
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
42
43
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
43
44
|
const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
|
|
@@ -183,7 +184,46 @@ function buildGetHandler(table, targetName) {
|
|
|
183
184
|
t.tryStatement(t.blockStatement(tryBody), buildErrorCatch('Record not found.')),
|
|
184
185
|
]), false, true);
|
|
185
186
|
}
|
|
186
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Get the set of field names that have defaults in the create input type.
|
|
189
|
+
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
|
|
190
|
+
* TypeRegistry and checks each field's defaultValue from introspection.
|
|
191
|
+
*/
|
|
192
|
+
function getFieldsWithDefaults(table, typeRegistry) {
|
|
193
|
+
const fieldsWithDefaults = new Set();
|
|
194
|
+
if (!typeRegistry)
|
|
195
|
+
return fieldsWithDefaults;
|
|
196
|
+
// Look up the CreateXInput type (e.g. CreateDatabaseInput)
|
|
197
|
+
const createInputTypeName = (0, utils_2.getCreateInputTypeName)(table);
|
|
198
|
+
const createInputType = typeRegistry.get(createInputTypeName);
|
|
199
|
+
if (!createInputType?.inputFields)
|
|
200
|
+
return fieldsWithDefaults;
|
|
201
|
+
// The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
|
|
202
|
+
// Find the inner input type that contains the actual field definitions
|
|
203
|
+
for (const inputField of createInputType.inputFields) {
|
|
204
|
+
// The inner field's type name is the actual input type (e.g. DatabaseInput)
|
|
205
|
+
const innerTypeName = inputField.type.name
|
|
206
|
+
|| inputField.type.ofType?.name
|
|
207
|
+
|| inputField.type.ofType?.ofType?.name;
|
|
208
|
+
if (!innerTypeName)
|
|
209
|
+
continue;
|
|
210
|
+
const innerType = typeRegistry.get(innerTypeName);
|
|
211
|
+
if (!innerType?.inputFields)
|
|
212
|
+
continue;
|
|
213
|
+
// Check each field in the inner input type for defaultValue
|
|
214
|
+
for (const field of innerType.inputFields) {
|
|
215
|
+
if (field.defaultValue !== undefined) {
|
|
216
|
+
fieldsWithDefaults.add(field.name);
|
|
217
|
+
}
|
|
218
|
+
// Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
|
|
219
|
+
if (field.type.kind !== 'NON_NULL') {
|
|
220
|
+
fieldsWithDefaults.add(field.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return fieldsWithDefaults;
|
|
225
|
+
}
|
|
226
|
+
function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
187
227
|
const { singularName } = (0, utils_1.getTableNames)(table);
|
|
188
228
|
const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
|
|
189
229
|
const pk = pkFields[0];
|
|
@@ -191,6 +231,8 @@ function buildMutationHandler(table, operation, targetName) {
|
|
|
191
231
|
f.name !== 'nodeId' &&
|
|
192
232
|
f.name !== 'createdAt' &&
|
|
193
233
|
f.name !== 'updatedAt');
|
|
234
|
+
// Get fields that have defaults from introspection (for create operations)
|
|
235
|
+
const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
|
|
194
236
|
const questions = [];
|
|
195
237
|
if (operation === 'update' || operation === 'delete') {
|
|
196
238
|
questions.push(t.objectExpression([
|
|
@@ -202,11 +244,14 @@ function buildMutationHandler(table, operation, targetName) {
|
|
|
202
244
|
}
|
|
203
245
|
if (operation !== 'delete') {
|
|
204
246
|
for (const field of editableFields) {
|
|
247
|
+
// For create: field is required only if it has no default value
|
|
248
|
+
// For update: all fields are optional (user only updates what they want)
|
|
249
|
+
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
|
|
205
250
|
questions.push(t.objectExpression([
|
|
206
251
|
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
|
|
207
252
|
t.objectProperty(t.identifier('name'), t.stringLiteral(field.name)),
|
|
208
253
|
t.objectProperty(t.identifier('message'), t.stringLiteral(field.name)),
|
|
209
|
-
t.objectProperty(t.identifier('required'), t.booleanLiteral(
|
|
254
|
+
t.objectProperty(t.identifier('required'), t.booleanLiteral(isRequired)),
|
|
210
255
|
]));
|
|
211
256
|
}
|
|
212
257
|
}
|
|
@@ -365,9 +410,9 @@ function generateTableCommand(table, options) {
|
|
|
365
410
|
const tn = options?.targetName;
|
|
366
411
|
statements.push(buildListHandler(table, tn));
|
|
367
412
|
statements.push(buildGetHandler(table, tn));
|
|
368
|
-
statements.push(buildMutationHandler(table, 'create', tn));
|
|
369
|
-
statements.push(buildMutationHandler(table, 'update', tn));
|
|
370
|
-
statements.push(buildMutationHandler(table, 'delete', tn));
|
|
413
|
+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
|
|
414
|
+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
|
|
415
|
+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
|
|
371
416
|
const header = (0, utils_1.getGeneratedFileHeader)(`CLI commands for ${table.name}`);
|
|
372
417
|
const code = (0, babel_ast_1.generateCode)(statements);
|
|
373
418
|
return {
|
|
@@ -8,3 +8,11 @@ import type { GeneratedFile } from './executor-generator';
|
|
|
8
8
|
* and mutation input parsing.
|
|
9
9
|
*/
|
|
10
10
|
export declare function generateUtilsFile(): GeneratedFile;
|
|
11
|
+
/**
|
|
12
|
+
* Generate a node-fetch.ts file with NodeHttpAdapter for CLI.
|
|
13
|
+
*
|
|
14
|
+
* Provides a GraphQLAdapter implementation using node:http/node:https
|
|
15
|
+
* instead of the Fetch API. This cleanly handles *.localhost subdomain
|
|
16
|
+
* routing (DNS resolution + Host header) without any global patching.
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateNodeFetchFile(): GeneratedFile;
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.generateUtilsFile = generateUtilsFile;
|
|
37
|
+
exports.generateNodeFetchFile = generateNodeFetchFile;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const utils_1 = require("../utils");
|
|
@@ -76,3 +77,16 @@ function generateUtilsFile() {
|
|
|
76
77
|
content: readTemplateFile('cli-utils.ts', 'CLI utility functions for type coercion and input handling'),
|
|
77
78
|
};
|
|
78
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Generate a node-fetch.ts file with NodeHttpAdapter for CLI.
|
|
82
|
+
*
|
|
83
|
+
* Provides a GraphQLAdapter implementation using node:http/node:https
|
|
84
|
+
* instead of the Fetch API. This cleanly handles *.localhost subdomain
|
|
85
|
+
* routing (DNS resolution + Host header) without any global patching.
|
|
86
|
+
*/
|
|
87
|
+
function generateNodeFetchFile() {
|
|
88
|
+
return {
|
|
89
|
+
fileName: 'node-fetch.ts',
|
|
90
|
+
content: readTemplateFile('node-fetch.ts', 'Node HTTP adapter for localhost subdomain routing'),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -25,4 +25,6 @@ export declare function generateSelectTypesFile(): GeneratedClientFile;
|
|
|
25
25
|
/**
|
|
26
26
|
* Generate the main index.ts with createClient factory
|
|
27
27
|
*/
|
|
28
|
-
export declare function generateCreateClientFile(tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean
|
|
28
|
+
export declare function generateCreateClientFile(tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean, options?: {
|
|
29
|
+
nodeHttpAdapter?: boolean;
|
|
30
|
+
}): GeneratedClientFile;
|
|
@@ -114,7 +114,7 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
|
|
|
114
114
|
/**
|
|
115
115
|
* Generate the main index.ts with createClient factory
|
|
116
116
|
*/
|
|
117
|
-
function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations) {
|
|
117
|
+
function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations, options) {
|
|
118
118
|
const statements = [];
|
|
119
119
|
// Add imports
|
|
120
120
|
// Import OrmClient (value) and OrmClientConfig (type) separately
|
|
@@ -156,6 +156,12 @@ function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations)
|
|
|
156
156
|
statements.push(t.exportAllDeclaration(t.stringLiteral('./select-types')));
|
|
157
157
|
// Re-export all models
|
|
158
158
|
statements.push(t.exportAllDeclaration(t.stringLiteral('./models')));
|
|
159
|
+
// Re-export NodeHttpAdapter when enabled (for use in any Node.js application)
|
|
160
|
+
if (options?.nodeHttpAdapter) {
|
|
161
|
+
statements.push(t.exportNamedDeclaration(null, [
|
|
162
|
+
t.exportSpecifier(t.identifier('NodeHttpAdapter'), t.identifier('NodeHttpAdapter')),
|
|
163
|
+
], t.stringLiteral('./node-fetch')));
|
|
164
|
+
}
|
|
159
165
|
// Re-export custom operations
|
|
160
166
|
if (hasCustomQueries) {
|
|
161
167
|
statements.push(t.exportNamedDeclaration(null, [
|
|
@@ -91,7 +91,7 @@ function generateOrm(options) {
|
|
|
91
91
|
const typesBarrel = (0, barrel_1.generateTypesBarrel)(useSharedTypes);
|
|
92
92
|
files.push({ path: typesBarrel.fileName, content: typesBarrel.content });
|
|
93
93
|
// 7. Generate main index.ts with createClient
|
|
94
|
-
const indexFile = (0, client_generator_1.generateCreateClientFile)(tables, hasCustomQueries, hasCustomMutations);
|
|
94
|
+
const indexFile = (0, client_generator_1.generateCreateClientFile)(tables, hasCustomQueries, hasCustomMutations, { nodeHttpAdapter: !!options.config.nodeHttpAdapter });
|
|
95
95
|
files.push({ path: indexFile.fileName, content: indexFile.content });
|
|
96
96
|
return {
|
|
97
97
|
files,
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the RUNTIME code that gets copied to generated output.
|
|
5
5
|
* Provides helpers for CLI commands: type coercion (string CLI args -> proper
|
|
6
|
-
* GraphQL types), field filtering (strip extra minimist fields),
|
|
7
|
-
* mutation input parsing.
|
|
6
|
+
* GraphQL types), field filtering (strip extra minimist fields),
|
|
7
|
+
* mutation input parsing, and select field parsing.
|
|
8
8
|
*
|
|
9
9
|
* NOTE: This file is read at codegen time and written to output.
|
|
10
10
|
* Any changes here will affect all generated CLI utils.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import objectPath from 'nested-obj';
|
|
14
|
+
|
|
13
15
|
export type FieldType =
|
|
14
16
|
| 'string'
|
|
15
17
|
| 'boolean'
|
|
@@ -142,3 +144,58 @@ export function parseMutationInput(
|
|
|
142
144
|
}
|
|
143
145
|
return answers;
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build a select object from a comma-separated list of dot-notation paths.
|
|
150
|
+
* Uses `nested-obj` to parse paths like 'clientMutationId,result.accessToken,result.userId'
|
|
151
|
+
* into the nested structure expected by the ORM:
|
|
152
|
+
*
|
|
153
|
+
* { clientMutationId: true, result: { select: { accessToken: true, userId: true } } }
|
|
154
|
+
*
|
|
155
|
+
* Paths without dots set the key to `true` (scalar select).
|
|
156
|
+
* Paths with dots create nested `{ select: { ... } }` wrappers, matching the
|
|
157
|
+
* ORM's expected structure for OBJECT sub-fields (e.g. `SignUpPayloadSelect.result`).
|
|
158
|
+
*
|
|
159
|
+
* @param paths - Comma-separated dot-notation field paths (e.g. 'clientMutationId,result.accessToken')
|
|
160
|
+
* @returns The nested select object for the ORM
|
|
161
|
+
*/
|
|
162
|
+
export function buildSelectFromPaths(
|
|
163
|
+
paths: string,
|
|
164
|
+
): Record<string, unknown> {
|
|
165
|
+
const result: Record<string, unknown> = {};
|
|
166
|
+
const trimmedPaths = paths
|
|
167
|
+
.split(',')
|
|
168
|
+
.map((p) => p.trim())
|
|
169
|
+
.filter((p) => p.length > 0);
|
|
170
|
+
|
|
171
|
+
for (const path of trimmedPaths) {
|
|
172
|
+
if (!path.includes('.')) {
|
|
173
|
+
// Simple scalar field: clientMutationId -> { clientMutationId: true }
|
|
174
|
+
result[path] = true;
|
|
175
|
+
} else {
|
|
176
|
+
// Nested path: result.accessToken -> { result: { select: { accessToken: true } } }
|
|
177
|
+
// Convert dot-notation to ORM's { select: { ... } } nesting pattern
|
|
178
|
+
const parts = path.split('.');
|
|
179
|
+
let current = result;
|
|
180
|
+
for (let i = 0; i < parts.length; i++) {
|
|
181
|
+
const part = parts[i];
|
|
182
|
+
if (i === parts.length - 1) {
|
|
183
|
+
// Leaf node: set to true
|
|
184
|
+
objectPath.set(current, part, true);
|
|
185
|
+
} else {
|
|
186
|
+
// Intermediate node: ensure { select: { ... } } wrapper exists
|
|
187
|
+
if (!current[part] || typeof current[part] !== 'object') {
|
|
188
|
+
current[part] = { select: {} };
|
|
189
|
+
}
|
|
190
|
+
const wrapper = current[part] as Record<string, unknown>;
|
|
191
|
+
if (!wrapper.select || typeof wrapper.select !== 'object') {
|
|
192
|
+
wrapper.select = {};
|
|
193
|
+
}
|
|
194
|
+
current = wrapper.select as Record<string, unknown>;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|