Skip to content

Commit d18f708

Browse files
authored
Merge pull request #815 from constructive-io/devin/1773529303-codegen-exclude-computed-fields
fix(codegen): exclude computed fields from default CLI select objects
2 parents 1ade5f1 + fabee97 commit d18f708

2 files changed

Lines changed: 75 additions & 57 deletions

File tree

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

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import {
66
getGeneratedFileHeader,
77
getPrimaryKeyInfo,
88
getScalarFields,
9+
getSelectableScalarFields,
910
getTableNames,
11+
getWritableFieldNames,
12+
resolveInnerInputType,
1013
ucFirst,
1114
lcFirst,
1215
getCreateInputTypeName,
@@ -150,8 +153,8 @@ function buildFieldSchemaObject(table: CleanTable): t.ObjectExpression {
150153
);
151154
}
152155

153-
function buildSelectObject(table: CleanTable): t.ObjectExpression {
154-
const fields = getScalarFields(table);
156+
function buildSelectObject(table: CleanTable, typeRegistry?: TypeRegistry): t.ObjectExpression {
157+
const fields = getSelectableScalarFields(table, typeRegistry);
155158
return t.objectExpression(
156159
fields.map((f) =>
157160
t.objectProperty(t.identifier(f.name), t.booleanLiteral(true)),
@@ -305,9 +308,9 @@ function buildSubcommandSwitch(
305308
return t.switchStatement(t.identifier('subcommand'), cases);
306309
}
307310

308-
function buildListHandler(table: CleanTable, targetName?: string): t.FunctionDeclaration {
311+
function buildListHandler(table: CleanTable, targetName?: string, typeRegistry?: TypeRegistry): t.FunctionDeclaration {
309312
const { singularName } = getTableNames(table);
310-
const selectObj = buildSelectObject(table);
313+
const selectObj = buildSelectObject(table, typeRegistry);
311314

312315
const tryBody: t.Statement[] = [
313316
buildGetClientStatement(targetName),
@@ -349,11 +352,11 @@ function buildListHandler(table: CleanTable, targetName?: string): t.FunctionDec
349352
);
350353
}
351354

352-
function buildGetHandler(table: CleanTable, targetName?: string): t.FunctionDeclaration {
355+
function buildGetHandler(table: CleanTable, targetName?: string, typeRegistry?: TypeRegistry): t.FunctionDeclaration {
353356
const { singularName } = getTableNames(table);
354357
const pkFields = getPrimaryKeyInfo(table);
355358
const pk = pkFields[0];
356-
const selectObj = buildSelectObject(table);
359+
const selectObj = buildSelectObject(table, typeRegistry);
357360

358361
const promptQuestion = t.objectExpression([
359362
t.objectProperty(t.identifier('type'), t.stringLiteral('text')),
@@ -423,38 +426,6 @@ function buildGetHandler(table: CleanTable, targetName?: string): t.FunctionDecl
423426
);
424427
}
425428

426-
/**
427-
* Get the set of field names that have defaults in the create input type.
428-
* Looks up the CreateXInput -> inner input type (e.g. DatabaseInput) in the
429-
* TypeRegistry and checks each field's defaultValue from introspection.
430-
*/
431-
/**
432-
* Resolve the inner input type from a CreateXInput or UpdateXInput type.
433-
* The CreateXInput has an inner field (e.g. "database" of type DatabaseInput)
434-
* that contains the actual field definitions.
435-
*/
436-
export function resolveInnerInputType(
437-
inputTypeName: string,
438-
typeRegistry: TypeRegistry,
439-
): { name: string; fields: Set<string> } | null {
440-
const inputType = typeRegistry.get(inputTypeName);
441-
if (!inputType?.inputFields) return null;
442-
443-
for (const inputField of inputType.inputFields) {
444-
const innerTypeName = inputField.type.name
445-
|| inputField.type.ofType?.name
446-
|| inputField.type.ofType?.ofType?.name;
447-
if (!innerTypeName) continue;
448-
449-
const innerType = typeRegistry.get(innerTypeName);
450-
if (!innerType?.inputFields) continue;
451-
452-
const fields = new Set(innerType.inputFields.map((f) => f.name));
453-
return { name: innerTypeName, fields };
454-
}
455-
return null;
456-
}
457-
458429
export function getFieldsWithDefaults(
459430
table: CleanTable,
460431
typeRegistry?: TypeRegistry,
@@ -481,22 +452,6 @@ export function getFieldsWithDefaults(
481452
return fieldsWithDefaults;
482453
}
483454

484-
/**
485-
* Get the set of field names that actually exist in the create/update input type.
486-
* Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
487-
* should be excluded from the data object in create/update handlers.
488-
*/
489-
function getWritableFieldNames(
490-
table: CleanTable,
491-
typeRegistry?: TypeRegistry,
492-
): Set<string> | null {
493-
if (!typeRegistry) return null;
494-
495-
const createInputTypeName = getCreateInputTypeName(table);
496-
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
497-
return resolved?.fields ?? null;
498-
}
499-
500455
function buildMutationHandler(
501456
table: CleanTable,
502457
operation: 'create' | 'update' | 'delete',
@@ -589,7 +544,7 @@ function buildMutationHandler(
589544
? t.objectExpression([
590545
t.objectProperty(t.identifier(pk.name), t.booleanLiteral(true)),
591546
])
592-
: buildSelectObject(table);
547+
: buildSelectObject(table, typeRegistry);
593548

594549
let ormArgs: t.ObjectExpression;
595550

@@ -1013,8 +968,8 @@ export function generateTableCommand(table: CleanTable, options?: TableCommandOp
1013968

1014969
const tn = options?.targetName;
1015970
const ormTypes = { createInputTypeName, patchTypeName, innerFieldName };
1016-
statements.push(buildListHandler(table, tn));
1017-
if (hasGet) statements.push(buildGetHandler(table, tn));
971+
statements.push(buildListHandler(table, tn, options?.typeRegistry));
972+
if (hasGet) statements.push(buildGetHandler(table, tn, options?.typeRegistry));
1018973
statements.push(buildMutationHandler(table, 'create', tn, options?.typeRegistry, ormTypes));
1019974
if (hasUpdate) statements.push(buildMutationHandler(table, 'update', tn, options?.typeRegistry, ormTypes));
1020975
if (hasDelete) statements.push(buildMutationHandler(table, 'delete', tn, options?.typeRegistry, ormTypes));

graphql/codegen/src/core/codegen/utils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
CleanField,
88
CleanFieldType,
99
CleanTable,
10+
TypeRegistry,
1011
} from '../../types/schema';
1112
import { scalarToFilterType, scalarToTsType } from './scalars';
1213

@@ -335,6 +336,68 @@ export function getScalarFields(table: CleanTable): CleanField[] {
335336
return table.fields.filter((f) => !isRelationField(f.name, table));
336337
}
337338

339+
/**
340+
* Resolve the inner input type from a CreateXInput.
341+
* PostGraphile create inputs wrap the actual field definitions in an inner type
342+
* (e.g. CreateUserInput -> { user: UserInput }) — this resolves that inner type
343+
* and returns the set of field names it contains.
344+
*/
345+
export function resolveInnerInputType(
346+
inputTypeName: string,
347+
typeRegistry: TypeRegistry,
348+
): { name: string; fields: Set<string> } | null {
349+
const inputType = typeRegistry.get(inputTypeName);
350+
if (!inputType?.inputFields) return null;
351+
352+
for (const inputField of inputType.inputFields) {
353+
const innerTypeName = inputField.type.name
354+
|| inputField.type.ofType?.name
355+
|| inputField.type.ofType?.ofType?.name;
356+
if (!innerTypeName) continue;
357+
358+
const innerType = typeRegistry.get(innerTypeName);
359+
if (!innerType?.inputFields) continue;
360+
361+
const fields = new Set(innerType.inputFields.map((f) => f.name));
362+
return { name: innerTypeName, fields };
363+
}
364+
return null;
365+
}
366+
367+
/**
368+
* Get the set of field names that actually exist in the create input type.
369+
* Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
370+
* are plugin-added computed fields that don't correspond to real database columns.
371+
* Returns null when no typeRegistry is provided (caller should treat as "no filtering").
372+
*/
373+
export function getWritableFieldNames(
374+
table: CleanTable,
375+
typeRegistry?: TypeRegistry,
376+
): Set<string> | null {
377+
if (!typeRegistry) return null;
378+
379+
const createInputTypeName = getCreateInputTypeName(table);
380+
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
381+
return resolved?.fields ?? null;
382+
}
383+
384+
/**
385+
* Get scalar fields that represent actual database columns (not computed/plugin-added).
386+
* When a TypeRegistry is provided, filters out fields that don't exist in the
387+
* create input type — these are computed fields added by plugins (e.g. search scores,
388+
* hash UUIDs) that aren't real columns and shouldn't appear in default selections.
389+
* Without a TypeRegistry, falls back to all scalar fields.
390+
*/
391+
export function getSelectableScalarFields(
392+
table: CleanTable,
393+
typeRegistry?: TypeRegistry,
394+
): CleanField[] {
395+
const writableFields = getWritableFieldNames(table, typeRegistry);
396+
return getScalarFields(table).filter(
397+
(f) => writableFields === null || writableFields.has(f.name),
398+
);
399+
}
400+
338401
/**
339402
* Primary key field information
340403
*/

0 commit comments

Comments
 (0)