@constructive-io/graphql-codegen 4.2.0 → 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 (30) 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 +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/node-fetch.ts +162 -0
  14. package/core/generate.js +20 -2
  15. package/esm/core/codegen/cli/custom-command-generator.js +15 -4
  16. package/esm/core/codegen/cli/executor-generator.d.ts +6 -2
  17. package/esm/core/codegen/cli/executor-generator.js +48 -12
  18. package/esm/core/codegen/cli/index.d.ts +7 -1
  19. package/esm/core/codegen/cli/index.js +21 -4
  20. package/esm/core/codegen/cli/table-command-generator.d.ts +3 -1
  21. package/esm/core/codegen/cli/table-command-generator.js +50 -5
  22. package/esm/core/codegen/cli/utils-generator.d.ts +8 -0
  23. package/esm/core/codegen/cli/utils-generator.js +13 -0
  24. package/esm/core/codegen/orm/client-generator.d.ts +3 -1
  25. package/esm/core/codegen/orm/client-generator.js +7 -1
  26. package/esm/core/codegen/orm/index.js +1 -1
  27. package/esm/core/generate.js +20 -2
  28. package/esm/types/config.d.ts +18 -0
  29. package/package.json +10 -10
  30. package/types/config.d.ts +18 -0
@@ -101,9 +101,19 @@ function buildDefaultSelectString(returnType, isMutation) {
101
101
  }
102
102
  return '';
103
103
  }
104
- function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr) {
105
- const callArgs = [argsExpr];
106
- if (selectExpr) {
104
+ function buildOrmCustomCall(opKind, opName, argsExpr, selectExpr, hasArgs = true) {
105
+ const callArgs = [];
106
+ if (hasArgs) {
107
+ // Operation has arguments: pass args as first param, select as second
108
+ callArgs.push(argsExpr);
109
+ if (selectExpr) {
110
+ callArgs.push(t.objectExpression([
111
+ t.objectProperty(t.identifier('select'), selectExpr),
112
+ ]));
113
+ }
114
+ }
115
+ else if (selectExpr) {
116
+ // No arguments: pass { select } as the only param (ORM signature)
107
117
  callArgs.push(t.objectExpression([
108
118
  t.objectProperty(t.identifier('select'), selectExpr),
109
119
  ]));
@@ -197,8 +207,9 @@ function generateCustomCommand(op, options) {
197
207
  ]));
198
208
  selectExpr = t.identifier('selectFields');
199
209
  }
210
+ const hasArgs = op.args.length > 0;
200
211
  bodyStatements.push(t.variableDeclaration('const', [
201
- t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr))),
212
+ t.variableDeclarator(t.identifier('result'), t.awaitExpression(buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs))),
202
213
  ]));
203
214
  if (options?.saveToken) {
204
215
  bodyStatements.push(t.ifStatement(t.logicalExpression('&&', t.memberExpression(t.identifier('argv'), t.identifier('saveToken')), t.identifier('result')), t.blockStatement([
@@ -7,5 +7,9 @@ export interface MultiTargetExecutorInput {
7
7
  endpoint: string;
8
8
  ormImportPath: string;
9
9
  }
10
- export 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;
@@ -44,8 +44,12 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
44
44
  decl.importKind = typeOnly ? 'type' : 'value';
45
45
  return decl;
46
46
  }
47
- function generateExecutorFile(toolName) {
47
+ function generateExecutorFile(toolName, options) {
48
48
  const statements = [];
49
+ // Import NodeHttpAdapter for *.localhost subdomain routing
50
+ if (options?.nodeHttpAdapter) {
51
+ statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
52
+ }
49
53
  statements.push(createImportDeclaration('appstash', ['createConfigStore']));
50
54
  statements.push(createImportDeclaration('../orm', ['createClient']));
51
55
  statements.push(t.variableDeclaration('const', [
@@ -98,12 +102,26 @@ function generateExecutorFile(toolName) {
98
102
  ]))),
99
103
  ])),
100
104
  ])),
