diff --git a/graphile/graphile-settings/src/plugins/meta-schema/relation-meta-builders.ts b/graphile/graphile-settings/src/plugins/meta-schema/relation-meta-builders.ts index d35d99547..1b4a798f4 100644 --- a/graphile/graphile-settings/src/plugins/meta-schema/relation-meta-builders.ts +++ b/graphile/graphile-settings/src/plugins/meta-schema/relation-meta-builders.ts @@ -43,12 +43,14 @@ export function buildBelongsToRelations( sameAttributes(unique.attributes, localAttributes), ); + const remoteCodec = relation.remoteResource?.codec; + belongsTo.push({ fieldName: relationName, isUnique, - type: relation.remoteResource?.codec?.name || null, + type: remoteCodec?.name || null, keys: buildFieldList(localAttributes, codec, attributes, context), - references: { name: relation.remoteResource?.codec?.name || 'unknown' }, + references: { name: remoteCodec?.name || 'unknown' }, }); } @@ -73,12 +75,14 @@ export function buildReverseRelations( sameAttributes(unique.attributes, remoteAttributes), ); + const remoteCodec = relation.remoteResource?.codec; + const meta: HasRelation = { fieldName: relationName, isUnique, - type: relation.remoteResource?.codec?.name || null, + type: remoteCodec?.name || null, keys: buildFieldList(relation.localAttributes || [], codec, attributes, context), - referencedBy: { name: relation.remoteResource?.codec?.name || 'unknown' }, + referencedBy: { name: remoteCodec?.name || 'unknown' }, }; if (isUnique) { diff --git a/graphql/query/src/generators/field-selector.ts b/graphql/query/src/generators/field-selector.ts index 6aa295d3f..033e8a373 100644 --- a/graphql/query/src/generators/field-selector.ts +++ b/graphql/query/src/generators/field-selector.ts @@ -293,11 +293,23 @@ function getRelatedTableScalarFields( return {}; } - // Find the related table in allTables - const relatedTable = allTables.find((t) => t.name === referencedTableName); + // Find the related table in allTables. + // PostGraphile v5 uses different inflections in different contexts: + // - table.name: PascalCase tableType (e.g., "Shipment", "DriverVehicleAssignment") + // - relation referencedBy.name: raw codec name (e.g., "shipments", "driverVehicleAssignments") + // Try exact match first, then case-insensitive match with optional trailing 's' for plural. + const nameLower = referencedTableName.toLowerCase().replace(/_/g, ''); + const nameBase = nameLower.endsWith('s') ? nameLower.slice(0, -1) : nameLower; + const relatedTable = + allTables.find((t) => t.name === referencedTableName) ?? + allTables.find((t) => { + const tLower = t.name.toLowerCase().replace(/_/g, ''); + return tLower === nameLower || tLower === nameBase; + }); if (!relatedTable) { - // Related table not found in schema - return empty selection - return {}; + // Related table not found in schema — return fallback { __typename: true } + // so the query remains valid (nodes need at least one subfield). + return { __typename: true }; } // Get ALL scalar fields from the related table (non-relational fields) diff --git a/graphql/query/src/generators/select.ts b/graphql/query/src/generators/select.ts index 9f158da40..72167bc71 100644 --- a/graphql/query/src/generators/select.ts +++ b/graphql/query/src/generators/select.ts @@ -788,7 +788,18 @@ function findRelatedTable( } // Find the related table in allTables - return allTables.find((tbl) => tbl.name === referencedTableName) || null; + const exactMatch = allTables.find((tbl) => tbl.name === referencedTableName); + if (exactMatch) return exactMatch; + + // Fuzzy match: case-insensitive, strip underscores, optional trailing 's'. + // Needed because relation target names from _meta use snake_case codec names + // (e.g. "routes", "delivery_zone") while allTables[].name is PascalCase (e.g. "Route", "DeliveryZone"). + const nameLower = referencedTableName.toLowerCase().replace(/_/g, ''); + const nameBase = nameLower.endsWith('s') ? nameLower.slice(0, -1) : nameLower; + return allTables.find((tbl) => { + const tLower = tbl.name.toLowerCase().replace(/_/g, ''); + return tLower === nameLower || tLower === nameBase; + }) || null; } /**