From 62061ea0e9b4e5bbcf147d2777d5e71823a365c8 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 20 May 2026 19:26:53 +0000 Subject: [PATCH] fix(codegen): collect input types referenced by table field arguments Extend collectInputTypeNames() to optionally accept tables and scan their computed-field args for Input/Filter type references. This handles bucket-style computed fields (e.g. requestBulkUploadUrls) whose arg types were inlined into *Select types but never registered for generation. The fix is applied at two levels: - In the orchestrator (index.ts): passes tables to collectInputTypeNames - In generateInputTypesFile: defense-in-depth merge for direct callers Includes regression tests for both the end-to-end generation and the collectInputTypeNames function itself. --- .../codegen/input-types-generator.test.ts | 92 +++++++++++++++++++ graphql/codegen/src/core/codegen/orm/index.ts | 2 +- .../core/codegen/orm/input-types-generator.ts | 28 +++++- 3 files changed, 116 insertions(+), 6 deletions(-) diff --git a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts index 63f18029dd..6c6038ba4f 100644 --- a/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts +++ b/graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts @@ -1003,3 +1003,95 @@ describe('edge cases', () => { ); }); }); + +// ============================================================================ +// Tests - Table Field Argument Input Types (Bucket-style computed fields) +// ============================================================================ + +describe('table field argument input types', () => { + it('emits Input types referenced only by table-field args', () => { + const bucketTable = createTable({ + name: 'FooBucket', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { + name: 'requestBulkUploadUrls', + type: { gqlType: 'String', isArray: true } as FieldType, + args: [ + { + name: 'files', + type: createNonNull( + createList( + createNonNull( + createTypeRef('INPUT_OBJECT', 'FooBulkUploadFileInput'), + ), + ), + ), + isRequired: true, + }, + ], + }, + ], + }); + + const registry = createTypeRegistry({ + FooBulkUploadFileInput: { + kind: 'INPUT_OBJECT', + name: 'FooBulkUploadFileInput', + inputFields: [ + { + name: 'fileName', + type: createNonNull(createTypeRef('SCALAR', 'String')), + }, + { + name: 'contentType', + type: createTypeRef('SCALAR', 'String'), + }, + { + name: 'sizeBytes', + type: createNonNull(createTypeRef('SCALAR', 'Int')), + }, + ], + }, + }); + + const result = generateInputTypesFile(registry, new Set(), [bucketTable]); + + expect(result.content).toContain( + 'export interface FooBulkUploadFileInput {', + ); + expect(result.content).toContain('fileName: string;'); + expect(result.content).toContain('contentType?: string;'); + expect(result.content).toContain('sizeBytes: number;'); + }); + + it('collectInputTypeNames discovers field-arg types when tables provided', () => { + const bucketTable = createTable({ + name: 'FooBucket', + fields: [ + { name: 'id', type: fieldTypes.uuid }, + { + name: 'requestBulkUploadUrls', + type: { gqlType: 'String', isArray: true } as FieldType, + args: [ + { + name: 'files', + type: createNonNull( + createTypeRef('INPUT_OBJECT', 'FooBulkUploadFileInput'), + ), + isRequired: true, + }, + ], + }, + ], + }); + + // Without tables: only collects from operations + const withoutTables = collectInputTypeNames([]); + expect(withoutTables.has('FooBulkUploadFileInput')).toBe(false); + + // With tables: also collects from field args + const withTables = collectInputTypeNames([], [bucketTable]); + expect(withTables.has('FooBulkUploadFileInput')).toBe(true); + }); +}); diff --git a/graphql/codegen/src/core/codegen/orm/index.ts b/graphql/codegen/src/core/codegen/orm/index.ts index f9be00d932..26556e8865 100644 --- a/graphql/codegen/src/core/codegen/orm/index.ts +++ b/graphql/codegen/src/core/codegen/orm/index.ts @@ -117,7 +117,7 @@ export function generateOrm(options: GenerateOrmOptions): GenerateOrmResult { ...(customOperations?.queries ?? []), ...(customOperations?.mutations ?? []), ]; - const usedInputTypes = collectInputTypeNames(allOps); + const usedInputTypes = collectInputTypeNames(allOps, tables); const usedPayloadTypes = collectPayloadTypeNames(allOps); // Also include payload types for table CRUD mutations (they reference Edge types) diff --git a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts index 37c94f3168..99539e6c4c 100644 --- a/graphql/codegen/src/core/codegen/orm/input-types-generator.ts +++ b/graphql/codegen/src/core/codegen/orm/input-types-generator.ts @@ -1559,19 +1559,21 @@ function generateAllCrudInputTypes( // ============================================================================ /** - * Collect all input type names used by operations + * Collect all input type names used by operations and table field arguments. + * + * Scans both custom operation args and computed-field args (e.g. + * `requestBulkUploadUrls(files: [FooBulkUploadFileInput!]!)` on bucket + * tables) so that referenced Input types are registered for generation. */ export function collectInputTypeNames( operations: Array<{ args: Argument[] }>, + tables?: Table[], ): Set { const inputTypes = new Set(); function collectFromTypeRef(typeRef: Argument['type']) { const baseName = getTypeBaseName(typeRef); - if (baseName && baseName.endsWith('Input')) { - inputTypes.add(baseName); - } - if (baseName && baseName.endsWith('Filter')) { + if (baseName && (baseName.endsWith('Input') || baseName.endsWith('Filter'))) { inputTypes.add(baseName); } } @@ -1582,6 +1584,17 @@ export function collectInputTypeNames( } } + if (tables) { + for (const table of tables) { + for (const field of table.fields) { + if (!field.args) continue; + for (const arg of field.args) { + collectFromTypeRef(arg.type); + } + } + } + } + return inputTypes; } @@ -2057,6 +2070,7 @@ export function generateInputTypesFile( // 7. Custom input types from TypeRegistry // Also include any extra types referenced by plugin-injected filter fields + // and by table field arguments (e.g. bucket computed-field args) const mergedUsedInputTypes = new Set(usedInputTypes); if (hasTables) { const filterExtraTypes = collectFilterExtraInputTypes( @@ -2066,6 +2080,10 @@ export function generateInputTypesFile( for (const typeName of filterExtraTypes) { mergedUsedInputTypes.add(typeName); } + const fieldArgTypes = collectInputTypeNames([], tablesList); + for (const typeName of fieldArgTypes) { + mergedUsedInputTypes.add(typeName); + } } const tableCrudTypes = tables ? buildTableCrudTypeNames(tables) : undefined; // Pass customScalarTypes + enumTypes as already-generated to avoid duplicate declarations