101
- t.returnStatement(t.callExpression(t.identifier('createClient'), [
102
- t.objectExpression([
103
- t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('ctx'), t.identifier('endpoint'))),
104
- t.objectProperty(t.identifier('headers'), t.identifier('headers')),
105
+ // Build createClient config — use NodeHttpAdapter for *.localhost endpoints
106
+ ...(options?.nodeHttpAdapter
107
+ ? [
108
+ t.returnStatement(t.callExpression(t.identifier('createClient'), [
109
+ t.objectExpression([
110
+ t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
111
+ t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')),
112
+ t.identifier('headers'),
113
+ ])),
114
+ ]),
115
+ ])),
116
+ ]
117
+ : [
118
+ t.returnStatement(t.callExpression(t.identifier('createClient'), [
119
+ t.objectExpression([
120
+ t.objectProperty(t.identifier('endpoint'), t.memberExpression(t.identifier('ctx'), t.identifier('endpoint'))),
121
+ t.objectProperty(t.identifier('headers'), t.identifier('headers')),
122
+ ]),
123
+ ])),
105
124
  ]),
106
- ])),
107
125
  ]);
108
126
  const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [contextNameParam], getClientBody);
109
127
  statements.push(t.exportNamedDeclaration(getClientFunc));
@@ -114,8 +132,12 @@ function generateExecutorFile(toolName) {
114
132
  content: header + '\n' + code,
115
133
  };
116
134
  }
