Skip to content

Commit 87b67d2

Browse files
author
unknown
committed
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
1 parent b4e199d commit 87b67d2

8 files changed

Lines changed: 298 additions & 15 deletions

File tree

graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,7 +1153,7 @@ export default async (argv: Partial<Record<string, unknown>>, prompter: Inquirer
11531153
}
11541154
const client = getClient();
11551155
const selectFields = buildSelectFromPaths(argv.select ?? "");
1156-
const result = await client.query.currentUser({}, {
1156+
const result = await client.query.currentUser({
11571157
select: selectFields
11581158
}).execute();
11591159
console.log(JSON.stringify(result, null, 2));

graphql/codegen/src/core/codegen/cli/custom-command-generator.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,21 @@ function buildOrmCustomCall(
143143
opName: string,
144144
argsExpr: t.Expression,
145145
selectExpr?: t.Expression,
146+
hasArgs: boolean = true,
146147
): t.Expression {
147-
const callArgs: t.Expression[] = [argsExpr];
148-
if (selectExpr) {
148+
const callArgs: t.Expression[] = [];
149+
if (hasArgs) {
150+
// Operation has arguments: pass args as first param, select as second
151+
callArgs.push(argsExpr);
152+
if (selectExpr) {
153+
callArgs.push(
154+
t.objectExpression([
155+
t.objectProperty(t.identifier('select'), selectExpr),
156+
]),
157+
);
158+
}
159+
} else if (selectExpr) {
160+
// No arguments: pass { select } as the only param (ORM signature)
149161
callArgs.push(
150162
t.objectExpression([
151163
t.objectProperty(t.identifier('select'), selectExpr),
@@ -343,12 +355,13 @@ export function generateCustomCommand(op: CleanOperation, options?: CustomComman
343355
selectExpr = t.identifier('selectFields');
344356
}
345357

358+
const hasArgs = op.args.length > 0;
346359
bodyStatements.push(
347360
t.variableDeclaration('const', [
348361
t.variableDeclarator(
349362
t.identifier('result'),
350363
t.awaitExpression(
351-
buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr),
364+
buildOrmCustomCall(opKind, op.name, argsExpr, selectExpr, hasArgs),
352365
),
353366
),
354367
]),

graphql/codegen/src/core/codegen/cli/executor-generator.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,21 @@ function createImportDeclaration(
3030
return decl;
3131
}
3232

33-
export function generateExecutorFile(toolName: string): GeneratedFile {
33+
export interface ExecutorOptions {
34+
/** Enable localhost fetch adapter import */
35+
localhostAdapter?: boolean;
36+
}
37+
38+
export function generateExecutorFile(toolName: string, options?: ExecutorOptions): GeneratedFile {
3439
const statements: t.Statement[] = [];
3540

41+
// Import localhost adapter first (side-effect import patches globalThis.fetch)
42+
if (options?.localhostAdapter) {
43+
statements.push(
44+
t.importDeclaration([], t.stringLiteral('./localhost-fetch')),
45+
);
46+
}
47+
3648
statements.push(
3749
createImportDeclaration('appstash', ['createConfigStore']),
3850
);
@@ -226,9 +238,17 @@ export function generateExecutorFile(toolName: string): GeneratedFile {
226238
export function generateMultiTargetExecutorFile(
227239
toolName: string,
228240
targets: MultiTargetExecutorInput[],
241+
options?: ExecutorOptions,
229242
): GeneratedFile {
230243
const statements: t.Statement[] = [];
231244

245+
// Import localhost adapter first (side-effect import patches globalThis.fetch)
246+
if (options?.localhostAdapter) {
247+
statements.push(
248+
t.importDeclaration([], t.stringLiteral('./localhost-fetch')),
249+
);
250+
}
251+
232252
statements.push(
233253
createImportDeclaration('appstash', ['createConfigStore']),
234254
);

graphql/codegen/src/core/codegen/cli/index.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { BuiltinNames, GraphQLSDKConfigTarget } from '../../../types/config';
2-
import type { CleanOperation, CleanTable } from '../../../types/schema';
2+
import type { CleanOperation, CleanTable, TypeRegistry } from '../../../types/schema';
33
import { generateCommandMap, generateMultiTargetCommandMap } from './command-map-generator';
44
import { generateCustomCommand } from './custom-command-generator';
55
import { generateExecutorFile, generateMultiTargetExecutorFile } from './executor-generator';
@@ -11,7 +11,7 @@ import {
1111
generateMultiTargetContextCommand,
1212
} from './infra-generator';
1313
import { generateTableCommand } from './table-command-generator';
14-
import { generateUtilsFile } from './utils-generator';
14+
import { generateUtilsFile, generateLocalhostFetchFile } from './utils-generator';
1515

1616
export interface GenerateCliOptions {
1717
tables: CleanTable[];
@@ -20,6 +20,8 @@ export interface GenerateCliOptions {
2020
mutations: CleanOperation[];
2121
};
2222
config: GraphQLSDKConfigTarget;
23+
/** TypeRegistry from introspection, used to check field defaults */
24+
typeRegistry?: TypeRegistry;
2325
}
2426

2527
export interface GenerateCliResult {
@@ -43,20 +45,29 @@ export function generateCli(options: GenerateCliOptions): GenerateCliResult {
4345
? cliConfig.toolName
4446
: 'app';
4547

46-
const executorFile = generateExecutorFile(toolName);
48+
const useLocalhostAdapter = typeof cliConfig === 'object' && !!cliConfig.localhostAdapter;
49+
50+
const executorFile = generateExecutorFile(toolName, { localhostAdapter: useLocalhostAdapter });
4751
files.push(executorFile);
4852

4953
const utilsFile = generateUtilsFile();
5054
files.push(utilsFile);
5155

56+
// Generate localhost adapter if configured
57+
if (useLocalhostAdapter) {
58+
files.push(generateLocalhostFetchFile());
59+
}
60+
5261
const contextFile = generateContextCommand(toolName);
5362
files.push(contextFile);
5463

5564
const authFile = generateAuthCommand(toolName);
5665
files.push(authFile);
5766

5867
for (const table of tables) {
59-
const tableFile = generateTableCommand(table);
68+
const tableFile = generateTableCommand(table, {
69+
typeRegistry: options.typeRegistry,
70+
});
6071
files.push(tableFile);
6172
}
6273

@@ -99,12 +110,16 @@ export interface MultiTargetCliTarget {
99110
mutations: CleanOperation[];
100111
};
101112
isAuthTarget?: boolean;
113+
/** TypeRegistry from introspection, used to check field defaults */
114+
typeRegistry?: TypeRegistry;
102115
}
103116

104117
export interface GenerateMultiTargetCliOptions {
105118
toolName: string;
106119
builtinNames?: BuiltinNames;
107120
targets: MultiTargetCliTarget[];
121+
/** Enable localhost fetch adapter for *.localhost subdomain routing */
122+
localhostAdapter?: boolean;
108123
}
109124

110125
export function resolveBuiltinNames(
@@ -138,12 +153,19 @@ export function generateMultiTargetCli(
138153
endpoint: t.endpoint,
139154
ormImportPath: t.ormImportPath,
140155
}));
141-
const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs);
156+
const executorFile = generateMultiTargetExecutorFile(toolName, executorInputs, {
157+
localhostAdapter: !!options.localhostAdapter,
158+
});
142159
files.push(executorFile);
143160

144161
const utilsFile = generateUtilsFile();
145162
files.push(utilsFile);
146163

164+
// Generate localhost adapter if configured
165+
if (options.localhostAdapter) {
166+
files.push(generateLocalhostFetchFile());
167+
}
168+
147169
const contextFile = generateMultiTargetContextCommand(
148170
toolName,
149171
builtinNames.context,
@@ -174,6 +196,7 @@ export function generateMultiTargetCli(
174196
const tableFile = generateTableCommand(table, {
175197
targetName: target.name,
176198
executorImportPath: '../../executor',
199+
typeRegistry: target.typeRegistry,
177200
});
178201
files.push(tableFile);
179202
}

graphql/codegen/src/core/codegen/cli/table-command-generator.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
getTableNames,
1010
ucFirst,
1111
} from '../utils';
12-
import type { CleanTable } from '../../../types/schema';
12+
import type { CleanTable, TypeRegistry } from '../../../types/schema';
1313
import type { GeneratedFile } from './executor-generator';
14+
import { getCreateInputTypeName } from '../utils';
1415

1516
function createImportDeclaration(
1617
moduleSpecifier: string,
@@ -334,10 +335,55 @@ function buildGetHandler(table: CleanTable, targetName?: string): t.FunctionDecl
334335
);
335336
}
336337

338+
/**
339+
* Get the set of field names that have defaults in the create input type.
340+
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
341+
* TypeRegistry and checks each field's defaultValue from introspection.
342+
*/
343+
function getFieldsWithDefaults(
344+
table: CleanTable,
345+
typeRegistry?: TypeRegistry,
346+
): Set<string> {
347+
const fieldsWithDefaults = new Set<string>();
348+
if (!typeRegistry) return fieldsWithDefaults;
349+
350+
// Look up the CreateXInput type (e.g. CreateDatabaseInput)
351+
const createInputTypeName = getCreateInputTypeName(table);
352+
const createInputType = typeRegistry.get(createInputTypeName);
353+
if (!createInputType?.inputFields) return fieldsWithDefaults;
354+
355+
// The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
356+
// Find the inner input type that contains the actual field definitions
357+
for (const inputField of createInputType.inputFields) {
358+
// The inner field's type name is the actual input type (e.g. DatabaseInput)
359+
const innerTypeName = inputField.type.name
360+
|| inputField.type.ofType?.name
361+
|| inputField.type.ofType?.ofType?.name;
362+
if (!innerTypeName) continue;
363+
364+
const innerType = typeRegistry.get(innerTypeName);
365+
if (!innerType?.inputFields) continue;
366+
367+
// Check each field in the inner input type for defaultValue
368+
for (const field of innerType.inputFields) {
369+
if (field.defaultValue !== undefined) {
370+
fieldsWithDefaults.add(field.name);
371+
}
372+
// Also check if the field is NOT wrapped in NON_NULL (nullable = has default or is optional)
373+
if (field.type.kind !== 'NON_NULL') {
374+
fieldsWithDefaults.add(field.name);
375+
}
376+
}
377+
}
378+
379+
return fieldsWithDefaults;
380+
}
381+
337382
function buildMutationHandler(
338383
table: CleanTable,
339384
operation: 'create' | 'update' | 'delete',
340385
targetName?: string,
386+
typeRegistry?: TypeRegistry,
341387
): t.FunctionDeclaration {
342388
const { singularName } = getTableNames(table);
343389
const pkFields = getPrimaryKeyInfo(table);
@@ -351,6 +397,9 @@ function buildMutationHandler(
351397
f.name !== 'updatedAt',
352398
);
353399

400+
// Get fields that have defaults from introspection (for create operations)
401+
const fieldsWithDefaults = getFieldsWithDefaults(table, typeRegistry);
402+
354403
const questions: t.Expression[] = [];
355404

356405
if (operation === 'update' || operation === 'delete') {
@@ -366,6 +415,9 @@ function buildMutationHandler(
366415

367416
if (operation !== 'delete') {
368417
for (const field of editableFields) {
418+
// For create: field is required only if it has no default value
419+
// For update: all fields are optional (user only updates what they want)
420+
const isRequired = operation === 'create' && !fieldsWithDefaults.has(field.name);
369421
questions.push(
370422
t.objectExpression([
371423
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
@@ -379,7 +431,7 @@ function buildMutationHandler(
379431
),
380432
t.objectProperty(
381433
t.identifier('required'),
382-
t.booleanLiteral(operation === 'create'),
434+
t.booleanLiteral(isRequired),
383435
),
384436
]),
385437
);
@@ -530,6 +582,8 @@ function buildMutationHandler(
530582
export interface TableCommandOptions {
531583
targetName?: string;
532584
executorImportPath?: string;
585+
/** TypeRegistry from introspection, used to check field defaults */
586+
typeRegistry?: TypeRegistry;
533587
}
534588

535589
export function generateTableCommand(table: CleanTable, options?: TableCommandOptions): GeneratedFile {
@@ -741,9 +795,9 @@ export function generateTableCommand(table: CleanTable, options?: TableCommandOp
741795
const tn = options?.targetName;
742796
statements.push(buildListHandler(table, tn));
743797
statements.push(buildGetHandler(table, tn));
744-
statements.push(buildMutationHandler(table, 'create', tn));
745-
statements.push(buildMutationHandler(table, 'update', tn));
746-
statements.push(buildMutationHandler(table, 'delete', tn));
798+
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry));
799+
statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry));
800+
statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry));
747801

748802
const header = getGeneratedFileHeader(`CLI commands for ${table.name}`);
749803
const code = generateCode(statements);

graphql/codegen/src/core/codegen/cli/utils-generator.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,20 @@ export function generateUtilsFile(): GeneratedFile {
5959
),
6060
};
6161
}
62+
63+
/**
64+
* Generate a localhost-fetch.ts file that patches globalThis.fetch for *.localhost URLs.
65+
* This enables seamless local development with subdomain routing (e.g. auth.localhost:3000).
66+
*
67+
* Node.js cannot resolve *.localhost subdomains and the Fetch API forbids the Host header.
68+
* This adapter uses node:http.request to proxy requests with proper Host headers.
69+
*/
70+
export function generateLocalhostFetchFile(): GeneratedFile {
71+
return {
72+
fileName: 'localhost-fetch.ts',
73+
content: readTemplateFile(
74+
'localhost-fetch.ts',
75+
'Localhost fetch adapter — patches globalThis.fetch for *.localhost subdomain routing',
76+
),
77+
};
78+
}

0 commit comments

Comments
 (0)