Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions graphql/codegen/src/__tests__/codegen/input-types-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
// Jest globals - no import needed
import {
collectCrudNestedInputTypes,
collectInputTypeNames,
collectPayloadTypeNames,
generateInputTypesFile,
Expand Down Expand Up @@ -1095,3 +1096,143 @@ describe('table field argument input types', () => {
expect(withTables.has('FooBulkUploadFileInput')).toBe(true);
});
});

describe('CRUD nested input types', () => {
it('collects INPUT_OBJECT types referenced by Create entity input fields', () => {
const table = createTable({
name: 'PlatformFunctionDefinition',
fields: [
{ name: 'id', type: fieldTypes.uuid },
{ name: 'name', type: fieldTypes.string },
],
query: {
all: 'platformFunctionDefinitions',
one: 'platformFunctionDefinition',
create: 'createPlatformFunctionDefinition',
update: 'updatePlatformFunctionDefinition',
delete: 'deletePlatformFunctionDefinition',
},
});

const registry = createTypeRegistry({
CreatePlatformFunctionDefinitionInput: {
kind: 'INPUT_OBJECT',
name: 'CreatePlatformFunctionDefinitionInput',
inputFields: [
{
name: 'platformFunctionDefinition',
type: createNonNull(createTypeRef('INPUT_OBJECT', 'PlatformFunctionDefinitionInput')),
},
],
},
PlatformFunctionDefinitionInput: {
kind: 'INPUT_OBJECT',
name: 'PlatformFunctionDefinitionInput',
inputFields: [
{
name: 'name',
type: createTypeRef('SCALAR', 'String'),
},
{
name: 'requiredConfigs',
type: createList(createTypeRef('INPUT_OBJECT', 'FunctionRequirementInput')),
},
],
},
FunctionRequirementInput: {
kind: 'INPUT_OBJECT',
name: 'FunctionRequirementInput',
inputFields: [
{
name: 'name',
type: createTypeRef('SCALAR', 'String'),
},
{
name: 'required',
type: createTypeRef('SCALAR', 'Boolean'),
},
],
},
});

const nested = collectCrudNestedInputTypes([table], registry);
expect(nested.has('FunctionRequirementInput')).toBe(true);
});

it('generates FunctionRequirementInput interface in input-types output', () => {
const table = createTable({
name: 'PlatformFunctionDefinition',
fields: [
{ name: 'id', type: fieldTypes.uuid },
{ name: 'name', type: fieldTypes.string },
],
query: {
all: 'platformFunctionDefinitions',
one: 'platformFunctionDefinition',
create: 'createPlatformFunctionDefinition',
update: 'updatePlatformFunctionDefinition',
delete: 'deletePlatformFunctionDefinition',
},
});

const registry = createTypeRegistry({
CreatePlatformFunctionDefinitionInput: {
kind: 'INPUT_OBJECT',
name: 'CreatePlatformFunctionDefinitionInput',
inputFields: [
{
name: 'platformFunctionDefinition',
type: createNonNull(createTypeRef('INPUT_OBJECT', 'PlatformFunctionDefinitionInput')),
},
],
},
PlatformFunctionDefinitionInput: {
kind: 'INPUT_OBJECT',
name: 'PlatformFunctionDefinitionInput',
inputFields: [
{
name: 'name',
type: createTypeRef('SCALAR', 'String'),
},
{
name: 'requiredConfigs',
type: createList(createTypeRef('INPUT_OBJECT', 'FunctionRequirementInput')),
},
],
},
PlatformFunctionDefinitionPatch: {
kind: 'INPUT_OBJECT',
name: 'PlatformFunctionDefinitionPatch',
inputFields: [
{
name: 'name',
type: createTypeRef('SCALAR', 'String'),
},
{
name: 'requiredConfigs',
type: createList(createTypeRef('INPUT_OBJECT', 'FunctionRequirementInput')),
},
],
},
FunctionRequirementInput: {
kind: 'INPUT_OBJECT',
name: 'FunctionRequirementInput',
inputFields: [
{
name: 'name',
type: createTypeRef('SCALAR', 'String'),
},
{
name: 'required',
type: createTypeRef('SCALAR', 'Boolean'),
},
],
},
});

const result = generateInputTypesFile(registry, new Set(), [table]);
expect(result.content).toContain('export interface FunctionRequirementInput {');
expect(result.content).toContain('name?: string;');
expect(result.content).toContain('required?: boolean;');
});
});
55 changes: 55 additions & 0 deletions graphql/codegen/src/core/codegen/orm/input-types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,55 @@ export function collectInputTypeNames(
return inputTypes;
}

/**
* Collect input type names referenced by CRUD entity input fields.
*
* When a table's Create/Patch input contains fields that reference non-scalar
* input types (e.g. `requiredConfigs: [FunctionRequirementInput]`), those
* types must be generated. The CRUD generator inlines the field types as
* strings but doesn't track which input types they reference.
*/
export function collectCrudNestedInputTypes(
tables: Table[],
typeRegistry: TypeRegistry,
): Set<string> {
const nestedTypes = new Set<string>();

for (const table of tables) {
// Check Create entity input
const createInputTypeName = getCreateInputTypeName(table);
collectNestedInputTypesFromInput(createInputTypeName, typeRegistry, nestedTypes);

// Check Patch entity input
const patchTypeName = getPatchTypeName(table);
collectNestedInputTypesFromInput(patchTypeName, typeRegistry, nestedTypes);
}

return nestedTypes;
}

function collectNestedInputTypesFromInput(
inputTypeName: string,
typeRegistry: TypeRegistry,
collected: Set<string>,
): void {
const inputType = typeRegistry.get(inputTypeName);
if (!inputType || inputType.kind !== 'INPUT_OBJECT' || !inputType.inputFields) return;

for (const field of inputType.inputFields) {
const baseName = getTypeBaseName(field.type);
if (!baseName) continue;
if (SCALAR_NAMES.has(baseName)) continue;
if (collected.has(baseName)) continue;

const refType = typeRegistry.get(baseName);
if (refType?.kind === 'INPUT_OBJECT') {
collected.add(baseName);
collectNestedInputTypesFromInput(baseName, typeRegistry, collected);
}
}
}

/**
* Build a set of exact table CRUD input type names to skip
* These are generated by generateAllCrudInputTypes, so we don't need to regenerate them
Expand Down Expand Up @@ -2084,6 +2133,12 @@ export function generateInputTypesFile(
for (const typeName of fieldArgTypes) {
mergedUsedInputTypes.add(typeName);
}
// Collect input types nested inside CRUD entity inputs (e.g. FunctionRequirementInput
// referenced by Create/Patch fields) that the CRUD generator inlines as type strings
const crudNestedTypes = collectCrudNestedInputTypes(tablesList, typeRegistry);
for (const typeName of crudNestedTypes) {
mergedUsedInputTypes.add(typeName);
}
}
const tableCrudTypes = tables ? buildTableCrudTypeNames(tables) : undefined;
// Pass customScalarTypes + enumTypes as already-generated to avoid duplicate declarations
Expand Down
Loading