Skip to content

Commit fabee97

Browse files
committed
refactor(codegen): extract getSelectableScalarFields into shared utils
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.
1 parent d96c8c1 commit fabee97

2 files changed

Lines changed: 67 additions & 52 deletions

File tree

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

Lines changed: 4 additions & 52 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,
@@ -151,10 +154,7 @@ function buildFieldSchemaObject(table: CleanTable): t.ObjectExpression {
151154
}
152155

153156
function buildSelectObject(table: CleanTable, typeRegistry?: TypeRegistry): t.ObjectExpression {
154-
const writableFields = getWritableFieldNames(table, typeRegistry);
155-
const fields = getScalarFields(table).filter(
156-
(f) => writableFields === null || writableFields.has(f.name),
157-
);
157+
const fields = getSelectableScalarFields(table, typeRegistry);
158158
return t.objectExpression(
159159
fields.map((f) =>
160160
t.objectProperty(t.identifier(f.name), t.booleanLiteral(true)),
@@ -426,38 +426,6 @@ function buildGetHandler(table: CleanTable, targetName?: string, typeRegistry?:
426426
);
427427
}
428428

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

487-
/**
488-
* Get the set of field names that actually exist in the create/update input type.
489-
* Fields not in this set (e.g. computed fields like searchTsvRank, hashUuid)
490-
* should be excluded from the data object in create/update handlers.
491-
*/
492-
function getWritableFieldNames(
493-
table: CleanTable,
494-
typeRegistry?: TypeRegistry,
495-
): Set<string> | null {
496-
if (!typeRegistry) return null;
497-
498-
const createInputTypeName = getCreateInputTypeName(table);
499-
const resolved = resolveInnerInputType(createInputTypeName, typeRegistry);
500-
return resolved?.fields ?? null;
501-
}
502-
503455
function buildMutationHandler(
504456
table: CleanTable,
505457
operation: 'create' | 'update' | 'delete',

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)