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
8 changes: 4 additions & 4 deletions graphql/codegen/src/__tests__/introspect/infer-tables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ describe('Mutation Operation Matching', () => {
fields: [{ name: 'id', type: nonNull(scalar('UUID')) }],
},
{ name: 'UsersConnection', kind: 'OBJECT', fields: [] },
{ name: 'CreateUserPayload', kind: 'OBJECT', fields: [] },
{ name: 'CreateUserPayload', kind: 'OBJECT', fields: [{ name: 'user', type: object('User') }] },
],
[{ name: 'users', type: object('UsersConnection') }],
[
Expand Down Expand Up @@ -760,8 +760,8 @@ describe('Mutation Operation Matching', () => {
fields: [{ name: 'id', type: nonNull(scalar('UUID')) }],
},
{ name: 'UsersConnection', kind: 'OBJECT', fields: [] },
{ name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] },
{ name: 'DeleteUserPayload', kind: 'OBJECT', fields: [] },
{ name: 'UpdateUserPayload', kind: 'OBJECT', fields: [{ name: 'user', type: object('User') }] },
{ name: 'DeleteUserPayload', kind: 'OBJECT', fields: [{ name: 'user', type: object('User') }] },
],
[{ name: 'users', type: object('UsersConnection') }],
[
Expand Down Expand Up @@ -797,7 +797,7 @@ describe('Mutation Operation Matching', () => {
fields: [{ name: 'id', type: nonNull(scalar('UUID')) }],
},
{ name: 'UsersConnection', kind: 'OBJECT', fields: [] },
{ name: 'UpdateUserPayload', kind: 'OBJECT', fields: [] },
{ name: 'UpdateUserPayload', kind: 'OBJECT', fields: [{ name: 'user', type: object('User') }] },
],
[{ name: 'users', type: object('UsersConnection') }],
[
Expand Down
43 changes: 37 additions & 6 deletions graphql/query/src/introspect/infer-tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function buildCleanTable(

// Match query and mutation operations
const queryOps = matchQueryOperations(entityName, queryFields, entityToConnection);
const mutationOps = matchMutationOperations(entityName, mutationFields);
const mutationOps = matchMutationOperations(entityName, mutationFields, typeMap);

// Check if we found at least one real operation (not a fallback)
const hasRealOperation = !!(
Expand Down Expand Up @@ -699,6 +699,30 @@ interface MutationOperations {
bulkDelete: string | null;
}

/**
* Check whether a mutation field is a real PostGraphile CRUD mutation.
*
* CRUD mutation payloads always contain a field named lcFirst(entityName)
* whose type is the entity itself (e.g. DeleteUserPayload has `user: User`).
* Custom SQL-function mutations that happen to follow the same naming
* convention (e.g. `deletePrincipal`) return something else — typically
* `result: Boolean` or `result: UUID` — and must not be treated as CRUD.
*/
function isCrudMutation(
field: IntrospectionField,
entityName: string,
typeMap: Map<string, IntrospectionType>,
): boolean {
const payloadTypeName = getBaseTypeName(field.type);
if (!payloadTypeName) return false;
const payloadType = typeMap.get(payloadTypeName);
if (!payloadType || !payloadType.fields) return false;
const entityFieldName = lcFirst(entityName);
return payloadType.fields.some(
(f) => f.name === entityFieldName && getBaseTypeName(f.type) === entityName,
);
}

/**
* Match mutation operations for an entity
*
Expand All @@ -710,10 +734,15 @@ interface MutationOperations {
* - bulkUpsert{PluralName} (bulk upsert)
* - bulkUpdate{PluralName} (bulk update)
* - bulkDelete{PluralName} (bulk delete)
*
* A candidate is only accepted if its payload type returns the entity
* (i.e. it is a real CRUD mutation, not a custom SQL function that
* happens to share the naming convention).
*/
function matchMutationOperations(
entityName: string,
mutationFields: IntrospectionField[],
typeMap: Map<string, IntrospectionType>,
): MutationOperations {
let create: string | null = null;
let update: string | null = null;
Expand All @@ -736,28 +765,30 @@ function matchMutationOperations(

for (const field of mutationFields) {
// Exact match for create
if (field.name === expectedCreate) {
if (field.name === expectedCreate && isCrudMutation(field, entityName, typeMap)) {
create = field.name;
}

// Match update (could be updateUser, updateUserById, or updateUserByFooAndBar for composite PKs)
if (field.name === expectedUpdate) {
if (field.name === expectedUpdate && isCrudMutation(field, entityName, typeMap)) {
update = field.name;
} else if (
!update &&
(field.name === `${expectedUpdate}ById` ||
field.name.startsWith(`${expectedUpdate}By`))
field.name.startsWith(`${expectedUpdate}By`)) &&
isCrudMutation(field, entityName, typeMap)
) {
update = field.name;
}

// Match delete (could be deleteUser, deleteUserById, or deleteUserByFooAndBar for composite PKs)
if (field.name === expectedDelete) {
if (field.name === expectedDelete && isCrudMutation(field, entityName, typeMap)) {
del = field.name;
} else if (
!del &&
(field.name === `${expectedDelete}ById` ||
field.name.startsWith(`${expectedDelete}By`))
field.name.startsWith(`${expectedDelete}By`)) &&
isCrudMutation(field, entityName, typeMap)
) {
del = field.name;
}
Expand Down
Loading
Loading