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
105 changes: 99 additions & 6 deletions graphql/codegen/src/core/codegen/orm/input-types-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,10 +1077,38 @@ function getFilterTypeForField(fieldType: string, isArray = false): string {
}

/**
* Build properties for a table filter interface
* Build properties for a table filter interface.
*
* When typeRegistry is available, uses the schema's filter input type as
* the sole source of truth — this captures plugin-injected filter fields
* (e.g., bm25Body, tsvTsv, trgmName, vectorEmbedding, geom) that are not
* present on the entity type itself. Same pattern as buildOrderByValues().
*/
function buildTableFilterProperties(table: Table): InterfaceProperty[] {
function buildTableFilterProperties(
table: Table,
typeRegistry?: TypeRegistry,
): InterfaceProperty[] {
const filterName = getFilterTypeName(table);

// When the schema's filter type is available, use it as the source of truth
if (typeRegistry) {
const filterType = typeRegistry.get(filterName);
if (filterType?.kind === 'INPUT_OBJECT' && filterType.inputFields) {
const properties: InterfaceProperty[] = [];
for (const field of filterType.inputFields) {
const tsType = typeRefToTs(field.type);
properties.push({
name: field.name,
type: tsType,
optional: true,
description: stripSmartComments(field.description, true),
});
}
return properties;
}
}

// Fallback: derive from table fields when schema filter type is not available
const properties: InterfaceProperty[] = [];

for (const field of table.fields) {
Expand All @@ -1105,13 +1133,19 @@ function buildTableFilterProperties(table: Table): InterfaceProperty[] {
/**
* Generate table filter type statements
*/
function generateTableFilterTypes(tables: Table[]): t.Statement[] {
function generateTableFilterTypes(
tables: Table[],
typeRegistry?: TypeRegistry,
): t.Statement[] {
const statements: t.Statement[] = [];

for (const table of tables) {
const filterName = getFilterTypeName(table);
statements.push(
createExportedInterface(filterName, buildTableFilterProperties(table)),
createExportedInterface(
filterName,
buildTableFilterProperties(table, typeRegistry),
),
);
}

Expand Down Expand Up @@ -1956,6 +1990,54 @@ function generateConnectionFieldsMap(
// Plugin-Injected Type Collector
// ============================================================================

/**
* Collect extra input type names referenced by plugin-injected filter fields.
*
* When the schema's filter type is used as source of truth, plugin-injected
* fields reference custom filter types (e.g., Bm25BodyFilter, TsvectorFilter,
* GeometryFilter) that also need to be generated. This function discovers
* those types by comparing the schema's filter type fields against the
* standard scalar filter types.
*/
function collectFilterExtraInputTypes(
tables: Table[],
typeRegistry: TypeRegistry,
): Set<string> {
const extraTypes = new Set<string>();

for (const table of tables) {
const filterTypeName = getFilterTypeName(table);
const filterType = typeRegistry.get(filterTypeName);
if (
!filterType ||
filterType.kind !== 'INPUT_OBJECT' ||
!filterType.inputFields
) {
continue;
}

const tableFieldNames = new Set(
table.fields
.filter((f) => !isRelationField(f.name, table))
.map((f) => f.name),
);

for (const field of filterType.inputFields) {
// Skip standard column-derived fields and logical operators
if (tableFieldNames.has(field.name)) continue;
if (['and', 'or', 'not'].includes(field.name)) continue;

// Collect the base type name of this extra field
const baseName = getTypeBaseName(field.type);
if (baseName && !SCALAR_NAMES.has(baseName)) {
extraTypes.add(baseName);
}
}
}

return extraTypes;
}

/**
* Collect extra input type names referenced by plugin-injected condition fields.
*
Expand Down Expand Up @@ -2048,7 +2130,9 @@ export function generateInputTypesFile(
statements.push(...generateEntitySelectTypes(tablesList, tableByName));

// 4. Table filter types
statements.push(...generateTableFilterTypes(tablesList));
// Pass typeRegistry to use schema's filter type as source of truth,
// capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
statements.push(...generateTableFilterTypes(tablesList, typeRegistry));

// 4b. Table condition types (simple equality filter)
// Pass typeRegistry to merge plugin-injected condition fields
Expand All @@ -2071,8 +2155,17 @@ export function generateInputTypesFile(
statements.push(...generateConnectionFieldsMap(tablesList, tableByName));

// 7. Custom input types from TypeRegistry
// Also include any extra types referenced by plugin-injected condition fields
// Also include any extra types referenced by plugin-injected filter/condition fields
const mergedUsedInputTypes = new Set(usedInputTypes);
if (hasTables) {
const filterExtraTypes = collectFilterExtraInputTypes(
tablesList,
typeRegistry,
);
for (const typeName of filterExtraTypes) {
mergedUsedInputTypes.add(typeName);
}
}
if (hasTables && conditionEnabled) {
const conditionExtraTypes = collectConditionExtraInputTypes(
tablesList,
Expand Down
37 changes: 18 additions & 19 deletions graphql/query/src/introspect/infer-tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,19 +723,19 @@ function inferConstraints(
const updateInput = typeMap.get(updateInputName);
const deleteInput = typeMap.get(deleteInputName);

const keyInputField =
inferPrimaryKeyFromInputObject(updateInput) ||
inferPrimaryKeyFromInputObject(deleteInput);
// Prefer Delete input (fewer non-PK fields) over Update input
const keyFields =
inferPrimaryKeyFromInputObject(deleteInput).length > 0
? inferPrimaryKeyFromInputObject(deleteInput)
: inferPrimaryKeyFromInputObject(updateInput);
Comment on lines +727 to +730
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Redundant double invocation of inferPrimaryKeyFromInputObject(deleteInput)

The ternary expression calls inferPrimaryKeyFromInputObject(deleteInput) twice — once to check .length > 0 and again to obtain the result. The function iterates through inputFields with .find() and .filter() each time. While the function is pure and this doesn't cause incorrect behavior, it unnecessarily performs the work twice. The result of the first call should be stored in a variable.

Suggested change
const keyFields =
inferPrimaryKeyFromInputObject(deleteInput).length > 0
? inferPrimaryKeyFromInputObject(deleteInput)
: inferPrimaryKeyFromInputObject(updateInput);
const deleteKeyFields = inferPrimaryKeyFromInputObject(deleteInput);
const keyFields = deleteKeyFields.length > 0
? deleteKeyFields
: inferPrimaryKeyFromInputObject(updateInput);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


if (keyInputField) {
if (keyFields.length > 0) {
primaryKey.push({
name: 'primary',
fields: [
{
name: keyInputField.name,
type: convertToCleanFieldType(keyInputField.type),
},
],
fields: keyFields.map((f) => ({
name: f.name,
type: convertToCleanFieldType(f.type),
})),
});
}

Expand Down Expand Up @@ -769,27 +769,28 @@ function inferConstraints(
}

/**
* Infer a single-row lookup key from an Update/Delete input object.
* Infer primary key fields from an Update/Delete input object.
*
* Priority:
* 1. Canonical keys: id, nodeId, rowId
* 2. Single non-patch/non-clientMutationId scalar-ish field
* 2. All non-patch/non-clientMutationId fields (supports composite keys)
*
* If multiple possible key fields remain, return null to avoid guessing.
* Returns all candidate key fields, enabling composite PK detection
* for junction tables like PostTag(postId, tagId).
*/
function inferPrimaryKeyFromInputObject(
inputType: IntrospectionType | undefined,
): IntrospectionInputValue | null {
): IntrospectionInputValue[] {
const inputFields = inputType?.inputFields ?? [];
if (inputFields.length === 0) return null;
if (inputFields.length === 0) return [];

const canonicalKey = inputFields.find(
(field) =>
field.name === 'id' || field.name === 'nodeId' || field.name === 'rowId',
);
if (canonicalKey) return canonicalKey;
if (canonicalKey) return [canonicalKey];

const candidates = inputFields.filter((field) => {
return inputFields.filter((field) => {
if (field.name === 'clientMutationId') return false;

const baseTypeName = getBaseTypeName(field.type);
Expand All @@ -801,8 +802,6 @@ function inferPrimaryKeyFromInputObject(

return true;
});

return candidates.length === 1 ? candidates[0] : null;
}

/**
Expand Down
Loading