diff --git a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap index 1fa921534..6d2071bfd 100644 --- a/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap +++ b/graphql/codegen/src/__tests__/codegen/__snapshots__/react-query-hooks.test.ts.snap @@ -770,7 +770,7 @@ export const deleteUserMutationDocument = \` mutation DeleteUserMutation($input: DeleteUserInput!) { deleteUser(input: $input) { clientMutationId - deletedId + deletedUserNodeId } } \`; @@ -782,7 +782,7 @@ export interface DeleteUserMutationVariables { export interface DeleteUserMutationResult { deleteUser: { clientMutationId: string | null; - deletedId: string | null; + deletedUserNodeId: string | null; }; } /** @@ -834,7 +834,7 @@ export const deletePostMutationDocument = \` mutation DeletePostMutation($input: DeletePostInput!) { deletePost(input: $input) { clientMutationId - deletedId + deletedPostNodeId } } \`; @@ -846,7 +846,7 @@ export interface DeletePostMutationVariables { export interface DeletePostMutationResult { deletePost: { clientMutationId: string | null; - deletedId: string | null; + deletedPostNodeId: string | null; }; } /** @@ -895,7 +895,7 @@ export const deleteUserMutationDocument = \` mutation DeleteUserMutation($input: DeleteUserInput!) { deleteUser(input: $input) { clientMutationId - deletedId + deletedUserNodeId } } \`; @@ -907,7 +907,7 @@ export interface DeleteUserMutationVariables { export interface DeleteUserMutationResult { deleteUser: { clientMutationId: string | null; - deletedId: string | null; + deletedUserNodeId: string | null; }; } /** diff --git a/graphql/codegen/src/cli/codegen/gql-ast.ts b/graphql/codegen/src/cli/codegen/gql-ast.ts index ec2c13aac..8dc14c157 100644 --- a/graphql/codegen/src/cli/codegen/gql-ast.ts +++ b/graphql/codegen/src/cli/codegen/gql-ast.ts @@ -12,7 +12,7 @@ import type { ArgumentNode, VariableDefinitionNode, } from 'graphql'; -import type { CleanTable, CleanField } from '../../types/schema'; +import type { CleanTable, CleanField, TypeRegistry } from '../../types/schema'; import { getTableNames, getAllRowsQueryName, @@ -26,6 +26,8 @@ import { getScalarFields, getPrimaryKeyInfo, ucFirst, + getDeletedNodeIdFieldName, + getDeletePayloadTypeName, } from './utils'; @@ -345,16 +347,24 @@ export function buildUpdateMutationAST(config: UpdateMutationConfig): DocumentNo export interface DeleteMutationConfig { table: CleanTable; + /** TypeRegistry for looking up actual payload field names from schema */ + typeRegistry?: TypeRegistry; } /** * Build a delete mutation AST for a table */ export function buildDeleteMutationAST(config: DeleteMutationConfig): DocumentNode { - const { table } = config; + const { table, typeRegistry } = config; const { typeName } = getTableNames(table); const mutationName = getDeleteMutationName(table); const inputTypeName = `Delete${typeName}Input`; + const deletePayloadTypeName = getDeletePayloadTypeName(table); + const deletedNodeIdFieldName = getDeletedNodeIdFieldName( + typeRegistry, + deletePayloadTypeName, + typeName + ); // Variable definitions const variableDefinitions: VariableDefinitionNode[] = [ @@ -383,7 +393,7 @@ export function buildDeleteMutationAST(config: DeleteMutationConfig): DocumentNo selectionSet: t.selectionSet({ selections: [ t.field({ name: 'clientMutationId' }), - t.field({ name: 'deletedId' }), + t.field({ name: deletedNodeIdFieldName }), ], }), }), diff --git a/graphql/codegen/src/cli/codegen/index.ts b/graphql/codegen/src/cli/codegen/index.ts index 8778b7406..0cb931a2d 100644 --- a/graphql/codegen/src/cli/codegen/index.ts +++ b/graphql/codegen/src/cli/codegen/index.ts @@ -256,6 +256,7 @@ export function generate(options: GenerateOptions): GenerateResult { useCentralizedKeys, hasRelationships, tableTypeNames, + typeRegistry: customOperations?.typeRegistry, }); for (const hook of mutationHooks) { files.push({ diff --git a/graphql/codegen/src/cli/codegen/mutations.ts b/graphql/codegen/src/cli/codegen/mutations.ts index 11596b3fe..d7df7362b 100644 --- a/graphql/codegen/src/cli/codegen/mutations.ts +++ b/graphql/codegen/src/cli/codegen/mutations.ts @@ -7,7 +7,7 @@ * useUpdateCarMutation.ts * useDeleteCarMutation.ts */ -import type { CleanTable } from '../../types/schema'; +import type { CleanTable, TypeRegistry } from '../../types/schema'; import * as t from '@babel/types'; import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast'; import { @@ -33,6 +33,8 @@ import { ucFirst, lcFirst, getGeneratedFileHeader, + getDeletedNodeIdFieldName, + getDeletePayloadTypeName, } from './utils'; function isAutoGeneratedField(fieldName: string, pkFieldNames: Set): boolean { @@ -58,6 +60,8 @@ export interface MutationGeneratorOptions { hasRelationships?: boolean; /** All table type names for determining which types to import from types.ts vs schema-types.ts */ tableTypeNames?: Set; + /** TypeRegistry for looking up actual payload field names from schema introspection */ + typeRegistry?: TypeRegistry; } export function generateCreateMutationHook( @@ -688,6 +692,7 @@ export function generateDeleteMutationHook( reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, + typeRegistry, } = options; if (!reactQueryEnabled) { @@ -708,7 +713,15 @@ export function generateDeleteMutationHook( const pkFields = getPrimaryKeyInfo(table); const pkField = pkFields[0]; - const mutationAST = buildDeleteMutationAST({ table }); + // Get the correct deletedNodeId field name from schema or convention + const deletePayloadTypeName = getDeletePayloadTypeName(table); + const deletedNodeIdFieldName = getDeletedNodeIdFieldName( + typeRegistry, + deletePayloadTypeName, + typeName + ); + + const mutationAST = buildDeleteMutationAST({ table, typeRegistry }); const mutationDocument = printGraphQL(mutationAST); const statements: t.Statement[] = []; @@ -792,9 +805,10 @@ export function generateDeleteMutationHook( ); statements.push(t.exportNamedDeclaration(variablesInterface)); - const deletedPkProp = t.tsPropertySignature( - t.identifier(`deleted${ucFirst(pkField.name)}`), - t.tsTypeAnnotation(t.tsUnionType([pkTypeAnnotation, t.tsNullKeyword()])) + // Use the schema-inferred field name for the deleted node ID + const deletedNodeIdProp = t.tsPropertySignature( + t.identifier(deletedNodeIdFieldName), + t.tsTypeAnnotation(t.tsUnionType([t.tsStringKeyword(), t.tsNullKeyword()])) ); const clientMutationIdProp = t.tsPropertySignature( t.identifier('clientMutationId'), @@ -804,7 +818,7 @@ export function generateDeleteMutationHook( const resultInterfaceBody = t.tsInterfaceBody([ t.tsPropertySignature( t.identifier(mutationName), - t.tsTypeAnnotation(t.tsTypeLiteral([clientMutationIdProp, deletedPkProp])) + t.tsTypeAnnotation(t.tsTypeLiteral([clientMutationIdProp, deletedNodeIdProp])) ), ]); const resultInterface = t.tsInterfaceDeclaration( diff --git a/graphql/codegen/src/cli/codegen/utils.ts b/graphql/codegen/src/cli/codegen/utils.ts index dc46b8703..0af9d7d06 100644 --- a/graphql/codegen/src/cli/codegen/utils.ts +++ b/graphql/codegen/src/cli/codegen/utils.ts @@ -5,6 +5,7 @@ import type { CleanTable, CleanField, CleanFieldType, + TypeRegistry, } from '../../types/schema'; import { scalarToTsType, scalarToFilterType } from './scalars'; import { pluralize } from 'inflekt'; @@ -412,6 +413,51 @@ export function getQueryKeyPrefix(table: CleanTable): string { return lcFirst(table.name); } +// ============================================================================ +// Delete mutation helpers +// ============================================================================ + +/** + * Get the deleted node ID field name from a delete mutation payload. + * + * PostGraphile generates delete payloads with a field following the pattern + * `deleted{EntityName}NodeId`. This function looks up the actual field name + * from the TypeRegistry if available, or falls back to the convention. + * + * @param typeRegistry - The type registry from schema introspection + * @param deletePayloadTypeName - The payload type name (e.g., "DeleteUserPayload") + * @param entityTypeName - The entity type name (e.g., "User") for fallback + * @returns The field name (e.g., "deletedUserNodeId") + */ +export function getDeletedNodeIdFieldName( + typeRegistry: TypeRegistry | undefined, + deletePayloadTypeName: string, + entityTypeName: string +): string { + if (typeRegistry) { + const payloadType = typeRegistry.get(deletePayloadTypeName); + if (payloadType?.fields) { + // Find the field that matches the pattern deleted*NodeId + const deletedNodeIdField = payloadType.fields.find( + (f) => f.name.startsWith('deleted') && f.name.endsWith('NodeId') + ); + if (deletedNodeIdField) { + return deletedNodeIdField.name; + } + } + } + // Fallback to PostGraphile naming convention + return `deleted${entityTypeName}NodeId`; +} + +/** + * Get the delete payload type name for a table. + * Uses inflection if available, otherwise falls back to convention. + */ +export function getDeletePayloadTypeName(table: CleanTable): string { + return table.inflection?.deletePayloadType || `Delete${table.name}Payload`; +} + // ============================================================================ // Code generation helpers // ============================================================================