@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.
Files changed (31) hide show
  1. package/core/codegen/cli/custom-command-generator.js +58 -19
  2. package/core/codegen/cli/executor-generator.d.ts +6 -2
  3. package/core/codegen/cli/executor-generator.js +48 -12
  4. package/core/codegen/cli/index.d.ts +7 -1
  5. package/core/codegen/cli/index.js +20 -3
  6. package/core/codegen/cli/table-command-generator.d.ts +3 -1
  7. package/core/codegen/cli/table-command-generator.js +50 -5
  8. package/core/codegen/cli/utils-generator.d.ts +8 -0
  9. package/core/codegen/cli/utils-generator.js +14 -0
  10. package/core/codegen/orm/client-generator.d.ts +3 -1
  11. package/core/codegen/orm/client-generator.js +7 -1
  12. package/core/codegen/orm/index.js +1 -1
  13. package/core/codegen/templates/cli-utils.ts +59 -2
  14. package/core/codegen/templates/node-fetch.ts +162 -0
  15. package/core/generate.js +20 -2
  16. package/esm/core/codegen/cli/custom-command-generator.js +58 -19
  17. package/esm/core/codegen/cli/executor-generator.d.ts +6 -2
  18. package/esm/core/codegen/cli/executor-generator.js +48 -12
  19. package/esm/core/codegen/cli/index.d.ts +7 -1
  20. package/esm/core/codegen/cli/index.js +21 -4
  21. package/esm/core/codegen/cli/table-command-generator.d.ts +3 -1
  22. package/esm/core/codegen/cli/table-command-generator.js +50 -5
  23. package/esm/core/codegen/cli/utils-generator.d.ts +8 -0
  24. package/esm/core/codegen/cli/utils-generator.js +13 -0
  25. package/esm/core/codegen/orm/client-generator.d.ts +3 -1
  26. package/esm/core/codegen/orm/client-generator.js +7 -1
  27. package/esm/core/codegen/orm/index.js +1 -1
  28. package/esm/core/generate.js +20 -2
  29. package/esm/types/config.d.ts +18 -0
  30. package/package.json +10 -10
  31. package/types/config.d.ts +18 -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,16 @@ 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,
559
577
  });
560
578
  const cliFilesToWrite = files.map((file) => ({
561
579
  path: node_path_1.default.posix.join('cli', file.fileName),
@@ -43,30 +43,46 @@ function unwrapType(ref) {
43
43
  return ref;
44
44
  }
45
45
  /**
46
- * Build a select object expression from return-type fields.
47
- * If the return type has known fields, generates { field1: true, field2: true, ... }.
48
- * Falls back to { clientMutationId: true } for mutations without known fields.
46
+ * Check if the return type (after unwrapping) is an OBJECT type.
49
47
  */
50
- function buildSelectObject(returnType, isMutation) {
48
+ function hasObjectReturnType(returnType) {
49
+ const base = unwrapType(returnType);
50
+ return base.kind === 'OBJECT';
51
+ }
52
+ /**
53
+ * Build a default select string from the return type's top-level scalar fields.
54
+ * For OBJECT return types with known fields, generates a comma-separated list
55
+ * of all top-level field names (e.g. 'clientMutationId,result').
56
+ * Falls back to 'clientMutationId' for mutations without known fields.
57
+ */
58
+ function buildDefaultSelectString(returnType, isMutation) {
51
59
  const base = unwrapType(returnType);
52
60
  if (base.fields && base.fields.length > 0) {
53
- return t.objectExpression(base.fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true))));
61
+ return base.fields.map((f) => f.name).join(',');
54
62
  }
55
- // Fallback: all PostGraphile mutation payloads have clientMutationId
56
63
  if (isMutation) {
57
- return t.objectExpression([
58
- t.objectProperty(t.identifier('clientMutationId'), t.booleanLiteral(true)),
59
- ]);
64
+ return 'clientMutationId';
60
65
  }
61
- return t.objectExpression([]);
66
+ return '';
62
67
  }
63
- function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
64
- return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), [
65
- argsExpr,
66
- t.objectExpression([
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)
81
+ callArgs.push(t.objectExpression([
67
82
  t.objectProperty(t.identifier('select'), selectExpr),
68
- ]),
69
- ]), t.identifier('execute')), []);
83
+ ]));
84
+ }
85
+ return t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.memberExpression(t.identifier('client'), t.identifier(opKind)), t.identifier(opName)), callArgs), t.identifier('execute')), []);
70
86
  }
71
87
  export function generateCustomCommand(op, options) {
72
88
  const commandName = toKebabCase(op.name);
@@ -82,13 +98,23 @@ export function generateCustomCommand(op, options) {
82
98
  const base = unwrapType(arg.type);
83
99
  return base.kind === 'INPUT_OBJECT';
84
100
  });
