From d96c8c1aef35853bb1027536faf07504c016b94a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 14 Mar 2026 23:05:22 +0000 Subject: [PATCH 1/2] fix(codegen): exclude computed fields from default CLI select objects Uses getWritableFieldNames (which checks the create input type) to filter out computed fields like search scores (descriptionTrgmSimilarity, searchScore, etc.) and other plugin-added fields (hashUuid) from the default select in handleList, handleGet, and mutation response handlers. Fields not present in the create input type are considered computed and are excluded from default selection. They can still be explicitly requested in custom queries. --- .../codegen/cli/table-command-generator.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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..b9af96b5f 100644 --- a/graphql/codegen/src/core/codegen/cli/table-command-generator.ts +++ b/graphql/codegen/src/core/codegen/cli/table-command-generator.ts @@ -150,8 +150,11 @@ 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 writableFields = getWritableFieldNames(table, typeRegistry); + const fields = getScalarFields(table).filter( + (f) => writableFields === null || writableFields.has(f.name), + ); 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')), @@ -589,7 +592,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 +1016,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)); From fabee97f72e250e506968235be7fab221198df47 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 14 Mar 2026 23:13:36 +0000 Subject: [PATCH 2/2] refactor(codegen): extract getSelectableScalarFields into shared utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move resolveInnerInputType, getWritableFieldNames, and new getSelectableScalarFields helper into utils.ts as shared abstractions. These helpers centralize the concept of 'real database columns vs computed plugin fields' so any generator (CLI, docs, hooks, ORM) can reuse them. getSelectableScalarFields composes getScalarFields + getWritableFieldNames into a single call. Remove duplicated resolveInnerInputType and getWritableFieldNames from table-command-generator.ts — it now imports them from utils. --- .../codegen/cli/table-command-generator.ts | 56 ++--------------- graphql/codegen/src/core/codegen/utils.ts | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 52 deletions(-) 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 b9af96b5f..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, @@ -151,10 +154,7 @@ function buildFieldSchemaObject(table: CleanTable): t.ObjectExpression { } function buildSelectObject(table: CleanTable, typeRegistry?: TypeRegistry): t.ObjectExpression { - const writableFields = getWritableFieldNames(table, typeRegistry); - const fields = getScalarFields(table).filter( - (f) => writableFields === null || writableFields.has(f.name), - ); + const fields = getSelectableScalarFields(table, typeRegistry); return t.objectExpression( fields.map((f) => t.objectProperty(t.identifier(f.name), t.booleanLiteral(true)), @@ -426,38 +426,6 @@ function buildGetHandler(table: CleanTable, targetName?: string, typeRegistry?: ); } -/** - * 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, @@ -484,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', 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 */