117
- function generateMultiTargetExecutorFile(toolName, targets) {
135
+ function generateMultiTargetExecutorFile(toolName, targets, options) {
118
136
  const statements = [];
137
+ // Import NodeHttpAdapter for *.localhost subdomain routing
138
+ if (options?.nodeHttpAdapter) {
139
+ statements.push(createImportDeclaration('./node-fetch', ['NodeHttpAdapter']));
140
+ }
119
141
  statements.push(createImportDeclaration('appstash', ['createConfigStore']));
120
142
  for (const target of targets) {
121
143
  const aliasName = `create${target.name[0].toUpperCase()}${target.name.slice(1)}Client`;
@@ -199,12 +221,26 @@ function generateMultiTargetExecutorFile(toolName, targets) {
199
221
  ]), t.blockStatement([
200
222
  t.expressionStatement(t.assignmentExpression('=', t.identifier('endpoint'), t.logicalExpression('||', t.memberExpression(t.identifier('defaultEndpoints'), t.identifier('targetName'), true), t.stringLiteral('')))),
201
223
  ])),
202
- t.returnStatement(t.callExpression(t.identifier('createFn'), [
203
- t.objectExpression([
204
- t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')),
205
- t.objectProperty(t.identifier('headers'), t.identifier('headers')),
224
+ // Build createClient config — use NodeHttpAdapter for *.localhost endpoints
225
+ ...(options?.nodeHttpAdapter
226
+ ? [
227
+ t.returnStatement(t.callExpression(t.identifier('createFn'), [
228
+ t.objectExpression([
229
+ t.objectProperty(t.identifier('adapter'), t.newExpression(t.identifier('NodeHttpAdapter'), [
230
+ t.identifier('endpoint'),
231
+ t.identifier('headers'),
232
+ ])),
233
+ ]),
234
+ ])),
235
+ ]
236
+ : [
237
+ t.returnStatement(t.callExpression(t.identifier('createFn'), [
238
+ t.objectExpression([
239
+ t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')),
240
+ t.objectProperty(t.identifier('headers'), t.identifier('headers')),
241
+ ]),
242
+ ])),
206
243
  ]),
207
- ])),
208
244
  ]);
209
245
  const getClientFunc = t.functionDeclaration(t.identifier('getClient'), [targetNameParam, contextNameParam], getClientBody);
210
246
  statements.push(t.exportNamedDeclaration(getClientFunc));
@@ -1,5 +1,5 @@
1
1
  import type { BuiltinNames, GraphQLSDKConfigTarget } from '../../../types/config';
2
- import type { CleanOperation, CleanTable } from '../../../types/schema';
2
+ import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema';
3
3
  import type { GeneratedFile } from './executor-generator';
4
4
  export interface GenerateCliOptions {
5
5
  tables: CleanTable[];
@@ -8,6 +8,8 @@ export interface GenerateCliOptions {
8
8
  mutations: CleanOperation[];
9
9
  };
10
10
  config: GraphQLSDKConfigTarget;
11
+ /** TypeRegistry from introspection, used to check field defaults */
12
+ typeRegistry?: TypeRegistry;
11
13
  }
12
14
  export interface GenerateCliResult {
13
15
  files: GeneratedFile[];
@@ -30,11 +32,15 @@ export interface MultiTargetCliTarget {
30
32
  mutations: CleanOperation[];
31
33
  };
32
34
  isAuthTarget?: boolean;
35
+ /** TypeRegistry from introspection, used to check field defaults */
36
+ typeRegistry?: TypeRegistry;
33
37
  }
34
38
  export interface GenerateMultiTargetCliOptions {
35
39
  toolName: string;
36
40
  builtinNames?: BuiltinNames;
37
41
  targets: MultiTargetCliTarget[];
42
+ /** Enable NodeHttpAdapter for *.localhost subdomain routing */
43
+ nodeHttpAdapter?: boolean;
38
44
  }
39
45
  export declare function resolveBuiltinNames(targetNames: string[], userOverrides?: BuiltinNames): {
40
46
  auth: string;
@@ -17,16 +17,26 @@ function generateCli(options) {
17
17
  const toolName = typeof cliConfig === 'object' && cliConfig.toolName
18
18
  ? cliConfig.toolName
19
19
  : 'app';
20
- const executorFile = (0, executor_generator_1.generateExecutorFile)(toolName);
20
+ // Use top-level nodeHttpAdapter from config (auto-enabled for CLI by generate.ts)
21
+ const useNodeHttpAdapter = !!config.nodeHttpAdapter;
22
+ const executorFile = (0, executor_generator_1.generateExecutorFile)(toolName, {
23
+ nodeHttpAdapter: useNodeHttpAdapter,
24
+ });
21
25
  files.push(executorFile);
22
26
  const utilsFile = (0, utils_generator_1.generateUtilsFile)();
23
27
  files.push(utilsFile);
28
+ // Generate node HTTP adapter if configured (for *.localhost subdomain routing)
29
+ if (useNodeHttpAdapter) {
30
+ files.push((0, utils_generator_1.generateNodeFetchFile)());
31
+ }
24
32
  const contextFile = (0, infra_generator_1.generateContextCommand)(toolName);
25
33
  files.push(contextFile);
26
34
  const authFile = (0, infra_generator_1.generateAuthCommand)(toolName);
27
35
  files.push(authFile);
28
36
  for (const table of tables) {
29
- const tableFile = (0, table_command_generator_1.generateTableCommand)(table);
37
+ const tableFile = (0, table_command_generator_1.generateTableCommand)(table, {
38
+ typeRegistry: options.typeRegistry,
39
+ });
30
40
  files.push(tableFile);
31
41
  }
32
42
  const allCustomOps = [
@@ -71,10 +81,16 @@ function generateMultiTargetCli(options) {
71
81
  endpoint: t.endpoint,
72
82
  ormImportPath: t.ormImportPath,
73
83
  }));
74
- const executorFile = (0, executor_generator_1.generateMultiTargetExecutorFile)(toolName, executorInputs);
84
+ const executorFile = (0, executor_generator_1.generateMultiTargetExecutorFile)(toolName, executorInputs, {
85
+ nodeHttpAdapter: !!options.nodeHttpAdapter,
86
+ });
75
87
  files.push(executorFile);
76
88
  const utilsFile = (0, utils_generator_1.generateUtilsFile)();
77
89
  files.push(utilsFile);
90
+ // Generate node HTTP adapter if configured (for *.localhost subdomain routing)
91
+ if (options.nodeHttpAdapter) {
92
+ files.push((0, utils_generator_1.generateNodeFetchFile)());
93
+ }
78
94
  const contextFile = (0, infra_generator_1.generateMultiTargetContextCommand)(toolName, builtinNames.context, targets.map((t) => ({ name: t.name, endpoint: t.endpoint })));
79
95
  files.push(contextFile);
80
96
  const authFile = (0, infra_generator_1.generateAuthCommandWithName)(toolName, builtinNames.auth);
@@ -92,6 +108,7 @@ function generateMultiTargetCli(options) {
92
108
  const tableFile = (0, table_command_generator_1.generateTableCommand)(table, {
93
109
  targetName: target.name,
94
110
  executorImportPath: '../../executor',
111
+ typeRegistry: target.typeRegistry,
95
112
  });
96
113
  files.push(tableFile);
97
114
  }
@@ -1,7 +1,9 @@
1
- import type { CleanTable } from '../../../types/schema';
1
+ import type { CleanTable, TypeRegistry } from '../../../types/schema';
2
2
  import type { GeneratedFile } from './executor-generator';
3
3
  export interface TableCommandOptions {
4
4
  targetName?: string;
5
5
  executorImportPath?: string;
6
+ /** TypeRegistry from introspection, used to check field defaults */
7
+ typeRegistry?: TypeRegistry;
6
8
  }
7
9
  export declare function generateTableCommand(table: CleanTable, options?: TableCommandOptions): GeneratedFile;
@@ -38,6 +38,7 @@ const t = __importStar(require("@babel/types"));
38
38
  const komoji_1 = require("komoji");
39
39
  const babel_ast_1 = require("../babel-ast");
40
40
  const utils_1 = require("../utils");
41
+ const utils_2 = require("../utils");
41
42
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
42
43
  const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
43
44
  const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
@@ -183,7 +184,46 @@ function buildGetHandler(table, targetName) {
183
184
  t.tryStatement(t.blockStatement(tryBody), buildErrorCatch('Record not found.')),
184
185
  ]), false, true);
