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..f82a478f3 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 NodeHttpAdapter for *.localhost subdomain routing */ + nodeHttpAdapter?: boolean; +} + +export function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile { const statements: t.Statement[] = []; + // Import NodeHttpAdapter for *.localhost subdomain routing + if (options?.nodeHttpAdapter) { + statements.push( + createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), + ); + } + statements.push( createImportDeclaration('appstash', ['createConfigStore']), ); @@ -191,20 +203,39 @@ export function generateExecutorFile(toolName: string): GeneratedFile { ]), ), - 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?.nodeHttpAdapter + ? [ + 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( @@ -226,9 +257,17 @@ export function generateExecutorFile(toolName: string): GeneratedFile { export function generateMultiTargetExecutorFile( toolName: string, targets: MultiTargetExecutorInput[], + options?: ExecutorOptions, ): GeneratedFile { const statements: t.Statement[] = []; + // Import NodeHttpAdapter for *.localhost subdomain routing + if (options?.nodeHttpAdapter) { + statements.push( + createImportDeclaration('./node-fetch', ['NodeHttpAdapter']), + ); + } + statements.push( createImportDeclaration('appstash', ['createConfigStore']), ); @@ -475,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?.nodeHttpAdapter + ? [ + 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 81c287d52..5acbe7e21 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, generateNodeFetchFile } 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,22 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult { ? cliConfig.toolName : 'app'; - const executorFile = generateExecutorFile(toolName); + // Use top-level nodeHttpAdapter from config (auto-enabled for CLI by generate.ts) + const useNodeHttpAdapter = !!config.nodeHttpAdapter; + + 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 (useNodeHttpAdapter) { + files.push(generateNodeFetchFile()); + } + const contextFile = generateContextCommand(toolName); files.push(contextFile); @@ -56,7 +68,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 +113,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 NodeHttpAdapter for *.localhost subdomain routing */ + nodeHttpAdapter?: boolean; } export function resolveBuiltinNames( @@ -138,12 +156,19 @@ export function generateMultiTargetCli( endpoint: t.endpoint, ormImportPath: t.ormImportPath, })); - const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs); + const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, { + nodeHttpAdapter: !!options.nodeHttpAdapter, + }); files.push(executorFile); const utilsFile = generateUtilsFile(); files.push(utilsFile); + // Generate node HTTP adapter if configured (for *.localhost subdomain routing) + if (options.nodeHttpAdapter) { + files.push(generateNodeFetchFile()); + } + const contextFile = generateMultiTargetContextCommand( toolName, builtinNames.context, @@ -174,6 +199,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..a669132cb 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 node-fetch.ts file with NodeHttpAdapter for CLI. + * + * 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 generateNodeFetchFile(): GeneratedFile { + return { + fileName: 'node-fetch.ts', + content: readTemplateFile( + 'node-fetch.ts', + 'Node HTTP adapter for localhost subdomain routing', + ), + }; +} 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 new file mode 100644 index 000000000..221f4df49 --- /dev/null +++ b/graphql/codegen/src/core/codegen/templates/node-fetch.ts @@ -0,0 +1,162 @@ +/** + * 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: + * + * 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; + } +} 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 0ca13a680..7f572a564 100644 --- a/graphql/codegen/src/types/config.ts +++ b/graphql/codegen/src/types/config.ts @@ -323,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