From 87b67d2c1305ec3953780f5523b1c5a4aa09f493 Mon Sep 17 00:00:00 2001 From: unknown <> Date: Tue, 24 Feb 2026 09:34:31 +0000 Subject: [PATCH 1/5] fix(codegen): upstream CLI fixes - no-arg query signature, field defaults, localhost adapter 1. Fix query call signature for no-arg queries (e.g. currentUser) - buildOrmCustomCall now skips empty args object and passes {select} as single param - Fixes: .currentUser({}, {select}) -> .currentUser({select}) 2. Mark fields with defaults as not-required in create operations - Uses TypeRegistry to look up CREATE input type's defaultValue from introspection - Fields with defaults or nullable types are marked required: false - Passed via new typeRegistry option on TableCommandOptions 3. Add localhost fetch adapter for *.localhost subdomain routing - New template: localhost-fetch.ts patches globalThis.fetch using node:http.request - Enables local dev with subdomain routing (e.g. auth.localhost:3000) - Configurable via cli.localhostAdapter option in CliConfig - Auto-imported in executor when enabled --- .../__snapshots__/cli-generator.test.ts.snap | 2 +- .../codegen/cli/custom-command-generator.ts | 19 ++- .../core/codegen/cli/executor-generator.ts | 22 ++- graphql/codegen/src/core/codegen/cli/index.ts | 33 +++- .../codegen/cli/table-command-generator.ts | 64 +++++++- .../src/core/codegen/cli/utils-generator.ts | 17 ++ .../core/codegen/templates/localhost-fetch.ts | 147 ++++++++++++++++++ graphql/codegen/src/types/config.ts | 9 ++ 8 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 graphql/codegen/src/core/codegen/templates/localhost-fetch.ts diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap index 6deecd43d..a7c4d9260 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap @@ -1153,7 +1153,7 @@ export default async (argv: Partial>, prompter: Inquirer } const client = getClient(); const selectFields = buildSelectFromPaths(argv.select ?? ""); - const result = await client.query.currentUser({}, { + const result = await client.query.currentUser({ select: selectFields }).execute(); console.log(JSON.stringify(result, null, 2)); diff --git a/graphql/codegen/src/core/codegen/cli/custom-command-generator.ts b/graphql/codegen/src/core/codegen/cli/custom-command-generator.ts index f374361e6..72ee42480 100644 --- a/graphql/codegen/src/core/codegen/cli/custom-command-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/custom-command-generator.ts @@ -143,9 +143,21 @@ function buildOrmCustomCall( opName: string, argsExpr: t.Expression, selectExpr?: t.Expression, + hasArgs: boolean = true, ): t.Expression { - const callArgs: t.Expression[] = [argsExpr]; - if (selectExpr) { + const callArgs: t.Expression[] = []; + if (hasArgs) { + // Operation has arguments: pass args as first param, select as second + callArgs.push(argsExpr); + if (selectExpr) { + callArgs.push( + t.objectExpression([ + t.objectProperty(t.identifier('select'), selectExpr), + ]), + ); + } + } else if (selectExpr) { + // No arguments: pass { select } as the only param (ORM signature) callArgs.push( t.objectExpression([ t.objectProperty(t.identifier('select'), selectExpr), @@ -343,12 +355,13 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman selectExpr = t.identifier('selectFields'); } + const hasArgs = op.args.length > 0; bodyStatements.push( t.variableDeclaration('const', [ t.variableDeclarator( t.identifier('result'), t.awaitExpression( - buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr), + buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs), ), ), ]), diff --git a/graphql/codegen/src/core/codegen/cli/executor-generator.ts b/graphql/codegen/src/core/codegen/cli/executor-generator.ts index e5a462f53..20295de62 100644 --- a/graphql/codegen/src/core/codegen/cli/executor-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/executor-generator.ts @@ -30,9 +30,21 @@ function createImportDeclaration( return decl; } -export function generateExecutorFile(toolName: string): GeneratedFile { +export interface ExecutorOptions { + /** Enable localhost fetch adapter import */ + localhostAdapter?: boolean; +} + +export function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile { const statements: t.Statement[] = []; + // Import localhost adapter first (side-effect import patches globalThis.fetch) + if (options?.localhostAdapter) { + statements.push( + t.importDeclaration([], t.stringLiteral('./localhost-fetch')), + ); + } + statements.push( createImportDeclaration('appstash', ['createConfigStore']), ); @@ -226,9 +238,17 @@ export function generateExecutorFile(toolName: string): GeneratedFile { export function generateMultiTargetExecutorFile( toolName: string, targets: MultiTargetExecutorInput[], + options?: ExecutorOptions, ): GeneratedFile { const statements: t.Statement[] = []; + // Import localhost adapter first (side-effect import patches globalThis.fetch) + if (options?.localhostAdapter) { + statements.push( + t.importDeclaration([], t.stringLiteral('./localhost-fetch')), + ); + } + statements.push( createImportDeclaration('appstash', ['createConfigStore']), ); diff --git a/graphql/codegen/src/core/codegen/cli/index.ts b/graphql/codegen/src/core/codegen/cli/index.ts index 81c287d52..15b35fe42 100644 --- a/graphql/codegen/src/core/codegen/cli/index.ts +++ b/graphql/codegen/src/core/codegen/cli/index.ts @@ -1,5 +1,5 @@ import type { BuiltinNames, GraphQLSDKConfigTarget } from '../../../types/config'; -import type { CleanOperation, CleanTable } from '../../../types/schema'; +import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema'; import { generateCommandMap, generateMultiTargetCommandMap } from './command-map-generator'; import { generateCustomCommand } from './custom-command-generator'; import { generateExecutorFile, generateMultiTargetExecutorFile } from './executor-generator'; @@ -11,7 +11,7 @@ import { generateMultiTargetContextCommand, } from './infra-generator'; import { generateTableCommand } from './table-command-generator'; -import { generateUtilsFile } from './utils-generator'; +import { generateUtilsFile, generateLocalhostFetchFile } from './utils-generator'; export interface GenerateCliOptions { tables: CleanTable[]; @@ -20,6 +20,8 @@ export interface GenerateCliOptions { mutations: CleanOperation[]; }; config: GraphQLSDKConfigTarget; + /** TypeRegistry from introspection, used to check field defaults */ + typeRegistry?: TypeRegistry; } export interface GenerateCliResult { @@ -43,12 +45,19 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { ? cliConfig.toolName : 'app'; - const executorFile = generateExecutorFile(toolName); + const useLocalhostAdapter = typeof cliConfig === 'object' && !!cliConfig.localhostAdapter; + + const executorFile = generateExecutorFile(toolName, { localhostAdapter: useLocalhostAdapter }); files.push(executorFile); const utilsFile = generateUtilsFile(); files.push(utilsFile); + // Generate localhost adapter if configured + if (useLocalhostAdapter) { + files.push(generateLocalhostFetchFile()); + } + const contextFile = generateContextCommand(toolName); files.push(contextFile); @@ -56,7 +65,9 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { files.push(authFile); for (const table of tables) { - const tableFile = generateTableCommand(table); + const tableFile = generateTableCommand(table, { + typeRegistry: options.typeRegistry, + }); files.push(tableFile); } @@ -99,12 +110,16 @@ export interface MultiTargetCliTarget { mutations: CleanOperation[]; }; isAuthTarget?: boolean; + /** TypeRegistry from introspection, used to check field defaults */ + typeRegistry?: TypeRegistry; } export interface GenerateMultiTargetCliOptions { toolName: string; builtinNames?: BuiltinNames; targets: MultiTargetCliTarget[]; + /** Enable localhost fetch adapter for *.localhost subdomain routing */ + localhostAdapter?: boolean; } export function resolveBuiltinNames( @@ -138,12 +153,19 @@ export function generateMultiTargetCli( endpoint: t.endpoint, ormImportPath: t.ormImportPath, })); - const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs); + const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, { + localhostAdapter: !!options.localhostAdapter, + }); files.push(executorFile); const utilsFile = generateUtilsFile(); files.push(utilsFile); + // Generate localhost adapter if configured + if (options.localhostAdapter) { + files.push(generateLocalhostFetchFile()); + } + const contextFile = generateMultiTargetContextCommand( toolName, builtinNames.context, @@ -174,6 +196,7 @@ export function generateMultiTargetCli( const tableFile = generateTableCommand(table, { targetName: target.name, executorImportPath: '../../executor', + typeRegistry: target.typeRegistry, }); files.push(tableFile); } diff --git a/graphql/codegen/src/core/codegen/cli/table-command-generator.ts b/graphql/codegen/src/core/codegen/cli/table-command-generator.ts index 37274861e..e0acbf522 100644 --- a/graphql/codegen/src/core/codegen/cli/table-command-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/table-command-generator.ts @@ -9,8 +9,9 @@ import { getTableNames, ucFirst, } from '../utils'; -import type { CleanTable } from '../../../types/schema'; +import type { CleanTable, TypeRegistry } from '../../../types/schema'; import type { GeneratedFile } from './executor-generator'; +import { getCreateInputTypeName } from '../utils'; function createImportDeclaration( moduleSpecifier: string, @@ -334,10 +335,55 @@ function buildGetHandler(table: CleanTable, targetName?: string): t.FunctionDecl ); } +/** + * Get the set of field names that have defaults in the create input type. + * Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the + * TypeRegistry and checks each field's defaultValue from introspection. + */ +function getFieldsWithDefaults( + table: CleanTable, + typeRegistry?: TypeRegistry, +): Set { + const fieldsWithDefaults = new Set(); + if (!typeRegistry) return fieldsWithDefaults; + + // Look up the CreateXInput type (e.g. CreateDatabaseInput) + const createInputTypeName = getCreateInputTypeName(table); + const createInputType = typeRegistry.get(createInputTypeName); + if (!createInputType?.inputFields) return fieldsWithDefaults; + + // The CreateXInput has an inner field (e.g. "database" of type DatabaseInput) + // Find the inner input type that contains the actual field definitions + for (const inputField of createInputType.inputFields) { + // The inner field's type name is the actual input type (e.g. DatabaseInput) + const innerTypeName = inputField.type.name + || inputField.type.ofType?.name + || inputField.type.ofType?.ofType?.name; + if (!innerTypeName) continue; + + const innerType = typeRegistry.get(innerTypeName); + if (!innerType?.inputFields) continue; + + // Check each field in the inner input type for defaultValue + for (const field of innerType.inputFields) { + if (field.defaultValue !== undefined) { + fieldsWithDefaults.add(field.name); + } + // Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional) + if (field.type.kind !== 'NON_NULL') { + fieldsWithDefaults.add(field.name); + } + } + } + + return fieldsWithDefaults; +} + function buildMutationHandler( table: CleanTable, operation: 'create' | 'update' | 'delete', targetName?: string, + typeRegistry?: TypeRegistry, ): t.FunctionDeclaration { const { singularName } = getTableNames(table); const pkFields = getPrimaryKeyInfo(table); @@ -351,6 +397,9 @@ function buildMutationHandler( f.name !== 'updatedAt', ); + // Get fields that have defaults from introspection (for create operations) + const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry); + const questions: t.Expression[] = []; if (operation === 'update' || operation === 'delete') { @@ -366,6 +415,9 @@ function buildMutationHandler( if (operation !== 'delete') { for (const field of editableFields) { + // For create: field is required only if it has no default value + // For update: all fields are optional (user only updates what they want) + const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name); questions.push( t.objectExpression([ t.objectProperty(t.identifier('type'), t.stringLiteral('text')), @@ -379,7 +431,7 @@ function buildMutationHandler( ), t.objectProperty( t.identifier('required'), - t.booleanLiteral(operation === 'create'), + t.booleanLiteral(isRequired), ), ]), ); @@ -530,6 +582,8 @@ function buildMutationHandler( export interface TableCommandOptions { targetName?: string; executorImportPath?: string; + /** TypeRegistry from introspection, used to check field defaults */ + typeRegistry?: TypeRegistry; } export function generateTableCommand(table: CleanTable, options?: TableCommandOptions): GeneratedFile { @@ -741,9 +795,9 @@ export function generateTableCommand(table: CleanTable, options?: TableCommandOp const tn = options?.targetName; statements.push(buildListHandler(table, tn)); statements.push(buildGetHandler(table, tn)); - statements.push(buildMutationHandler(table, 'create', tn)); - statements.push(buildMutationHandler(table, 'update', tn)); - statements.push(buildMutationHandler(table, 'delete', tn)); + statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry)); + statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry)); + statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry)); const header = getGeneratedFileHeader(`CLI commands for ${table.name}`); const code = generateCode(statements); diff --git a/graphql/codegen/src/core/codegen/cli/utils-generator.ts b/graphql/codegen/src/core/codegen/cli/utils-generator.ts index a1dd4d52c..21993d530 100644 --- a/graphql/codegen/src/core/codegen/cli/utils-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/utils-generator.ts @@ -59,3 +59,20 @@ export function generateUtilsFile(): GeneratedFile { ), }; } + +/** + * Generate a localhost-fetch.ts file that patches globalThis.fetch for *.localhost URLs. + * This enables seamless local development with subdomain routing (e.g. auth.localhost:3000). + * + * Node.js cannot resolve *.localhost subdomains and the Fetch API forbids the Host header. + * This adapter uses node:http.request to proxy requests with proper Host headers. + */ +export function generateLocalhostFetchFile(): GeneratedFile { + return { + fileName: 'localhost-fetch.ts', + content: readTemplateFile( + 'localhost-fetch.ts', + 'Localhost fetch adapter — patches globalThis.fetch for *.localhost subdomain routing', + ), + }; +} diff --git a/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts b/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts new file mode 100644 index 000000000..8f102a49e --- /dev/null +++ b/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts @@ -0,0 +1,147 @@ +/** + * Localhost fetch adapter for CLI + * + * Node.js cannot resolve *.localhost subdomains (ENOTFOUND) — only browsers do. + * The Constructive GraphQL server uses Host-header-based subdomain routing + * (enableServicesApi), so requests must carry the correct Host header. + * + * The Fetch API treats "Host" as a **forbidden request header** and silently + * drops it. This module patches globalThis.fetch to use node:http.request for + * *.localhost URLs, giving full control over the Host header. + * + * This patch is automatically applied when the CLI is generated with localhost + * adapter support, enabling seamless local development with subdomain routing. + * + * NOTE: This file is read at codegen time and written to output. + * Any changes here will affect all generated CLI localhost adapters. + */ + +import http from 'node:http'; + +const _origFetch = globalThis.fetch; + +/** + * Convert a Headers/object/array to a plain object for http.request. + */ +function headersToObject( + headers: HeadersInit | undefined, +): Record { + if (!headers) return {}; + if (headers instanceof Headers) { + const obj: Record = {}; + headers.forEach((v, k) => { + obj[k] = v; + }); + return obj; + } + if (Array.isArray(headers)) { + const obj: Record = {}; + for (const entry of headers) { + if (Array.isArray(entry)) { + obj[entry[0]] = entry[1]; + } + } + return obj; + } + if (typeof headers === 'object') return { ...headers }; + return {}; +} + +/** + * Perform the request using http.request so we can set the Host header freely. + * Returns a spec-compliant Response object. + */ +function httpFetch(url: URL, init: RequestInit | undefined): Promise { + return new Promise((resolve, reject) => { + const method = init?.method || 'GET'; + const hdrs = headersToObject(init?.headers); + + const opts = { + hostname: url.hostname, // already rewritten to "localhost" + port: url.port || 80, + path: url.pathname + url.search, + method, + headers: hdrs, + }; + + const req = http.request(opts, (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk: Buffer) => chunks.push(chunk)); + res.on('end', () => { + const body = Buffer.concat(chunks); + const respHeaders = new Headers(); + for (const [k, v] of Object.entries(res.headers)) { + if (v != null) { + if (Array.isArray(v)) { + v.forEach((val) => respHeaders.append(k, val)); + } else { + respHeaders.set(k, String(v)); + } + } + } + resolve( + new Response(body, { + status: res.statusCode, + statusText: res.statusMessage, + headers: respHeaders, + }), + ); + }); + }); + + req.on('error', reject); + + if (init?.body != null) { + if (typeof init.body === 'string' || Buffer.isBuffer(init.body)) { + req.write(init.body); + } else if (init.body instanceof Uint8Array) { + req.write(Buffer.from(init.body)); + } + } + + req.end(); + }); +} + +globalThis.fetch = function patchedFetch( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + let urlStr: string; + if (typeof input === 'string') { + urlStr = input; + } else if (input instanceof URL) { + urlStr = input.href; + } else if (input instanceof Request) { + urlStr = input.url; + if (!init) init = {}; + if (!init.method && input.method) init.method = input.method; + if (!init.headers && input.headers) init.headers = input.headers; + } else { + return _origFetch(input, init); + } + + let url: URL; + try { + url = new URL(urlStr); + } catch { + return _origFetch(input, init); + } + + // Only patch *.localhost subdomains (not bare "localhost") + if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') { + const hostValue = url.host; // e.g. "auth.localhost:3000" + url.hostname = 'localhost'; // rewrite so Node.js can connect + + // Inject Host header for server-side subdomain routing + init = init ? { ...init } : {}; + const hdrs = headersToObject(init.headers); + hdrs['host'] = hostValue; + init.headers = hdrs; + + // Use http.request which allows the Host header (fetch API forbids it) + return httpFetch(url, init); + } + + return _origFetch(input, init); +}; diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts index 0ca13a680..c1b1c7ea1 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -199,6 +199,15 @@ export interface CliConfig { * context -> 'context' (renamed to 'env' on collision) */ builtinNames?: BuiltinNames; + + /** + * Enable localhost fetch adapter for *.localhost subdomain routing. + * When true, generates a localhost-fetch.ts that patches globalThis.fetch + * to use node:http.request for *.localhost URLs, enabling local development + * with subdomain-based routing (e.g. auth.localhost:3000). + * @default false + */ + localhostAdapter?: boolean; } /** From e4e20e84ed541c27aa096afe553026ef799f72d2 Mon Sep 17 00:00:00 2001 From: unknown <> Date: Tue, 24 Feb 2026 10:22:14 +0000 Subject: [PATCH 2/5] refactor(codegen): replace global fetch patch with NodeHttpAdapter Replaces the localhost-fetch.ts global monkey-patch with a clean NodeHttpAdapter class that implements GraphQLAdapter using node:http/https. - No global patching of globalThis.fetch - NodeHttpAdapter handles *.localhost DNS + Host header internally - Executor passes adapter to createClient instead of side-effect import - Follows same pattern as FetchAdapter in orm-client.ts - Follows same http.request pattern as fetch-schema.ts --- .../core/codegen/cli/executor-generator.ts | 84 ++++++--- graphql/codegen/src/core/codegen/cli/index.ts | 10 +- .../src/core/codegen/cli/utils-generator.ts | 16 +- .../core/codegen/templates/localhost-fetch.ts | 147 ---------------- .../src/core/codegen/templates/node-fetch.ts | 162 ++++++++++++++++++ 5 files changed, 236 insertions(+), 183 deletions(-) delete mode 100644 graphql/codegen/src/core/codegen/templates/localhost-fetch.ts create mode 100644 graphql/codegen/src/core/codegen/templates/node-fetch.ts diff --git a/graphql/codegen/src/core/codegen/cli/executor-generator.ts b/graphql/codegen/src/core/codegen/cli/executor-generator.ts index 20295de62..18abef775 100644 --- a/graphql/codegen/src/core/codegen/cli/executor-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/executor-generator.ts @@ -31,17 +31,17 @@ function createImportDeclaration( } export interface ExecutorOptions { - /** Enable localhost fetch adapter import */ + /** Enable NodeHttpAdapter for *.localhost subdomain routing */ localhostAdapter?: boolean; } export function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile { const statements: t.Statement[] = []; - // Import localhost adapter first (side-effect import patches globalThis.fetch) + // Import NodeHttpAdapter for *.localhost subdomain routing if (options?.localhostAdapter) { statements.push( - t.importDeclaration([], t.stringLiteral('./localhost-fetch')), + createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), ); } @@ -203,20 +203,39 @@ export function generateExecutorFile(toolName: string, options?: ExecutorOptions ]), ), - t.returnStatement( - t.callExpression(t.identifier('createClient'), [ - t.objectExpression([ - t.objectProperty( - t.identifier('endpoint'), - t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')), + // Build createClient config — use NodeHttpAdapter for *.localhost endpoints + ...(options?.localhostAdapter + ? [ + t.returnStatement( + t.callExpression(t.identifier('createClient'), [ + t.objectExpression([ + t.objectProperty( + t.identifier('adapter'), + t.newExpression(t.identifier('NodeHttpAdapter'), [ + t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')), + t.identifier('headers'), + ]), + ), + ]), + ]), ), - t.objectProperty( - t.identifier('headers'), - t.identifier('headers'), + ] + : [ + t.returnStatement( + t.callExpression(t.identifier('createClient'), [ + t.objectExpression([ + t.objectProperty( + t.identifier('endpoint'), + t.memberExpression(t.identifier('ctx'), t.identifier('endpoint')), + ), + t.objectProperty( + t.identifier('headers'), + t.identifier('headers'), + ), + ]), + ]), ), ]), - ]), - ), ]); const getClientFunc = t.functionDeclaration( @@ -242,10 +261,10 @@ export function generateMultiTargetExecutorFile( ): GeneratedFile { const statements: t.Statement[] = []; - // Import localhost adapter first (side-effect import patches globalThis.fetch) + // Import NodeHttpAdapter for *.localhost subdomain routing if (options?.localhostAdapter) { statements.push( - t.importDeclaration([], t.stringLiteral('./localhost-fetch')), + createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), ); } @@ -495,14 +514,33 @@ export function generateMultiTargetExecutorFile( ), ]), ), - t.returnStatement( - t.callExpression(t.identifier('createFn'), [ - t.objectExpression([ - t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')), - t.objectProperty(t.identifier('headers'), t.identifier('headers')), + // Build createClient config — use NodeHttpAdapter for *.localhost endpoints + ...(options?.localhostAdapter + ? [ + t.returnStatement( + t.callExpression(t.identifier('createFn'), [ + t.objectExpression([ + t.objectProperty( + t.identifier('adapter'), + t.newExpression(t.identifier('NodeHttpAdapter'), [ + t.identifier('endpoint'), + t.identifier('headers'), + ]), + ), + ]), + ]), + ), + ] + : [ + t.returnStatement( + t.callExpression(t.identifier('createFn'), [ + t.objectExpression([ + t.objectProperty(t.identifier('endpoint'), t.identifier('endpoint')), + t.objectProperty(t.identifier('headers'), t.identifier('headers')), + ]), + ]), + ), ]), - ]), - ), ]); const getClientFunc = t.functionDeclaration( diff --git a/graphql/codegen/src/core/codegen/cli/index.ts b/graphql/codegen/src/core/codegen/cli/index.ts index 15b35fe42..689487010 100644 --- a/graphql/codegen/src/core/codegen/cli/index.ts +++ b/graphql/codegen/src/core/codegen/cli/index.ts @@ -11,7 +11,7 @@ import { generateMultiTargetContextCommand, } from './infra-generator'; import { generateTableCommand } from './table-command-generator'; -import { generateUtilsFile, generateLocalhostFetchFile } from './utils-generator'; +import { generateUtilsFile, generateNodeFetchFile } from './utils-generator'; export interface GenerateCliOptions { tables: CleanTable[]; @@ -53,9 +53,9 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { const utilsFile = generateUtilsFile(); files.push(utilsFile); - // Generate localhost adapter if configured + // Generate node HTTP adapter if configured (for *.localhost subdomain routing) if (useLocalhostAdapter) { - files.push(generateLocalhostFetchFile()); + files.push(generateNodeFetchFile()); } const contextFile = generateContextCommand(toolName); @@ -161,9 +161,9 @@ export function generateMultiTargetCli( const utilsFile = generateUtilsFile(); files.push(utilsFile); - // Generate localhost adapter if configured + // Generate node HTTP adapter if configured (for *.localhost subdomain routing) if (options.localhostAdapter) { - files.push(generateLocalhostFetchFile()); + files.push(generateNodeFetchFile()); } const contextFile = generateMultiTargetContextCommand( diff --git a/graphql/codegen/src/core/codegen/cli/utils-generator.ts b/graphql/codegen/src/core/codegen/cli/utils-generator.ts index 21993d530..a669132cb 100644 --- a/graphql/codegen/src/core/codegen/cli/utils-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/utils-generator.ts @@ -61,18 +61,18 @@ export function generateUtilsFile(): GeneratedFile { } /** - * Generate a localhost-fetch.ts file that patches globalThis.fetch for *.localhost URLs. - * This enables seamless local development with subdomain routing (e.g. auth.localhost:3000). + * Generate a node-fetch.ts file with NodeHttpAdapter for CLI. * - * Node.js cannot resolve *.localhost subdomains and the Fetch API forbids the Host header. - * This adapter uses node:http.request to proxy requests with proper Host headers. + * Provides a GraphQLAdapter implementation using node:http/node:https + * instead of the Fetch API. This cleanly handles *.localhost subdomain + * routing (DNS resolution + Host header) without any global patching. */ -export function generateLocalhostFetchFile(): GeneratedFile { +export function generateNodeFetchFile(): GeneratedFile { return { - fileName: 'localhost-fetch.ts', + fileName: 'node-fetch.ts', content: readTemplateFile( - 'localhost-fetch.ts', - 'Localhost fetch adapter — patches globalThis.fetch for *.localhost subdomain routing', + 'node-fetch.ts', + 'Node HTTP adapter for localhost subdomain routing', ), }; } diff --git a/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts b/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts deleted file mode 100644 index 8f102a49e..000000000 --- a/graphql/codegen/src/core/codegen/templates/localhost-fetch.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Localhost fetch adapter for CLI - * - * Node.js cannot resolve *.localhost subdomains (ENOTFOUND) — only browsers do. - * The Constructive GraphQL server uses Host-header-based subdomain routing - * (enableServicesApi), so requests must carry the correct Host header. - * - * The Fetch API treats "Host" as a **forbidden request header** and silently - * drops it. This module patches globalThis.fetch to use node:http.request for - * *.localhost URLs, giving full control over the Host header. - * - * This patch is automatically applied when the CLI is generated with localhost - * adapter support, enabling seamless local development with subdomain routing. - * - * NOTE: This file is read at codegen time and written to output. - * Any changes here will affect all generated CLI localhost adapters. - */ - -import http from 'node:http'; - -const _origFetch = globalThis.fetch; - -/** - * Convert a Headers/object/array to a plain object for http.request. - */ -function headersToObject( - headers: HeadersInit | undefined, -): Record { - if (!headers) return {}; - if (headers instanceof Headers) { - const obj: Record = {}; - headers.forEach((v, k) => { - obj[k] = v; - }); - return obj; - } - if (Array.isArray(headers)) { - const obj: Record = {}; - for (const entry of headers) { - if (Array.isArray(entry)) { - obj[entry[0]] = entry[1]; - } - } - return obj; - } - if (typeof headers === 'object') return { ...headers }; - return {}; -} - -/** - * Perform the request using http.request so we can set the Host header freely. - * Returns a spec-compliant Response object. - */ -function httpFetch(url: URL, init: RequestInit | undefined): Promise { - return new Promise((resolve, reject) => { - const method = init?.method || 'GET'; - const hdrs = headersToObject(init?.headers); - - const opts = { - hostname: url.hostname, // already rewritten to "localhost" - port: url.port || 80, - path: url.pathname + url.search, - method, - headers: hdrs, - }; - - const req = http.request(opts, (res) => { - const chunks: Buffer[] = []; - res.on('data', (chunk: Buffer) => chunks.push(chunk)); - res.on('end', () => { - const body = Buffer.concat(chunks); - const respHeaders = new Headers(); - for (const [k, v] of Object.entries(res.headers)) { - if (v != null) { - if (Array.isArray(v)) { - v.forEach((val) => respHeaders.append(k, val)); - } else { - respHeaders.set(k, String(v)); - } - } - } - resolve( - new Response(body, { - status: res.statusCode, - statusText: res.statusMessage, - headers: respHeaders, - }), - ); - }); - }); - - req.on('error', reject); - - if (init?.body != null) { - if (typeof init.body === 'string' || Buffer.isBuffer(init.body)) { - req.write(init.body); - } else if (init.body instanceof Uint8Array) { - req.write(Buffer.from(init.body)); - } - } - - req.end(); - }); -} - -globalThis.fetch = function patchedFetch( - input: RequestInfo | URL, - init?: RequestInit, -): Promise { - let urlStr: string; - if (typeof input === 'string') { - urlStr = input; - } else if (input instanceof URL) { - urlStr = input.href; - } else if (input instanceof Request) { - urlStr = input.url; - if (!init) init = {}; - if (!init.method && input.method) init.method = input.method; - if (!init.headers && input.headers) init.headers = input.headers; - } else { - return _origFetch(input, init); - } - - let url: URL; - try { - url = new URL(urlStr); - } catch { - return _origFetch(input, init); - } - - // Only patch *.localhost subdomains (not bare "localhost") - if (url.hostname.endsWith('.localhost') && url.hostname !== 'localhost') { - const hostValue = url.host; // e.g. "auth.localhost:3000" - url.hostname = 'localhost'; // rewrite so Node.js can connect - - // Inject Host header for server-side subdomain routing - init = init ? { ...init } : {}; - const hdrs = headersToObject(init.headers); - hdrs['host'] = hostValue; - init.headers = hdrs; - - // Use http.request which allows the Host header (fetch API forbids it) - return httpFetch(url, init); - } - - return _origFetch(input, init); -}; diff --git a/graphql/codegen/src/core/codegen/templates/node-fetch.ts b/graphql/codegen/src/core/codegen/templates/node-fetch.ts new file mode 100644 index 000000000..cc51bcce4 --- /dev/null +++ b/graphql/codegen/src/core/codegen/templates/node-fetch.ts @@ -0,0 +1,162 @@ +/** + * Node HTTP Adapter for CLI + * + * Implements the GraphQLAdapter interface using node:http / node:https + * instead of the Fetch API. This solves two Node.js limitations: + * + * 1. DNS: Node.js cannot resolve *.localhost subdomains (ENOTFOUND). + * Browsers handle this automatically, but Node requires manual resolution. + * + * 2. Host header: The Fetch API treats "Host" as a forbidden request header + * and silently drops it. The Constructive GraphQL server uses Host-header + * subdomain routing (enableServicesApi), so this header must be preserved. + * + * By using node:http.request directly, both issues are bypassed cleanly + * without any global patching. + * + * NOTE: This file is read at codegen time and written to output. + * Any changes here will affect all generated CLI node adapters. + */ + +import http from 'node:http'; +import https from 'node:https'; + +import type { + GraphQLAdapter, + GraphQLError, + QueryResult, +} from '@constructive-io/graphql-types'; + +interface HttpResponse { + statusCode: number; + statusMessage: string; + data: string; +} + +/** + * Check if a hostname is a localhost subdomain that needs special handling. + * Returns true for *.localhost (e.g. auth.localhost) but not bare "localhost". + */ +function isLocalhostSubdomain(hostname: string): boolean { + return hostname.endsWith('.localhost') && hostname !== 'localhost'; +} + +/** + * Make an HTTP/HTTPS request using native Node modules. + */ +function makeRequest( + url: URL, + options: http.RequestOptions, + body: string, +): Promise { + return new Promise((resolve, reject) => { + const protocol = url.protocol === 'https:' ? https : http; + + const req = protocol.request(url, options, (res) => { + let data = ''; + res.setEncoding('utf8'); + res.on('data', (chunk: string) => { + data += chunk; + }); + res.on('end', () => { + resolve({ + statusCode: res.statusCode || 0, + statusMessage: res.statusMessage || '', + data, + }); + }); + }); + + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +/** + * GraphQL adapter that uses node:http/node:https for requests. + * + * Handles *.localhost subdomains by rewriting the hostname to "localhost" + * and injecting the original Host header for server-side subdomain routing. + */ +export class NodeHttpAdapter implements GraphQLAdapter { + private headers: Record; + private url: URL; + + constructor( + private endpoint: string, + headers?: Record, + ) { + this.headers = headers ?? {}; + this.url = new URL(endpoint); + } + + async execute( + document: string, + variables?: Record, + ): Promise> { + const requestUrl = new URL(this.url.href); + const requestHeaders: Record = { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.headers, + }; + + // For *.localhost subdomains, rewrite hostname and inject Host header + if (isLocalhostSubdomain(requestUrl.hostname)) { + requestHeaders['Host'] = requestUrl.host; + requestUrl.hostname = 'localhost'; + } + + const body = JSON.stringify({ + query: document, + variables: variables ?? {}, + }); + + const requestOptions: http.RequestOptions = { + method: 'POST', + headers: requestHeaders, + }; + + const response = await makeRequest(requestUrl, requestOptions, body); + + if (response.statusCode < 200 || response.statusCode >= 300) { + return { + ok: false, + data: null, + errors: [ + { + message: `HTTP ${response.statusCode}: ${response.statusMessage}`, + }, + ], + }; + } + + const json = JSON.parse(response.data) as { + data?: T; + errors?: GraphQLError[]; + }; + + if (json.errors && json.errors.length > 0) { + return { + ok: false, + data: null, + errors: json.errors, + }; + } + + return { + ok: true, + data: json.data as T, + errors: undefined, + }; + } + + setHeaders(headers: Record): void { + this.headers = { ...this.headers, ...headers }; + } + + getEndpoint(): string { + return this.endpoint; + } +} From d1e9982f1d10407514322f60cdc3b26d619030a2 Mon Sep 17 00:00:00 2001 From: unknown <> Date: Tue, 24 Feb 2026 10:23:41 +0000 Subject: [PATCH 3/5] docs(codegen): update localhostAdapter JSDoc to reflect NodeHttpAdapter approach --- graphql/codegen/src/types/config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts index c1b1c7ea1..7b74d117c 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -201,10 +201,11 @@ export interface CliConfig { builtinNames?: BuiltinNames; /** - * Enable localhost fetch adapter for *.localhost subdomain routing. - * When true, generates a localhost-fetch.ts that patches globalThis.fetch - * to use node:http.request for *.localhost URLs, enabling local development + * Enable NodeHttpAdapter for *.localhost subdomain routing. + * When true, generates a node-fetch.ts with NodeHttpAdapter (implements GraphQLAdapter) + * using node:http/node:https for requests, enabling local development * with subdomain-based routing (e.g. auth.localhost:3000). + * No global patching — the adapter is passed to createClient via the adapter option. * @default false */ localhostAdapter?: boolean; From 8916fcdaed78ced98a2d203336d2ddaf1107db54 Mon Sep 17 00:00:00 2001 From: unknown <> Date: Tue, 24 Feb 2026 17:58:24 +0000 Subject: [PATCH 4/5] refactor(codegen): rename localhostAdapter to nodeHttpAdapter --- .../src/core/codegen/cli/executor-generator.ts | 10 +++++----- graphql/codegen/src/core/codegen/cli/index.ts | 17 ++++++++++------- graphql/codegen/src/types/config.ts | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/graphql/codegen/src/core/codegen/cli/executor-generator.ts b/graphql/codegen/src/core/codegen/cli/executor-generator.ts index 18abef775..f82a478f3 100644 --- a/graphql/codegen/src/core/codegen/cli/executor-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/executor-generator.ts @@ -32,14 +32,14 @@ function createImportDeclaration( export interface ExecutorOptions { /** Enable NodeHttpAdapter for *.localhost subdomain routing */ - localhostAdapter?: boolean; + nodeHttpAdapter?: boolean; } export function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile { const statements: t.Statement[] = []; // Import NodeHttpAdapter for *.localhost subdomain routing - if (options?.localhostAdapter) { + if (options?.nodeHttpAdapter) { statements.push( createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), ); @@ -204,7 +204,7 @@ export function generateExecutorFile(toolName: string, options?: ExecutorOptions ), // Build createClient config — use NodeHttpAdapter for *.localhost endpoints - ...(options?.localhostAdapter + ...(options?.nodeHttpAdapter ? [ t.returnStatement( t.callExpression(t.identifier('createClient'), [ @@ -262,7 +262,7 @@ export function generateMultiTargetExecutorFile( const statements: t.Statement[] = []; // Import NodeHttpAdapter for *.localhost subdomain routing - if (options?.localhostAdapter) { + if (options?.nodeHttpAdapter) { statements.push( createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), ); @@ -515,7 +515,7 @@ export function generateMultiTargetExecutorFile( ]), ), // Build createClient config — use NodeHttpAdapter for *.localhost endpoints - ...(options?.localhostAdapter + ...(options?.nodeHttpAdapter ? [ t.returnStatement( t.callExpression(t.identifier('createFn'), [ diff --git a/graphql/codegen/src/core/codegen/cli/index.ts b/graphql/codegen/src/core/codegen/cli/index.ts index 689487010..1edfa65fb 100644 --- a/graphql/codegen/src/core/codegen/cli/index.ts +++ b/graphql/codegen/src/core/codegen/cli/index.ts @@ -45,16 +45,19 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { ? cliConfig.toolName : 'app'; - const useLocalhostAdapter = typeof cliConfig === 'object' && !!cliConfig.localhostAdapter; + const useNodeHttpAdapter = + typeof cliConfig === 'object' && !!cliConfig.nodeHttpAdapter; - const executorFile = generateExecutorFile(toolName, { localhostAdapter: useLocalhostAdapter }); + const executorFile = generateExecutorFile(toolName, { + nodeHttpAdapter: useNodeHttpAdapter, + }); files.push(executorFile); const utilsFile = generateUtilsFile(); files.push(utilsFile); // Generate node HTTP adapter if configured (for *.localhost subdomain routing) - if (useLocalhostAdapter) { + if (useNodeHttpAdapter) { files.push(generateNodeFetchFile()); } @@ -118,8 +121,8 @@ export interface GenerateMultiTargetCliOptions { toolName: string; builtinNames?: BuiltinNames; targets: MultiTargetCliTarget[]; - /** Enable localhost fetch adapter for *.localhost subdomain routing */ - localhostAdapter?: boolean; + /** Enable NodeHttpAdapter for *.localhost subdomain routing */ + nodeHttpAdapter?: boolean; } export function resolveBuiltinNames( @@ -154,7 +157,7 @@ export function generateMultiTargetCli( ormImportPath: t.ormImportPath, })); const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, { - localhostAdapter: !!options.localhostAdapter, + nodeHttpAdapter: !!options.nodeHttpAdapter, }); files.push(executorFile); @@ -162,7 +165,7 @@ export function generateMultiTargetCli( files.push(utilsFile); // Generate node HTTP adapter if configured (for *.localhost subdomain routing) - if (options.localhostAdapter) { + if (options.nodeHttpAdapter) { files.push(generateNodeFetchFile()); } diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts index 7b74d117c..9fb6f1072 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -208,7 +208,7 @@ export interface CliConfig { * No global patching — the adapter is passed to createClient via the adapter option. * @default false */ - localhostAdapter?: boolean; + nodeHttpAdapter?: boolean; } /** From 09eb43b4c4630998304083e2661e7c5ffcc4813f Mon Sep 17 00:00:00 2001 From: unknown <> Date: Tue, 24 Feb 2026 18:40:42 +0000 Subject: [PATCH 5/5] refactor(codegen): elevate nodeHttpAdapter to top-level generation option - Move nodeHttpAdapter from CliConfig to GraphQLSDKConfigTarget - Auto-enable nodeHttpAdapter when CLI generation is enabled (unless explicitly false) - Generate node-fetch.ts in ORM output when nodeHttpAdapter is enabled - Export NodeHttpAdapter from ORM index.ts for standalone Node.js usage - Pass nodeHttpAdapter to multi-target CLI generator - Update template comment to reflect general Node.js usage (not CLI-specific) This allows any Node.js application (ORM, CLI, or custom) to use NodeHttpAdapter for localhost subdomain routing without needing CLI generation enabled. --- graphql/codegen/src/core/codegen/cli/index.ts | 4 +-- .../src/core/codegen/orm/client-generator.ts | 17 +++++++++++ graphql/codegen/src/core/codegen/orm/index.ts | 1 + .../src/core/codegen/templates/node-fetch.ts | 2 +- graphql/codegen/src/core/generate.ts | 27 +++++++++++++++-- graphql/codegen/src/types/config.ts | 29 ++++++++++++------- 6 files changed, 65 insertions(+), 15 deletions(-) diff --git a/graphql/codegen/src/core/codegen/cli/index.ts b/graphql/codegen/src/core/codegen/cli/index.ts index 1edfa65fb..5acbe7e21 100644 --- a/graphql/codegen/src/core/codegen/cli/index.ts +++ b/graphql/codegen/src/core/codegen/cli/index.ts @@ -45,8 +45,8 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { ? cliConfig.toolName : 'app'; - const useNodeHttpAdapter = - typeof cliConfig === 'object' && !!cliConfig.nodeHttpAdapter; + // Use top-level nodeHttpAdapter from config (auto-enabled for CLI by generate.ts) + const useNodeHttpAdapter = !!config.nodeHttpAdapter; const executorFile = generateExecutorFile(toolName, { nodeHttpAdapter: useNodeHttpAdapter, diff --git a/graphql/codegen/src/core/codegen/orm/client-generator.ts b/graphql/codegen/src/core/codegen/orm/client-generator.ts index 822399f7e..1d3233d9a 100644 --- a/graphql/codegen/src/core/codegen/orm/client-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/client-generator.ts @@ -122,6 +122,7 @@ export function generateCreateClientFile( tables: CleanTable[], hasCustomQueries: boolean, hasCustomMutations: boolean, + options?: { nodeHttpAdapter?: boolean }, ): GeneratedClientFile { const statements: t.Statement[] = []; @@ -215,6 +216,22 @@ export function generateCreateClientFile( // Re-export all models statements.push(t.exportAllDeclaration(t.stringLiteral('./models'))); + // Re-export NodeHttpAdapter when enabled (for use in any Node.js application) + if (options?.nodeHttpAdapter) { + statements.push( + t.exportNamedDeclaration( + null, + [ + t.exportSpecifier( + t.identifier('NodeHttpAdapter'), + t.identifier('NodeHttpAdapter'), + ), + ], + t.stringLiteral('./node-fetch'), + ), + ); + } + // Re-export custom operations if (hasCustomQueries) { statements.push( diff --git a/graphql/codegen/src/core/codegen/orm/index.ts b/graphql/codegen/src/core/codegen/orm/index.ts index 489607f56..91db97a23 100644 --- a/graphql/codegen/src/core/codegen/orm/index.ts +++ b/graphql/codegen/src/core/codegen/orm/index.ts @@ -170,6 +170,7 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { tables, hasCustomQueries, hasCustomMutations, + { nodeHttpAdapter: !!options.config.nodeHttpAdapter }, ); files.push({ path: indexFile.fileName, content: indexFile.content }); diff --git a/graphql/codegen/src/core/codegen/templates/node-fetch.ts b/graphql/codegen/src/core/codegen/templates/node-fetch.ts index cc51bcce4..221f4df49 100644 --- a/graphql/codegen/src/core/codegen/templates/node-fetch.ts +++ b/graphql/codegen/src/core/codegen/templates/node-fetch.ts @@ -1,5 +1,5 @@ /** - * Node HTTP Adapter for CLI + * Node HTTP Adapter for Node.js applications * * Implements the GraphQLAdapter interface using node:http / node:https * instead of the Fetch API. This solves two Node.js limitations: diff --git a/graphql/codegen/src/core/generate.ts b/graphql/codegen/src/core/generate.ts index 034eb1ee2..f8fb73179 100644 --- a/graphql/codegen/src/core/generate.ts +++ b/graphql/codegen/src/core/generate.ts @@ -109,6 +109,11 @@ export async function generate( const runOrm = runReactQuery || !!config.cli || (options.orm !== undefined ? !!options.orm : false); + // Auto-enable nodeHttpAdapter when CLI is enabled, unless explicitly set to false + const useNodeHttpAdapter = + options.nodeHttpAdapter === true || + (runCli && options.nodeHttpAdapter !== false); + if (!options.schemaOnly && !runReactQuery && !runOrm && !runCli) { return { success: false, @@ -256,7 +261,7 @@ export async function generate( mutations: customOperations.mutations, typeRegistry: customOperations.typeRegistry, }, - config, + config: { ...config, nodeHttpAdapter: useNodeHttpAdapter }, sharedTypesPath: bothEnabled ? '..' : undefined, }); filesToWrite.push( @@ -265,6 +270,16 @@ export async function generate( path: path.posix.join('orm', file.path), })), ); + + // Generate NodeHttpAdapter in ORM output when enabled + if (useNodeHttpAdapter) { + const { generateNodeFetchFile } = await import('./codegen/cli/utils-generator'); + const nodeFetchFile = generateNodeFetchFile(); + filesToWrite.push({ + path: path.posix.join('orm', nodeFetchFile.fileName), + content: nodeFetchFile.content, + }); + } } // Generate CLI commands @@ -276,7 +291,7 @@ export async function generate( queries: customOperations.queries, mutations: customOperations.mutations, }, - config, + config: { ...config, nodeHttpAdapter: useNodeHttpAdapter }, }); filesToWrite.push( ...files.map((file) => ({ @@ -669,10 +684,18 @@ export async function generateMulti( if (useUnifiedCli && cliTargets.length > 0 && !dryRun) { const cliConfig = typeof unifiedCli === 'object' ? unifiedCli : {}; const toolName = cliConfig.toolName ?? 'app'; + // Auto-enable nodeHttpAdapter for unified CLI unless explicitly disabled + // Check first target config for explicit nodeHttpAdapter setting + const firstTargetConfig = configs[names[0]]; + const multiNodeHttpAdapter = + firstTargetConfig?.nodeHttpAdapter === true || + (firstTargetConfig?.nodeHttpAdapter !== false); + const { files } = generateMultiTargetCli({ toolName, builtinNames: cliConfig.builtinNames, targets: cliTargets, + nodeHttpAdapter: multiNodeHttpAdapter, }); const cliFilesToWrite = files.map((file) => ({ diff --git a/graphql/codegen/src/types/config.ts b/graphql/codegen/src/types/config.ts index 9fb6f1072..7f572a564 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -199,16 +199,6 @@ export interface CliConfig { * context -> 'context' (renamed to 'env' on collision) */ builtinNames?: BuiltinNames; - - /** - * Enable NodeHttpAdapter for *.localhost subdomain routing. - * When true, generates a node-fetch.ts with NodeHttpAdapter (implements GraphQLAdapter) - * using node:http/node:https for requests, enabling local development - * with subdomain-based routing (e.g. auth.localhost:3000). - * No global patching — the adapter is passed to createClient via the adapter option. - * @default false - */ - nodeHttpAdapter?: boolean; } /** @@ -333,6 +323,25 @@ export interface GraphQLSDKConfigTarget { */ orm?: boolean; + /** + * Enable NodeHttpAdapter generation for Node.js applications. + * When true, generates a node-fetch.ts with NodeHttpAdapter (implements GraphQLAdapter) + * using node:http/node:https for requests, enabling local development + * with subdomain-based routing (e.g. auth.localhost:3000). + * No global patching — the adapter is passed to createClient via the adapter option. + * + * When CLI generation is enabled (`cli: true`), this is auto-enabled unless + * explicitly set to `false`. + * + * Can also be used standalone with the ORM client for any Node.js application: + * ```ts + * import { NodeHttpAdapter } from './orm/node-fetch'; + * const db = createClient({ adapter: new NodeHttpAdapter(endpoint, headers) }); + * ``` + * @default false + */ + nodeHttpAdapter?: boolean; + /** * Whether to generate React Query hooks * When enabled, generates React Query hooks to {output}/hooks