@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.
Files changed (31) hide show
  1. package/core/codegen/cli/custom-command-generator.js +15 -4
  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 +10 -2
  5. package/core/codegen/cli/index.js +31 -4
  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 +17 -0
  9. package/core/codegen/cli/utils-generator.js +29 -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-entry.ts +33 -0
  14. package/core/codegen/templates/node-fetch.ts +162 -0
  15. package/core/generate.js +21 -2
  16. package/esm/core/codegen/cli/custom-command-generator.js +15 -4
  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 +10 -2
  20. package/esm/core/codegen/cli/index.js +31 -5
  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 +17 -0
  24. package/esm/core/codegen/cli/utils-generator.js +27 -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 +21 -2
  29. package/esm/types/config.d.ts +26 -0
  30. package/package.json +10 -10
  31. 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 = [argsExpr];
70
- if (selectExpr) {
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 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,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
- 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 = [
@@ -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
- 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,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): GeneratedClientFile;
28
+ export declare function generateCreateClientFile(tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean, options?: {
29
+ nodeHttpAdapter?: boolean;
30
+ }): GeneratedClientFile;