@constructive-io/graphql-codegen 4.2.0 → 4.4.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 +15 -4
- 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 +10 -2
- package/core/codegen/cli/index.js +31 -4
- 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 +17 -0
- package/core/codegen/cli/utils-generator.js +29 -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-entry.ts +33 -0
- package/core/codegen/templates/node-fetch.ts +162 -0
- package/core/generate.js +21 -2
- package/esm/core/codegen/cli/custom-command-generator.js +15 -4
- 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 +10 -2
- package/esm/core/codegen/cli/index.js +31 -5
- 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 +17 -0
- package/esm/core/codegen/cli/utils-generator.js +27 -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 +21 -2
- package/esm/types/config.d.ts +26 -0
- package/package.json +10 -10
- package/types/config.d.ts +26 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node HTTP Adapter for Node.js applications
|
|
3
|
+
*
|
|
4
|
+
* Implements the GraphQLAdapter interface using node:http / node:https
|
|
5
|
+
* instead of the Fetch API. This solves two Node.js limitations:
|
|
6
|
+
*
|
|
7
|
+
* 1. DNS: Node.js cannot resolve *.localhost subdomains (ENOTFOUND).
|
|
8
|
+
* Browsers handle this automatically, but Node requires manual resolution.
|
|
9
|
+
*
|
|
10
|
+
* 2. Host header: The Fetch API treats "Host" as a forbidden request header
|
|
11
|
+
* and silently drops it. The Constructive GraphQL server uses Host-header
|
|
12
|
+
* subdomain routing (enableServicesApi), so this header must be preserved.
|
|
13
|
+
*
|
|
14
|
+
* By using node:http.request directly, both issues are bypassed cleanly
|
|
15
|
+
* without any global patching.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: This file is read at codegen time and written to output.
|
|
18
|
+
* Any changes here will affect all generated CLI node adapters.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import http from 'node:http';
|
|
22
|
+
import https from 'node:https';
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
GraphQLAdapter,
|
|
26
|
+
GraphQLError,
|
|
27
|
+
QueryResult,
|
|
28
|
+
} from '@constructive-io/graphql-types';
|
|
29
|
+
|
|
30
|
+
interface HttpResponse {
|
|
31
|
+
statusCode: number;
|
|
32
|
+
statusMessage: string;
|
|
33
|
+
data: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a hostname is a localhost subdomain that needs special handling.
|
|
38
|
+
* Returns true for *.localhost (e.g. auth.localhost) but not bare "localhost".
|
|
39
|
+
*/
|
|
40
|
+
function isLocalhostSubdomain(hostname: string): boolean {
|
|
41
|
+
return hostname.endsWith('.localhost') && hostname !== 'localhost';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Make an HTTP/HTTPS request using native Node modules.
|
|
46
|
+
*/
|
|
47
|
+
function makeRequest(
|
|
48
|
+
url: URL,
|
|
49
|
+
options: http.RequestOptions,
|
|
50
|
+
body: string,
|
|
51
|
+
): Promise<HttpResponse> {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const protocol = url.protocol === 'https:' ? https : http;
|
|
54
|
+
|
|
55
|
+
const req = protocol.request(url, options, (res) => {
|
|
56
|
+
let data = '';
|
|
57
|
+
res.setEncoding('utf8');
|
|
58
|
+
res.on('data', (chunk: string) => {
|
|
59
|
+
data += chunk;
|
|
60
|
+
});
|
|
61
|
+
res.on('end', () => {
|
|
62
|
+
resolve({
|
|
63
|
+
statusCode: res.statusCode || 0,
|
|
64
|
+
statusMessage: res.statusMessage || '',
|
|
65
|
+
data,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', reject);
|
|
71
|
+
req.write(body);
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* GraphQL adapter that uses node:http/node:https for requests.
|
|
78
|
+
*
|
|
79
|
+
* Handles *.localhost subdomains by rewriting the hostname to "localhost"
|
|
80
|
+
* and injecting the original Host header for server-side subdomain routing.
|
|
81
|
+
*/
|
|
82
|
+
export class NodeHttpAdapter implements GraphQLAdapter {
|
|
83
|
+
private headers: Record<string, string>;
|
|
84
|
+
private url: URL;
|
|
85
|
+
|
|
86
|
+
constructor(
|
|
87
|
+
private endpoint: string,
|
|
88
|
+
headers?: Record<string, string>,
|
|
89
|
+
) {
|
|
90
|
+
this.headers = headers ?? {};
|
|
91
|
+
this.url = new URL(endpoint);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async execute<T>(
|
|
95
|
+
document: string,
|
|
96
|
+
variables?: Record<string, unknown>,
|
|
97
|
+
): Promise<QueryResult<T>> {
|
|
98
|
+
const requestUrl = new URL(this.url.href);
|
|
99
|
+
const requestHeaders: Record<string, string> = {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
Accept: 'application/json',
|
|
102
|
+
...this.headers,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// For *.localhost subdomains, rewrite hostname and inject Host header
|
|
106
|
+
if (isLocalhostSubdomain(requestUrl.hostname)) {
|
|
107
|
+
requestHeaders['Host'] = requestUrl.host;
|
|
108
|
+
requestUrl.hostname = 'localhost';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const body = JSON.stringify({
|
|
112
|
+
query: document,
|
|
113
|
+
variables: variables ?? {},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const requestOptions: http.RequestOptions = {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: requestHeaders,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const response = await makeRequest(requestUrl, requestOptions, body);
|
|
122
|
+
|
|
123
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
data: null,
|
|
127
|
+
errors: [
|
|
128
|
+
{
|
|
129
|
+
message: `HTTP ${response.statusCode}: ${response.statusMessage}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const json = JSON.parse(response.data) as {
|
|
136
|
+
data?: T;
|
|
137
|
+
errors?: GraphQLError[];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (json.errors && json.errors.length > 0) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
data: null,
|
|
144
|
+
errors: json.errors,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
data: json.data as T,
|
|
151
|
+
errors: undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setHeaders(headers: Record<string, string>): void {
|
|
156
|
+
this.headers = { ...this.headers, ...headers };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getEndpoint(): string {
|
|
160
|
+
return this.endpoint;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/core/generate.js
CHANGED
|
@@ -76,6 +76,9 @@ async function generate(options = {}, internalOptions) {
|
|
|
76
76
|
const runReactQuery = config.reactQuery ?? false;
|
|
77
77
|
const runCli = internalOptions?.skipCli ? false : !!config.cli;
|
|
78
78
|
const runOrm = runReactQuery || !!config.cli || (options.orm !== undefined ? !!options.orm : false);
|
|
79
|
+
// Auto-enable nodeHttpAdapter when CLI is enabled, unless explicitly set to false
|
|
80
|
+
const useNodeHttpAdapter = options.nodeHttpAdapter === true ||
|
|
81
|
+
(runCli && options.nodeHttpAdapter !== false);
|
|
79
82
|
if (!options.schemaOnly && !runReactQuery && !runOrm && !runCli) {
|
|
80
83
|
return {
|
|
81
84
|
success: false,
|
|
@@ -209,13 +212,22 @@ async function generate(options = {}, internalOptions) {
|
|
|
209
212
|
mutations: customOperations.mutations,
|
|
210
213
|
typeRegistry: customOperations.typeRegistry,
|
|
211
214
|
},
|
|
212
|
-
config,
|
|
215
|
+
config: { ...config, nodeHttpAdapter: useNodeHttpAdapter },
|
|
213
216
|
sharedTypesPath: bothEnabled ? '..' : undefined,
|
|
214
217
|
});
|
|
215
218
|
filesToWrite.push(...files.map((file) => ({
|
|
216
219
|
...file,
|
|
217
220
|
path: node_path_1.default.posix.join('orm', file.path),
|
|
218
221
|
})));
|
|
222
|
+
// Generate NodeHttpAdapter in ORM output when enabled
|
|
223
|
+
if (useNodeHttpAdapter) {
|
|
224
|
+
const { generateNodeFetchFile } = await Promise.resolve().then(() => __importStar(require('./codegen/cli/utils-generator')));
|
|
225
|
+
const nodeFetchFile = generateNodeFetchFile();
|
|
226
|
+
filesToWrite.push({
|
|
227
|
+
path: node_path_1.default.posix.join('orm', nodeFetchFile.fileName),
|
|
228
|
+
content: nodeFetchFile.content,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
219
231
|
}
|
|
220
232
|
// Generate CLI commands
|
|
221
233
|
if (runCli) {
|
|
@@ -226,7 +238,7 @@ async function generate(options = {}, internalOptions) {
|
|
|
226
238
|
queries: customOperations.queries,
|
|
227
239
|
mutations: customOperations.mutations,
|
|
228
240
|
},
|
|
229
|
-
config,
|
|
241
|
+
config: { ...config, nodeHttpAdapter: useNodeHttpAdapter },
|
|
230
242
|
});
|
|
231
243
|
filesToWrite.push(...files.map((file) => ({
|
|
232
244
|
path: node_path_1.default.posix.join('cli', file.fileName),
|
|
@@ -552,10 +564,17 @@ async function generateMulti(options) {
|
|
|
552
564
|
if (useUnifiedCli && cliTargets.length > 0 && !dryRun) {
|
|
553
565
|
const cliConfig = typeof unifiedCli === 'object' ? unifiedCli : {};
|
|
554
566
|
const toolName = cliConfig.toolName ?? 'app';
|
|
567
|
+
// Auto-enable nodeHttpAdapter for unified CLI unless explicitly disabled
|
|
568
|
+
// Check first target config for explicit nodeHttpAdapter setting
|
|
569
|
+
const firstTargetConfig = configs[names[0]];
|
|
570
|
+
const multiNodeHttpAdapter = firstTargetConfig?.nodeHttpAdapter === true ||
|
|
571
|
+
(firstTargetConfig?.nodeHttpAdapter !== false);
|
|
555
572
|
const { files } = (0, cli_1.generateMultiTargetCli)({
|
|
556
573
|
toolName,
|
|
557
574
|
builtinNames: cliConfig.builtinNames,
|
|
558
575
|
targets: cliTargets,
|
|
576
|
+
nodeHttpAdapter: multiNodeHttpAdapter,
|
|
577
|
+
entryPoint: cliConfig.entryPoint,
|
|
559
578
|
});
|
|
560
579
|
const cliFilesToWrite = files.map((file) => ({
|
|
561
580
|
path: node_path_1.default.posix.join('cli', file.fileName),
|
|
@@ -65,9 +65,19 @@ function buildDefaultSelectString(returnType, isMutation) {
|
|
|
65
65
|
}
|
|
66
66
|
return '';
|
|
67
67
|
}
|
|
68
|
-
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
|
|
69
|
-
const callArgs = [
|
|
70
|
-
if (
|
|
68
|
+
function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr, hasArgs = true) {
|
|
69
|
+
const callArgs = [];
|
|
70
|
+
if (hasArgs) {
|
|
71
|
+
// Operation has arguments: pass args as first param, select as second
|
|
72
|
+
callArgs.push(argsExpr);
|
|
73
|
+
if (selectExpr) {
|
|
74
|
+
callArgs.push(t.objectExpression([
|
|
75
|
+
t.objectProperty(t.identifier('select'), selectExpr),
|
|
76
|
+
]));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (selectExpr) {
|
|
80
|
+
// No arguments: pass { select } as the only param (ORM signature)
|
|
71
81
|
callArgs.push(t.objectExpression([
|
|
72
82
|
t.objectProperty(t.identifier('select'), selectExpr),
|
|
73
83
|
]));
|
|
@@ -161,8 +171,9 @@ export function generateCustomCommand(op, options) {
|
|
|
161
171
|
]));
|
|
162
172
|
selectExpr = t.identifier('selectFields');
|
|
163
173
|
}
|
|
174
|
+
const hasArgs = op.args.length > 0;
|
|
164
175
|
bodyStatements.push(t.variableDeclaration('const', [
|
|
165
|
-
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr))),
|
|
176
|
+
t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs))),
|
|
166
177
|
]));
|
|
167
178
|
if (options?.saveToken) {
|
|
168
179
|
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;
|
|
@@ -7,8 +7,12 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
|
|
|
7
7
|
decl.importKind = typeOnly ? 'type' : 'value';
|
|
8
8
|
return decl;
|
|
9
9
|
}
|
|
10
|
-
export function generateExecutorFile(toolName) {
|
|
10
|
+
export function generateExecutorFile(toolName, options) {
|
|
11
11
|
const statements = [];
|
|
12
|
+
// Import NodeHttpAdapter for *.localhost subdomain routing
|
|
13
|
+
if (options?.nodeHttpAdapter) {
|
|
14
|
+
statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
|
|
15
|
+
}
|
|
12
16
|
statements.push(createImportDeclaration('appstash', ['createConfigStore']));
|
|
13
17
|
statements.push(createImportDeclaration('../orm', ['createClient']));
|
|
14
18
|
statements.push(t.variableDeclaration('const', [
|
|
@@ -61,12 +65,26 @@ export function generateExecutorFile(toolName) {
|
|
|
61
65
|
]))),
|
|
62
66
|
])),
|
|
63
67
|
])),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
t.
|
|
68
|
+
// Build createClient config — use NodeHttpAdapter for *.localhost endpoints
|
|
69
|
+
...(options?.nodeHttpAdapter
|
|
70
|
+
? [
|
|
71
|
+
t.returnStatement(t.callExpression(t.identifier('createClient'), [
|
|
72
|
+
t.objectExpression([
|
|
73
|
+
t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
|
|
74
|
+
t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')),
|
|
75
|
+
t.identifier('headers'),
|
|
76
|
+
])),
|
|
77
|
+
]),
|
|
78
|
+
])),
|
|
79
|
+
]
|
|
80
|
+
: [
|
|
81
|
+
t.returnStatement(t.callExpression(t.identifier('createClient'), [
|
|
82
|
+
t.objectExpression([
|
|
83
|
+
t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('ctx'), t.identifier('endpoint'))),
|
|
84
|
+
t.objectProperty(t.identifier('headers'), t.identifier('headers')),
|
|
85
|
+
]),
|
|
86
|
+
])),
|
|
68
87
|
]),
|
|
69
|
-
])),
|
|
70
88
|
]);
|
|
71
89
|
const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [contextNameParam], getClientBody);
|
|
72
90
|
statements.push(t.exportNamedDeclaration(getClientFunc));
|
|
@@ -77,8 +95,12 @@ export function generateExecutorFile(toolName) {
|
|
|
77
95
|
content: header + '\n' + code,
|
|
78
96
|
};
|
|
79
97
|
}
|
|
80
|
-
export function generateMultiTargetExecutorFile(toolName, targets) {
|
|
98
|
+
export function generateMultiTargetExecutorFile(toolName, targets, options) {
|
|
81
99
|
const statements = [];
|
|
100
|
+
// Import NodeHttpAdapter for *.localhost subdomain routing
|
|
101
|
+
if (options?.nodeHttpAdapter) {
|
|
102
|
+
statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
|
|
103
|
+
}
|
|
82
104
|
statements.push(createImportDeclaration('appstash', ['createConfigStore']));
|
|
83
105
|
for (const target of targets) {
|
|
84
106
|
const aliasName = `create${target.name[0].toUpperCase()}${target.name.slice(1)}Client`;
|
|
@@ -162,12 +184,26 @@ export function generateMultiTargetExecutorFile(toolName, targets) {
|
|
|
162
184
|
]), t.blockStatement([
|
|
163
185
|
t.expressionStatement(t.assignmentExpression('=', t.identifier('endpoint'), t.logicalExpression('||', t.memberExpression(t.identifier('defaultEndpoints'), t.identifier('targetName'), true), t.stringLiteral('')))),
|
|
164
186
|
])),
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
t.
|
|
187
|
+
// Build createClient config — use NodeHttpAdapter for *.localhost endpoints
|
|
188
|
+
...(options?.nodeHttpAdapter
|
|
189
|
+
? [
|
|
190
|
+
t.returnStatement(t.callExpression(t.identifier('createFn'), [
|
|
191
|
+
t.objectExpression([
|
|
192
|
+
t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
|
|
193
|
+
t.identifier('endpoint'),
|
|
194
|
+
t.identifier('headers'),
|
|
195
|
+
])),
|
|
196
|
+
]),
|
|
197
|
+
])),
|
|
198
|
+
]
|
|
199
|
+
: [
|
|
200
|
+
t.returnStatement(t.callExpression(t.identifier('createFn'), [
|
|
201
|
+
t.objectExpression([
|
|
202
|
+
t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')),
|
|
203
|
+
t.objectProperty(t.identifier('headers'), t.identifier('headers')),
|
|
204
|
+
]),
|
|
205
|
+
])),
|
|
169
206
|
]),
|
|
170
|
-
])),
|
|
171
207
|
]);
|
|
172
208
|
const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [targetNameParam, contextNameParam], getClientBody);
|
|
173
209
|
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,17 @@ 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;
|
|
44
|
+
/** Generate a runnable index.ts entry point */
|
|
45
|
+
entryPoint?: boolean;
|
|
38
46
|
}
|
|
39
47
|
export declare function resolveBuiltinNames(targetNames: string[], userOverrides?: BuiltinNames): {
|
|
40
48
|
auth: string;
|
|
@@ -50,5 +58,5 @@ export { generateReadme, generateAgentsDocs, getCliMcpTools, generateSkills, gen
|
|
|
50
58
|
export type { MultiTargetDocsInput } from './docs-generator';
|
|
51
59
|
export { resolveDocsConfig } from '../docs-utils';
|
|
52
60
|
export type { GeneratedDocFile, McpTool } from '../docs-utils';
|
|
53
|
-
export { generateUtilsFile } from './utils-generator';
|
|
61
|
+
export { generateUtilsFile, generateEntryPointFile } from './utils-generator';
|
|
54
62
|
export type { GeneratedFile, MultiTargetExecutorInput } from './executor-generator';
|
|
@@ -3,7 +3,7 @@ import { generateCustomCommand } from './custom-command-generator';
|
|
|
3
3
|
import { generateExecutorFile, generateMultiTargetExecutorFile } from './executor-generator';
|
|
4
4
|
import { generateAuthCommand, generateAuthCommandWithName, generateContextCommand, generateMultiTargetContextCommand, } from './infra-generator';
|
|
5
5
|
import { generateTableCommand } from './table-command-generator';
|
|
6
|
-
import { generateUtilsFile } from './utils-generator';
|
|
6
|
+
import { generateUtilsFile, generateNodeFetchFile, generateEntryPointFile } from './utils-generator';
|
|
7
7
|
export function generateCli(options) {
|
|
8
8
|
const { tables, customOperations, config } = options;
|
|
9
9
|
const files = [];
|
|
@@ -11,16 +11,26 @@ export function generateCli(options) {
|
|
|
11
11
|
const toolName = typeof cliConfig === 'object' && cliConfig.toolName
|
|
12
12
|
? cliConfig.toolName
|
|
13
13
|
: 'app';
|
|
14
|
-
|
|
14
|
+
// Use top-level nodeHttpAdapter from config (auto-enabled for CLI by generate.ts)
|
|
15
|
+
const useNodeHttpAdapter = !!config.nodeHttpAdapter;
|
|
16
|
+
const executorFile = generateExecutorFile(toolName, {
|
|
17
|
+
nodeHttpAdapter: useNodeHttpAdapter,
|
|
18
|
+
});
|
|
15
19
|
files.push(executorFile);
|
|
16
20
|
const utilsFile = generateUtilsFile();
|
|
17
21
|
files.push(utilsFile);
|
|
22
|
+
// Generate node HTTP adapter if configured (for *.localhost subdomain routing)
|
|
23
|
+
if (useNodeHttpAdapter) {
|
|
24
|
+
files.push(generateNodeFetchFile());
|
|
25
|
+
}
|
|
18
26
|
const contextFile = generateContextCommand(toolName);
|
|
19
27
|
files.push(contextFile);
|
|
20
28
|
const authFile = generateAuthCommand(toolName);
|
|
21
29
|
files.push(authFile);
|
|
22
30
|
for (const table of tables) {
|
|
23
|
-
const tableFile = generateTableCommand(table
|
|
31
|
+
const tableFile = generateTableCommand(table, {
|
|
32
|
+
typeRegistry: options.typeRegistry,
|
|
33
|
+
});
|
|
24
34
|
files.push(tableFile);
|
|
25
35
|
}
|
|
26
36
|
const allCustomOps = [
|
|
@@ -33,6 +43,11 @@ export function generateCli(options) {
|
|
|
33
43
|
}
|
|
34
44
|
const commandMapFile = generateCommandMap(tables, allCustomOps, toolName);
|
|
35
45
|
files.push(commandMapFile);
|
|
46
|
+
// Generate entry point if configured
|
|
47
|
+
const generateEntryPoint = typeof cliConfig === 'object' && !!cliConfig.entryPoint;
|
|
48
|
+
if (generateEntryPoint) {
|
|
49
|
+
files.push(generateEntryPointFile());
|
|
50
|
+
}
|
|
36
51
|
return {
|
|
37
52
|
files,
|
|
38
53
|
stats: {
|
|
@@ -65,10 +80,16 @@ export function generateMultiTargetCli(options) {
|
|
|
65
80
|
endpoint: t.endpoint,
|
|
66
81
|
ormImportPath: t.ormImportPath,
|
|
67
82
|
}));
|
|
68
|
-
const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs
|
|
83
|
+
const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, {
|
|
84
|
+
nodeHttpAdapter: !!options.nodeHttpAdapter,
|
|
85
|
+
});
|
|
69
86
|
files.push(executorFile);
|
|
70
87
|
const utilsFile = generateUtilsFile();
|
|
71
88
|
files.push(utilsFile);
|
|
89
|
+
// Generate node HTTP adapter if configured (for *.localhost subdomain routing)
|
|
90
|
+
if (options.nodeHttpAdapter) {
|
|
91
|
+
files.push(generateNodeFetchFile());
|
|
92
|
+
}
|
|
72
93
|
const contextFile = generateMultiTargetContextCommand(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
|
|
73
94
|
files.push(contextFile);
|
|
74
95
|
const authFile = generateAuthCommandWithName(toolName, builtinNames.auth);
|
|
@@ -86,6 +107,7 @@ export function generateMultiTargetCli(options) {
|
|
|
86
107
|
const tableFile = generateTableCommand(table, {
|
|
87
108
|
targetName: target.name,
|
|
88
109
|
executorImportPath: '../../executor',
|
|
110
|
+
typeRegistry: target.typeRegistry,
|
|
89
111
|
});
|
|
90
112
|
files.push(tableFile);
|
|
91
113
|
}
|
|
@@ -113,6 +135,10 @@ export function generateMultiTargetCli(options) {
|
|
|
113
135
|
targets: commandMapTargets,
|
|
114
136
|
});
|
|
115
137
|
files.push(commandMapFile);
|
|
138
|
+
// Generate entry point if configured
|
|
139
|
+
if (options.entryPoint) {
|
|
140
|
+
files.push(generateEntryPointFile());
|
|
141
|
+
}
|
|
116
142
|
return {
|
|
117
143
|
files,
|
|
118
144
|
stats: {
|
|
@@ -131,4 +157,4 @@ export { generateCommandMap, generateMultiTargetCommandMap } from './command-map
|
|
|
131
157
|
export { generateContextCommand, generateAuthCommand, generateMultiTargetContextCommand, generateAuthCommandWithName, } from './infra-generator';
|
|
132
158
|
export { generateReadme, generateAgentsDocs, getCliMcpTools, generateSkills, generateMultiTargetReadme, generateMultiTargetAgentsDocs, getMultiTargetCliMcpTools, generateMultiTargetSkills, } from './docs-generator';
|
|
133
159
|
export { resolveDocsConfig } from '../docs-utils';
|
|
134
|
-
export { generateUtilsFile } from './utils-generator';
|
|
160
|
+
export { generateUtilsFile, generateEntryPointFile } from './utils-generator';
|
|
@@ -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;
|
|
@@ -2,6 +2,7 @@ import * as t from '@babel/types';
|
|
|
2
2
|
import { toKebabCase } from 'komoji';
|
|
3
3
|
import { generateCode } from '../babel-ast';
|
|
4
4
|
import { getGeneratedFileHeader, getPrimaryKeyInfo, getScalarFields, getTableNames, ucFirst, } from '../utils';
|
|
5
|
+
import { getCreateInputTypeName } from '../utils';
|
|
5
6
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
6
7
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
7
8
|
const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
|
|
@@ -147,7 +148,46 @@ function buildGetHandler(table, targetName) {
|
|
|
147
148
|
t.tryStatement(t.blockStatement(tryBody), buildErrorCatch('Record not found.')),
|
|
148
149
|
]), false, true);
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Get the set of field names that have defaults in the create input type.
|
|
153
|
+
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
|
|
154
|
+
* TypeRegistry and checks each field's defaultValue from introspection.
|
|
155
|
+
*/
|
|
156
|
+
function getFieldsWithDefaults(table, typeRegistry) {
|
|
157
|
+
const fieldsWithDefaults = new Set();
|
|
158
|
+
if (!typeRegistry)
|
|
159
|
+
return fieldsWithDefaults;
|
|
160
|
+
// Look up the CreateXInput type (e.g. CreateDatabaseInput)
|
|
161
|
+
const createInputTypeName = getCreateInputTypeName(table);
|
|
162
|
+
const createInputType = typeRegistry.get(createInputTypeName);
|
|
163
|
+
if (!createInputType?.inputFields)
|
|
164
|
+
return fieldsWithDefaults;
|
|
165
|
+
// The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
|
|
166
|
+
// Find the inner input type that contains the actual field definitions
|
|
167
|
+
for (const inputField of createInputType.inputFields) {
|
|
168
|
+
// The inner field's type name is the actual input type (e.g. DatabaseInput)
|
|
169
|
+
const innerTypeName = inputField.type.name
|
|
170
|
+
|| inputField.type.ofType?.name
|
|
171
|
+
|| inputField.type.ofType?.ofType?.name;
|
|
172
|
+
if (!innerTypeName)
|
|
173
|
+
continue;
|
|
174
|
+
const innerType = typeRegistry.get(innerTypeName);
|
|
175
|
+
if (!innerType?.inputFields)
|
|
176
|
+
continue;
|
|
177
|
+
// Check each field in the inner input type for defaultValue
|
|
178
|
+
for (const field of innerType.inputFields) {
|
|
179
|
+
if (field.defaultValue !== undefined) {
|
|
180
|
+
fieldsWithDefaults.add(field.name);
|
|
181
|
+
}
|
|
182
|
+
// Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
|
|
183
|
+
if (field.type.kind !== 'NON_NULL') {
|
|
184
|
+
fieldsWithDefaults.add(field.name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return fieldsWithDefaults;
|
|
189
|
+
}
|
|
190
|
+
function buildMutationHandler(table, operation, targetName, typeRegistry) {
|
|
151
191
|
const { singularName } = getTableNames(table);
|
|
152
192
|
const pkFields = getPrimaryKeyInfo(table);
|
|
153
193
|
const pk = pkFields[0];
|
|
@@ -155,6 +195,8 @@ function buildMutationHandler(table, operation, targetName) {
|
|
|
155
195
|
f.name !== 'nodeId' &&
|
|
156
196
|
f.name !== 'createdAt' &&
|
|
157
197
|
f.name !== 'updatedAt');
|
|
198
|
+
// Get fields that have defaults from introspection (for create operations)
|
|
199
|
+
const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
|
|
158
200
|
const questions = [];
|
|
159
201
|
if (operation === 'update' || operation === 'delete') {
|
|
160
202
|
questions.push(t.objectExpression([
|
|
@@ -166,11 +208,14 @@ function buildMutationHandler(table, operation, targetName) {
|
|
|
166
208
|
}
|
|
167
209
|
if (operation !== 'delete') {
|
|
168
210
|
for (const field of editableFields) {
|
|
211
|
+
// For create: field is required only if it has no default value
|
|
212
|
+
// For update: all fields are optional (user only updates what they want)
|
|
213
|
+
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
|
|
169
214
|
questions.push(t.objectExpression([
|
|
170
215
|
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
|
|
171
216
|
t.objectProperty(t.identifier('name'), t.stringLiteral(field.name)),
|
|
172
217
|
t.objectProperty(t.identifier('message'), t.stringLiteral(field.name)),
|
|
173
|
-
t.objectProperty(t.identifier('required'), t.booleanLiteral(
|
|
218
|
+
t.objectProperty(t.identifier('required'), t.booleanLiteral(isRequired)),
|
|
174
219
|
]));
|
|
175
220
|
}
|
|
176
221
|
}
|
|
@@ -329,9 +374,9 @@ export function generateTableCommand(table, options) {
|
|
|
329
374
|
const tn = options?.targetName;
|
|
330
375
|
statements.push(buildListHandler(table, tn));
|
|
331
376
|
statements.push(buildGetHandler(table, tn));
|
|
332
|
-
statements.push(buildMutationHandler(table, 'create', tn));
|
|
333
|
-
statements.push(buildMutationHandler(table, 'update', tn));
|
|
334
|
-
statements.push(buildMutationHandler(table, 'delete', tn));
|
|
377
|
+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
|
|
378
|
+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
|
|
379
|
+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
|
|
335
380
|
const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
|
|
336
381
|
const code = generateCode(statements);
|
|
337
382
|
return {
|
|
@@ -8,3 +8,20 @@ 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;
|
|
19
|
+
/**
|
|
20
|
+
* Generate an index.ts entry point file for the CLI.
|
|
21
|
+
*
|
|
22
|
+
* Creates a runnable entry point that imports the command map,
|
|
23
|
+
* handles --version and --tty flags, and starts the CLI.
|
|
24
|
+
* This is off by default (cliEntryPoint: false) since many projects
|
|
25
|
+
* provide their own entry point with custom configuration.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateEntryPointFile(): GeneratedFile;
|
|
@@ -40,3 +40,30 @@ export function generateUtilsFile() {
|
|
|
40
40
|
content: readTemplateFile('cli-utils.ts', 'CLI utility functions for type coercion and input handling'),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate a node-fetch.ts file with NodeHttpAdapter for CLI.
|
|
45
|
+
*
|
|
46
|
+
* Provides a GraphQLAdapter implementation using node:http/node:https
|
|
47
|
+
* instead of the Fetch API. This cleanly handles *.localhost subdomain
|
|
48
|
+
* routing (DNS resolution + Host header) without any global patching.
|
|
49
|
+
*/
|
|
50
|
+
export function generateNodeFetchFile() {
|
|
51
|
+
return {
|
|
52
|
+
fileName: 'node-fetch.ts',
|
|
53
|
+
content: readTemplateFile('node-fetch.ts', 'Node HTTP adapter for localhost subdomain routing'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate an index.ts entry point file for the CLI.
|
|
58
|
+
*
|
|
59
|
+
* Creates a runnable entry point that imports the command map,
|
|
60
|
+
* handles --version and --tty flags, and starts the CLI.
|
|
61
|
+
* This is off by default (cliEntryPoint: false) since many projects
|
|
62
|
+
* provide their own entry point with custom configuration.
|
|
63
|
+
*/
|
|
64
|
+
export function generateEntryPointFile() {
|
|
65
|
+
return {
|
|
66
|
+
fileName: 'index.ts',
|
|
67
|
+
content: readTemplateFile('cli-entry.ts', 'CLI entry point'),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -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;
|