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 40d1b320b..0aa3c3e04 100644 --- a/graphql/codegen/src/core/codegen/cli/table-command-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/table-command-generator.ts @@ -6,7 +6,10 @@ import { getGeneratedFileHeader, getPrimaryKeyInfo, getScalarFields, + getSelectableScalarFields, getTableNames, + getWritableFieldNames, + resolveInnerInputType, ucFirst, lcFirst, getCreateInputTypeName, @@ -150,8 +153,8 @@ function buildFieldSchemaObject(table: CleanTable): t.ObjectExpression { ); } -function buildSelectObject(table: CleanTable): t.ObjectExpression { - const fields = getScalarFields(table); +function buildSelectObject(table: CleanTable, typeRegistry?: TypeRegistry): t.ObjectExpression { + const fields = getSelectableScalarFields(table, typeRegistry); return t.objectExpression( fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true)), @@ -305,9 +308,9 @@ function buildSubcommandSwitch( return t.switchStatement(t.identifier('subcommand'), cases); } -function buildListHandler(table: CleanTable, targetName?: string): t.FunctionDeclaration { +function buildListHandler(table: CleanTable, targetName?: string, typeRegistry?: TypeRegistry): t.FunctionDeclaration { const { singularName } = getTableNames(table); - const selectObj = buildSelectObject(table); + const selectObj = buildSelectObject(table, typeRegistry); const tryBody: t.Statement[] = [ buildGetClientStatement(targetName), @@ -349,11 +352,11 @@ function buildListHandler(table: CleanTable, targetName?: string): t.FunctionDec ); } -function buildGetHandler(table: CleanTable, targetName?: string): t.FunctionDeclaration { +function buildGetHandler(table: CleanTable, targetName?: string, typeRegistry?: TypeRegistry): t.FunctionDeclaration { const { singularName } = getTableNames(table); const pkFields = getPrimaryKeyInfo(table); const pk = pkFields[0]; - const selectObj = buildSelectObject(table); + const selectObj = buildSelectObject(table, typeRegistry); const promptQuestion = t.objectExpression([ t.objectProperty(t.identifier('type'), t.stringLiteral('text')), @@ -423,38 +426,6 @@ 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. - */ -/** - * Resolve the inner input type from a CreateXInput or UpdateXInput type. - * The CreateXInput has an inner field (e.g. "database" of type DatabaseInput) - * that contains the actual field definitions. - */ -export function resolveInnerInputType( - inputTypeName: string, - typeRegistry: TypeRegistry, -): { name: string; fields: Set } | null { - const inputType = typeRegistry.get(inputTypeName); - if (!inputType?.inputFields) return null; - - for (const inputField of inputType.inputFields) { - 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; - - const fields = new Set(innerType.inputFields.map((f) => f.name)); - return { name: innerTypeName, fields }; - } - return null; -} - export function getFieldsWithDefaults( table: CleanTable, typeRegistry?: TypeRegistry, @@ -481,22 +452,6 @@ export function getFieldsWithDefaults( return fieldsWithDefaults; } -/** - * Get the set of field names that actually exist in the create/update input type. - * Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid) - * should be excluded from the data object in create/update handlers. - */ -function getWritableFieldNames( - table: CleanTable, - typeRegistry?: TypeRegistry, -): Set | null { - if (!typeRegistry) return null; - - const createInputTypeName = getCreateInputTypeName(table); - const resolved = resolveInnerInputType(createInputTypeName, typeRegistry); - return resolved?.fields ?? null; -} - function buildMutationHandler( table: CleanTable, operation: 'create' | 'update' | 'delete', @@ -589,7 +544,7 @@ function buildMutationHandler( ? t.objectExpression([ t.objectProperty(t.identifier(pk.name), t.booleanLiteral(true)), ]) - : buildSelectObject(table); + : buildSelectObject(table, typeRegistry); let ormArgs: t.ObjectExpression; @@ -1013,8 +968,8 @@ export function generateTableCommand(table: CleanTable, options?: TableCommandOp const tn = options?.targetName; const ormTypes = { createInputTypeName, patchTypeName, innerFieldName }; - statements.push(buildListHandler(table, tn)); - if (hasGet) statements.push(buildGetHandler(table, tn)); + statements.push(buildListHandler(table, tn, options?.typeRegistry)); + if (hasGet) statements.push(buildGetHandler(table, tn, options?.typeRegistry)); statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes)); if (hasUpdate) statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes)); if (hasDelete) statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes)); diff --git a/graphql/codegen/src/core/codegen/utils.ts b/graphql/codegen/src/core/codegen/utils.ts index 8f87baf4c..7ca1f1480 100644 --- a/graphql/codegen/src/core/codegen/utils.ts +++ b/graphql/codegen/src/core/codegen/utils.ts @@ -7,6 +7,7 @@ import type { CleanField, CleanFieldType, CleanTable, + TypeRegistry, } from '../../types/schema'; import { scalarToFilterType, scalarToTsType } from './scalars'; @@ -335,6 +336,68 @@ export function getScalarFields(table: CleanTable): CleanField[] { return table.fields.filter((f) => !isRelationField(f.name, table)); } +/** + * Resolve the inner input type from a CreateXInput. + * PostGraphile create inputs wrap the actual field definitions in an inner type + * (e.g. CreateUserInput -> { user: UserInput }) — this resolves that inner type + * and returns the set of field names it contains. + */ +export function resolveInnerInputType( + inputTypeName: string, + typeRegistry: TypeRegistry, +): { name: string; fields: Set } | null { + const inputType = typeRegistry.get(inputTypeName); + if (!inputType?.inputFields) return null; + + for (const inputField of inputType.inputFields) { + 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; + + const fields = new Set(innerType.inputFields.map((f) => f.name)); + return { name: innerTypeName, fields }; + } + return null; +} + +/** + * Get the set of field names that actually exist in the create input type. + * Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid) + * are plugin-added computed fields that don't correspond to real database columns. + * Returns null when no typeRegistry is provided (caller should treat as "no filtering"). + */ +export function getWritableFieldNames( + table: CleanTable, + typeRegistry?: TypeRegistry, +): Set | null { + if (!typeRegistry) return null; + + const createInputTypeName = getCreateInputTypeName(table); + const resolved = resolveInnerInputType(createInputTypeName, typeRegistry); + return resolved?.fields ?? null; +} + +/** + * Get scalar fields that represent actual database columns (not computed/plugin-added). + * When a TypeRegistry is provided, filters out fields that don't exist in the + * create input type — these are computed fields added by plugins (e.g. search scores, + * hash UUIDs) that aren't real columns and shouldn't appear in default selections. + * Without a TypeRegistry, falls back to all scalar fields. + */ +export function getSelectableScalarFields( + table: CleanTable, + typeRegistry?: TypeRegistry, +): CleanField[] { + const writableFields = getWritableFieldNames(table, typeRegistry); + return getScalarFields(table).filter( + (f) => writableFields === null || writableFields.has(f.name), + ); +} + /** * Primary key field information */