101
+ // Check if return type is OBJECT (needs --select flag)
102
+ const isObjectReturn = hasObjectReturnType(op.returnType);
85
103
  const utilsPath = options?.executorImportPath
86
104
  ? options.executorImportPath.replace(/\/executor$/, '/utils')
87
105
  : '../utils';
88
106
  statements.push(createImportDeclaration('inquirerer', ['CLIOptions', 'Inquirerer']));
89
107
  statements.push(createImportDeclaration(executorPath, imports));
108
+ // Build the list of utils imports needed
109
+ const utilsImports = [];
90
110
  if (hasInputObjectArg) {
91
- statements.push(createImportDeclaration(utilsPath, ['parseMutationInput']));
111
+ utilsImports.push('parseMutationInput');
112
+ }
113
+ if (isObjectReturn) {
114
+ utilsImports.push('buildSelectFromPaths');
115
+ }
116
+ if (utilsImports.length > 0) {
117
+ statements.push(createImportDeclaration(utilsPath, utilsImports));
92
118
  }
93
119
  const questionsArray = op.args.length > 0
94
120
  ? buildQuestionsArray(op.args)
@@ -132,9 +158,22 @@ export function generateCustomCommand(op, options) {
132
158
  ? t.identifier('parsedAnswers')
133
159
  : t.identifier('answers'))
134
160
  : t.objectExpression([]);
135
- const selectExpr = buildSelectObject(op.returnType, op.kind === 'mutation');
161
+ // For OBJECT return types, generate runtime select from --select flag
162
+ // For scalar return types, no select is needed
163
+ let selectExpr;
164
+ if (isObjectReturn) {
165
+ const defaultSelect = buildDefaultSelectString(op.returnType, op.kind === 'mutation');
166
+ // Generate: const selectFields = buildSelectFromPaths(argv.select ?? 'defaultFields')
167
+ bodyStatements.push(t.variableDeclaration('const', [
168
+ t.variableDeclarator(t.identifier('selectFields'), t.callExpression(t.identifier('buildSelectFromPaths'), [
169
+ t.logicalExpression('??', t.memberExpression(t.identifier('argv'), t.identifier('select')), t.stringLiteral(defaultSelect)),
170
+ ])),
171
+ ]));
172
+ selectExpr = t.identifier('selectFields');
173
+ }
174
+ const hasArgs = op.args.length > 0;
136
175
  bodyStatements.push(t.variableDeclaration('const', [
137
- 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))),
138
177
  ]));
139
178
  if (options?.saveToken) {
140
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 declare function generateExecutorFile(toolName: string): GeneratedFile;
11
- export declare function generateMultiTargetExecutorFile(toolName: string, targets: MultiTargetExecutorInput[]): GeneratedFile;
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
- t.returnStatement(t.callExpression(t.identifier('createClient'), [
65
- t.objectExpression([
66
- t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('ctx'), t.identifier('endpoint'))),
67
- t.objectProperty(t.identifier('headers'), t.identifier('headers')),
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
- t.returnStatement(t.callExpression(t.identifier('createFn'), [
166
- t.objectExpression([
167
- t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')),
168
- t.objectProperty(t.identifier('headers'), t.identifier('headers')),
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,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;
@@ -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 } 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
- const executorFile = generateExecutorFile(toolName);
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 = [
@@ -65,10 +75,16 @@ export function generateMultiTargetCli(options) {
65
75
  endpoint: t.endpoint,
66
76
  ormImportPath: t.ormImportPath,
67
77
  }));
68
- const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs);
78
+ const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, {
79
+ nodeHttpAdapter: !!options.nodeHttpAdapter,
80
+ });
69
81
  files.push(executorFile);
70
82
  const utilsFile = generateUtilsFile();
71
83
  files.push(utilsFile);
84
+ // Generate node HTTP adapter if configured (for *.localhost subdomain routing)
85
+ if (options.nodeHttpAdapter) {
86
+ files.push(generateNodeFetchFile());
87
+ }
72
88
  const contextFile = generateMultiTargetContextCommand(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
73
89
  files.push(contextFile);
74
90
  const authFile = generateAuthCommandWithName(toolName, builtinNames.auth);
@@ -86,6 +102,7 @@ export function generateMultiTargetCli(options) {
86
102
  const tableFile = generateTableCommand(table, {
87
103
  targetName: target.name,
88
104
  executorImportPath: '../../executor',
105
+ typeRegistry: target.typeRegistry,
89
106
  });
90
107
  files.push(tableFile);
91
108
  }
@@ -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
- function buildMutationHandler(table, operation, targetName) {
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(operation === 'create')),
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,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;