185
186
  }
186
- function buildMutationHandler(table, operation, targetName) {
187
+ /**
188
+ * Get the set of field names that have defaults in the create input type.
189
+ * Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
190
+ * TypeRegistry and checks each field's defaultValue from introspection.
191
+ */
192
+ function getFieldsWithDefaults(table, typeRegistry) {
193
+ const fieldsWithDefaults = new Set();
194
+ if (!typeRegistry)
195
+ return fieldsWithDefaults;
196
+ // Look up the CreateXInput type (e.g. CreateDatabaseInput)
197
+ const createInputTypeName = (0, utils_2.getCreateInputTypeName)(table);
198
+ const createInputType = typeRegistry.get(createInputTypeName);
199
+ if (!createInputType?.inputFields)
200
+ return fieldsWithDefaults;
201
+ // The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
202
+ // Find the inner input type that contains the actual field definitions
203
+ for (const inputField of createInputType.inputFields) {
204
+ // The inner field's type name is the actual input type (e.g. DatabaseInput)
205
+ const innerTypeName = inputField.type.name
206
+ || inputField.type.ofType?.name
207
+ || inputField.type.ofType?.ofType?.name;
208
+ if (!innerTypeName)
209
+ continue;
210
+ const innerType = typeRegistry.get(innerTypeName);
211
+ if (!innerType?.inputFields)
212
+ continue;
213
+ // Check each field in the inner input type for defaultValue
214
+ for (const field of innerType.inputFields) {
215
+ if (field.defaultValue !== undefined) {
216
+ fieldsWithDefaults.add(field.name);
217
+ }
218
+ // Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
219
+ if (field.type.kind !== 'NON_NULL') {
220
+ fieldsWithDefaults.add(field.name);
221
+ }
222
+ }
223
+ }
224
+ return fieldsWithDefaults;
225
+ }
226
+ function buildMutationHandler(table, operation, targetName, typeRegistry) {
187
227
  const { singularName } = (0, utils_1.getTableNames)(table);
188
228
  const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
189
229
  const pk = pkFields[0];
@@ -191,6 +231,8 @@ function buildMutationHandler(table, operation, targetName) {
191
231
  f.name !== 'nodeId' &&
192
232
  f.name !== 'createdAt' &&
193
233
  f.name !== 'updatedAt');
234
+ // Get fields that have defaults from introspection (for create operations)
235
+ const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
194
236
  const questions = [];
195
237
  if (operation === 'update' || operation === 'delete') {
196
238
  questions.push(t.objectExpression([
@@ -202,11 +244,14 @@ function buildMutationHandler(table, operation, targetName) {
202
244
  }
203
245
  if (operation !== 'delete') {
204
246
  for (const field of editableFields) {
247
+ // For create: field is required only if it has no default value
248
+ // For update: all fields are optional (user only updates what they want)
249
+ const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
205
250
  questions.push(t.objectExpression([
206
251
  t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
207
252
  t.objectProperty(t.identifier('name'), t.stringLiteral(field.name)),
208
253
  t.objectProperty(t.identifier('message'), t.stringLiteral(field.name)),
209
- t.objectProperty(t.identifier('required'), t.booleanLiteral(operation === 'create')),
254
+ t.objectProperty(t.identifier('required'), t.booleanLiteral(isRequired)),
210
255
  ]));
211
256
  }
212
257
  }
@@ -365,9 +410,9 @@ function generateTableCommand(table, options) {
365
410
  const tn = options?.targetName;
366
411
  statements.push(buildListHandler(table, tn));
367
412
  statements.push(buildGetHandler(table, tn));
368
- statements.push(buildMutationHandler(table, 'create', tn));
369
- statements.push(buildMutationHandler(table, 'update', tn));
370
- statements.push(buildMutationHandler(table, 'delete', tn));
413
+ statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
414
+ statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
415
+ statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
371
416
  const header = (0, utils_1.getGeneratedFileHeader)(`CLI commands for ${table.name}`);
372
417
  const code = (0, babel_ast_1.generateCode)(statements);
373
418
  return {
@@ -8,3 +8,11 @@ import type { GeneratedFile } from './executor-generator';
8
8
  * and mutation input parsing.
9
9
  */
10
10
  export declare function generateUtilsFile(): GeneratedFile;
11
+ /**
12
+ * Generate a node-fetch.ts file with NodeHttpAdapter for CLI.
13
+ *
14
+ * Provides a GraphQLAdapter implementation using node:http/node:https
15
+ * instead of the Fetch API. This cleanly handles *.localhost subdomain
16
+ * routing (DNS resolution + Host header) without any global patching.
17
+ */
18
+ export declare function generateNodeFetchFile(): GeneratedFile;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.generateUtilsFile = generateUtilsFile;
37
+ exports.generateNodeFetchFile = generateNodeFetchFile;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const utils_1 = require("../utils");
@@ -76,3 +77,16 @@ function generateUtilsFile() {
76
77
  content: readTemplateFile('cli-utils.ts', 'CLI utility functions for type coercion and input handling'),
77
78
  };
78
79
  }
80
+ /**
81
+ * Generate a node-fetch.ts file with NodeHttpAdapter for CLI.
82
+ *
83
+ * Provides a GraphQLAdapter implementation using node:http/node:https
84
+ * instead of the Fetch API. This cleanly handles *.localhost subdomain
85
+ * routing (DNS resolution + Host header) without any global patching.
86
+ */
87
+ function generateNodeFetchFile() {
88
+ return {
89
+ fileName: 'node-fetch.ts',
90
+ content: readTemplateFile('node-fetch.ts', 'Node HTTP adapter for localhost subdomain routing'),
91
+ };
92
+ }
@@ -25,4 +25,6 @@ export declare function generateSelectTypesFile(): GeneratedClientFile;
25
25
  /**
26
26
  * Generate the main index.ts with createClient factory
27
27
  */
28
- export declare function generateCreateClientFile(tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean): GeneratedClientFile;
28
+ export declare function generateCreateClientFile(tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean, options?: {
29
+ nodeHttpAdapter?: boolean;
30
+ }): GeneratedClientFile;
@@ -114,7 +114,7 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
114
114
  /**
115
115
  * Generate the main index.ts with createClient factory
116
116
  */
117
- function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations) {
117
+ function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations, options) {
118
118
  const statements = [];
119
119
  // Add imports
120
120
  // Import OrmClient (value) and OrmClientConfig (type) separately
@@ -156,6 +156,12 @@ function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations)
156
156
  statements.push(t.exportAllDeclaration(t.stringLiteral('./select-types')));
157
157
  // Re-export all models
158
158
  statements.push(t.exportAllDeclaration(t.stringLiteral('./models')));
159
+ // Re-export NodeHttpAdapter when enabled (for use in any Node.js application)
160
+ if (options?.nodeHttpAdapter) {
161
+ statements.push(t.exportNamedDeclaration(null, [
162
+ t.exportSpecifier(t.identifier('NodeHttpAdapter'), t.identifier('NodeHttpAdapter')),
163
+ ], t.stringLiteral('./node-fetch')));
164
+ }
159
165
  // Re-export custom operations
160
166
  if (hasCustomQueries) {
161
167
  statements.push(t.exportNamedDeclaration(null, [
@@ -91,7 +91,7 @@ function generateOrm(options) {
91
91
  const typesBarrel = (0, barrel_1.generateTypesBarrel)(useSharedTypes);
92
92
  files.push({ path: typesBarrel.fileName, content: typesBarrel.content });
93
93
  // 7. Generate main index.ts with createClient
94
- const indexFile = (0, client_generator_1.generateCreateClientFile)(tables, hasCustomQueries, hasCustomMutations);
94
+ const indexFile = (0, client_generator_1.generateCreateClientFile)(tables, hasCustomQueries, hasCustomMutations, { nodeHttpAdapter: !!options.config.nodeHttpAdapter });
95
95
  files.push({ path: indexFile.fileName, content: indexFile.content });
96
96
  return {
97
97
  files,
@@ -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),
@@ -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,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;
@@ -40,3 +40,16 @@ 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
+ }
@@ -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;
@@ -75,7 +75,7 @@ function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false
75
75
  /**
76
76
  * Generate the main index.ts with createClient factory
77
77
  */
78
- export function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations) {
78
+ export function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations, options) {
79
79
  const statements = [];
80
80
  // Add imports
81
81
  // Import OrmClient (value) and OrmClientConfig (type) separately
@@ -117,6 +117,12 @@ export function generateCreateClientFile(tables, hasCustomQueries, hasCustomMuta
117
117
  statements.push(t.exportAllDeclaration(t.stringLiteral('./select-types')));
118
118
  // Re-export all models
119
119
  statements.push(t.exportAllDeclaration(t.stringLiteral('./models')));
120
+ // Re-export NodeHttpAdapter when enabled (for use in any Node.js application)
121
+ if (options?.nodeHttpAdapter) {
122
+ statements.push(t.exportNamedDeclaration(null, [
123
+ t.exportSpecifier(t.identifier('NodeHttpAdapter'), t.identifier('NodeHttpAdapter')),
124
+ ], t.stringLiteral('./node-fetch')));
125
+ }
120
126
  // Re-export custom operations
121
127
  if (hasCustomQueries) {
122
128
  statements.push(t.exportNamedDeclaration(null, [
@@ -87,7 +87,7 @@ export function generateOrm(options) {
87
87
  const typesBarrel = generateTypesBarrel(useSharedTypes);
88
88
  files.push({ path: typesBarrel.fileName, content: typesBarrel.content });
89
89
  // 7. Generate main index.ts with createClient
90
- const indexFile = generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations);
90
+ const indexFile = generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations, { nodeHttpAdapter: !!options.config.nodeHttpAdapter });
91
91
  files.push({ path: indexFile.fileName, content: indexFile.content });
92
92
  return {
93
93
  files,
@@ -34,6 +34,9 @@ export async function generate(options = {}, internalOptions) {
34
34
  const runReactQuery = config.reactQuery ?? false;
35
35
  const runCli = internalOptions?.skipCli ? false : !!config.cli;
36
36
  const runOrm = runReactQuery || !!config.cli || (options.orm !== undefined ? !!options.orm : false);
37
+ // Auto-enable nodeHttpAdapter when CLI is enabled, unless explicitly set to false
38
+ const useNodeHttpAdapter = options.nodeHttpAdapter === true ||
39
+ (runCli && options.nodeHttpAdapter !== false);
37
40
  if (!options.schemaOnly && !runReactQuery && !runOrm && !runCli) {
38
41
  return {
39
42
  success: false,
@@ -167,13 +170,22 @@ export async function generate(options = {}, internalOptions) {
167
170
  mutations: customOperations.mutations,
168
171
  typeRegistry: customOperations.typeRegistry,
169
172
  },
170
- config,
173
+ config: { ...config, nodeHttpAdapter: useNodeHttpAdapter },
171
174
  sharedTypesPath: bothEnabled ? '..' : undefined,
172
175
  });
173
176
  filesToWrite.push(...files.map((file) => ({
174
177
  ...file,
175
178
  path: path.posix.join('orm', file.path),
176
179
  })));
180
+ // Generate NodeHttpAdapter in ORM output when enabled
181
+ if (useNodeHttpAdapter) {
182
+ const { generateNodeFetchFile } = await import('./codegen/cli/utils-generator');
183
+ const nodeFetchFile = generateNodeFetchFile();
184
+ filesToWrite.push({
185
+ path: path.posix.join('orm', nodeFetchFile.fileName),
186
+ content: nodeFetchFile.content,
187
+ });
188
+ }
177
189
  }
178
190
  // Generate CLI commands
179
191
  if (runCli) {
@@ -184,7 +196,7 @@ export async function generate(options = {}, internalOptions) {
184
196
  queries: customOperations.queries,
185
197
  mutations: customOperations.mutations,
186
198
  },
187
- config,
199
+ config: { ...config, nodeHttpAdapter: useNodeHttpAdapter },
188
200
  });
189
201
  filesToWrite.push(...files.map((file) => ({
190
202
  path: path.posix.join('cli', file.fileName),
@@ -510,10 +522,16 @@ export async function generateMulti(options) {
510
522
  if (useUnifiedCli && cliTargets.length > 0 && !dryRun) {
511
523
  const cliConfig = typeof unifiedCli === 'object' ? unifiedCli : {};
512
524
  const toolName = cliConfig.toolName ?? 'app';
525
+ // Auto-enable nodeHttpAdapter for unified CLI unless explicitly disabled
526
+ // Check first target config for explicit nodeHttpAdapter setting
527
+ const firstTargetConfig = configs[names[0]];
528
+ const multiNodeHttpAdapter = firstTargetConfig?.nodeHttpAdapter === true ||
529
+ (firstTargetConfig?.nodeHttpAdapter !== false);
513
530
  const { files } = generateMultiTargetCli({
514
531
  toolName,
515
532
  builtinNames: cliConfig.builtinNames,
516
533
  targets: cliTargets,
534
+ nodeHttpAdapter: multiNodeHttpAdapter,
517
535
  });
518
536
  const cliFilesToWrite = files.map((file) => ({
519
537
  path: path.posix.join('cli', file.fileName),
@@ -278,6 +278,24 @@ export interface GraphQLSDKConfigTarget {
278
278
  * @default false
279
279
  */
280
280
  orm?: boolean;
281
+ /**
282
+ * Enable NodeHttpAdapter generation for Node.js applications.
283
+ * When true, generates a node-fetch.ts with NodeHttpAdapter (implements GraphQLAdapter)
284
+ * using node:http/node:https for requests, enabling local development
285
+ * with subdomain-based routing (e.g. auth.localhost:3000).
286
+ * No global patching — the adapter is passed to createClient via the adapter option.
287
+ *
288
+ * When CLI generation is enabled (`cli: true`), this is auto-enabled unless
289
+ * explicitly set to `false`.
290
+ *
291
+ * Can also be used standalone with the ORM client for any Node.js application:
292
+ * ```ts
293
+ * import { NodeHttpAdapter } from './orm/node-fetch';
294
+ * const db = createClient({ adapter: new NodeHttpAdapter(endpoint, headers) });
295
+ * ```
296
+ * @default false
297
+ */
298
+ nodeHttpAdapter?: boolean;
281
299
  /**
282
300
  * Whether to generate React Query hooks
283
301
  * When enabled, generates React Query hooks to {output}/hooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -56,24 +56,24 @@
56
56
  "@0no-co/graphql.web": "^1.1.2",
57
57
  "@babel/generator": "^7.28.6",
58
58
  "@babel/types": "^7.28.6",
59
- "@constructive-io/graphql-types": "^3.0.0",
59
+ "@constructive-io/graphql-types": "^3.1.0",
60
60
  "@inquirerer/utils": "^3.2.3",
61
- "@pgpmjs/core": "^6.1.0",
61
+ "@pgpmjs/core": "^6.2.0",
62
62
  "ajv": "^8.17.1",
63
63
  "deepmerge": "^4.3.1",
64
64
  "find-and-require-package-json": "^0.9.0",
65
- "gql-ast": "^3.0.0",
66
- "graphile-schema": "^1.1.1",
65
+ "gql-ast": "^3.1.0",
66
+ "graphile-schema": "^1.2.0",
67
67
  "graphql": "^16.9.0",
68
68
  "inflekt": "^0.3.1",
69
69
  "inquirerer": "^4.4.0",
70
70
  "jiti": "^2.6.1",
71
71
  "komoji": "^0.8.0",
72
72
  "oxfmt": "^0.26.0",
73
- "pg-cache": "^3.0.0",
74
- "pg-env": "^1.4.0",
75
- "pgsql-client": "^3.1.0",
76
- "pgsql-seed": "^2.1.0",
73
+ "pg-cache": "^3.1.0",
74
+ "pg-env": "^1.5.0",
75
+ "pgsql-client": "^3.2.0",
76
+ "pgsql-seed": "^2.2.0",
77
77
  "undici": "^7.19.0"
78
78
  },
79
79
  "peerDependencies": {
@@ -100,5 +100,5 @@
100
100
  "tsx": "^4.21.0",
101
101
  "typescript": "^5.9.3"
102
102
  },
103
- "gitHead": "b4e199d40e7495f25bafb6e8d318e731bd19b32c"
103
+ "gitHead": "b758178b808ce0bf451e86c0bd7e92079155db7c"
104
104
  }
package/types/config.d.ts CHANGED
@@ -278,6 +278,24 @@ export interface GraphQLSDKConfigTarget {
278
278
  * @default false
279
279
  */
280
280
  orm?: boolean;
281
+ /**
282
+ * Enable NodeHttpAdapter generation for Node.js applications.
283
+ * When true, generates a node-fetch.ts with NodeHttpAdapter (implements GraphQLAdapter)
284
+ * using node:http/node:https for requests, enabling local development
285
+ * with subdomain-based routing (e.g. auth.localhost:3000).
286
+ * No global patching — the adapter is passed to createClient via the adapter option.
287
+ *
288
+ * When CLI generation is enabled (`cli: true`), this is auto-enabled unless
289
+ * explicitly set to `false`.
290
+ *
291
+ * Can also be used standalone with the ORM client for any Node.js application:
292
+ * ```ts
293
+ * import { NodeHttpAdapter } from './orm/node-fetch';
294
+ * const db = createClient({ adapter: new NodeHttpAdapter(endpoint, headers) });
295
+ * ```
296
+ * @default false
297
+ */
298
+ nodeHttpAdapter?: boolean;
281
299
  /**
282
300
  * Whether to generate React Query hooks
283
301
  * When enabled, generates React Query hooks to {output}/hooks