From df7a3692f761f777255e95c17a939daffd1b7389 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Fri, 18 Apr 2025 09:47:36 -0400 Subject: [PATCH] fix(plugin-search): delete does not also delete the search doc (#12148) The plugin-search collection uses an `afterDelete` hook to remove search records from the database. Since a deleted document in postgres causes cascade updates for the foreign key, the query for the document by relationship was not returning the record to be deleted. The solution was to change the delete hook to `beforeDelete` for the search enabled collections. This way we purge records before the main document so the search document query can find and delete the record as expected. An alternative solution in #9623 would remove the `req` so the delete query could still find the document, however, this just works outside of transactions which isn't desirable. fixes https://github.com/payloadcms/payload/issues/9443 --- .../src/Search/hooks/deleteFromSearch.ts | 57 +++++++------------ packages/plugin-search/src/index.ts | 14 ++--- packages/plugin-search/src/types.ts | 8 +-- test/plugin-search/int.spec.ts | 7 ++- 4 files changed, 31 insertions(+), 55 deletions(-) diff --git a/packages/plugin-search/src/Search/hooks/deleteFromSearch.ts b/packages/plugin-search/src/Search/hooks/deleteFromSearch.ts index 04a395598..2c7fbcb64 100644 --- a/packages/plugin-search/src/Search/hooks/deleteFromSearch.ts +++ b/packages/plugin-search/src/Search/hooks/deleteFromSearch.ts @@ -1,43 +1,28 @@ import type { DeleteFromSearch } from '../../types.js' -export const deleteFromSearch: DeleteFromSearch = async ({ - collection, - doc, - pluginConfig, - req: { payload }, - req, -}) => { - const searchSlug = pluginConfig?.searchOverrides?.slug || 'search' - try { - const searchDocQuery = await payload.find({ - collection: searchSlug, - depth: 0, - limit: 1, - pagination: false, - req, - where: { - doc: { - equals: { - relationTo: collection.slug, - value: doc.id, +export const deleteFromSearch: DeleteFromSearch = + (pluginConfig) => + async ({ id, collection, req: { payload }, req }) => { + const searchSlug = pluginConfig?.searchOverrides?.slug || 'search' + + try { + await payload.delete({ + collection: searchSlug, + depth: 0, + req, + where: { + 'doc.relationTo': { + equals: collection.slug, + }, + 'doc.value': { + equals: id, }, }, - }, - }) - - if (searchDocQuery?.docs?.[0]) { - await payload.delete({ - id: searchDocQuery?.docs?.[0]?.id, - collection: searchSlug, - req, + }) + } catch (err: unknown) { + payload.logger.error({ + err, + msg: `Error deleting ${searchSlug} doc.`, }) } - } catch (err: unknown) { - payload.logger.error({ - err, - msg: `Error deleting ${searchSlug} doc.`, - }) } - - return doc -} diff --git a/packages/plugin-search/src/index.ts b/packages/plugin-search/src/index.ts index 05a4939dc..fc4be4bd5 100644 --- a/packages/plugin-search/src/index.ts +++ b/packages/plugin-search/src/index.ts @@ -1,4 +1,4 @@ -import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, Config } from 'payload' +import type { CollectionAfterChangeHook, Config } from 'payload' import type { SanitizedSearchPluginConfig, SearchPluginConfig } from './types.js' @@ -7,7 +7,6 @@ import { syncWithSearch } from './Search/hooks/syncWithSearch.js' import { generateSearchCollection } from './Search/index.js' type CollectionAfterChangeHookArgs = Parameters[0] -type CollectionAfterDeleteHookArgs = Parameters[0] export const searchPlugin = (incomingPluginConfig: SearchPluginConfig) => @@ -67,14 +66,9 @@ export const searchPlugin = }) }, ], - afterDelete: [ - ...(existingHooks?.afterDelete || []), - async (args: CollectionAfterDeleteHookArgs) => { - await deleteFromSearch({ - ...args, - pluginConfig, - }) - }, + beforeDelete: [ + ...(existingHooks?.beforeDelete || []), + deleteFromSearch(pluginConfig), ], }, } diff --git a/packages/plugin-search/src/types.ts b/packages/plugin-search/src/types.ts index 4b0e4c1f3..bd6dd0d15 100644 --- a/packages/plugin-search/src/types.ts +++ b/packages/plugin-search/src/types.ts @@ -1,6 +1,6 @@ import type { CollectionAfterChangeHook, - CollectionAfterDeleteHook, + CollectionBeforeDeleteHook, CollectionConfig, Field, Locale, @@ -96,8 +96,4 @@ export type SyncDocArgs = { // Convert the `collection` arg from `SanitizedCollectionConfig` to a string export type SyncWithSearch = (Args: SyncWithSearchArgs) => ReturnType -export type DeleteFromSearch = ( - Args: { - pluginConfig: SearchPluginConfig - } & Parameters[0], -) => ReturnType +export type DeleteFromSearch = (args: SearchPluginConfig) => CollectionBeforeDeleteHook diff --git a/test/plugin-search/int.spec.ts b/test/plugin-search/int.spec.ts index faaa3bdb3..cf34f031b 100644 --- a/test/plugin-search/int.spec.ts +++ b/test/plugin-search/int.spec.ts @@ -1,5 +1,6 @@ +import type { Payload } from 'payload' + import path from 'path' -import { NotFound, type Payload } from 'payload' import { wait } from 'payload/shared' import { fileURLToPath } from 'url' @@ -300,8 +301,8 @@ describe('@payloadcms/plugin-search', () => { collection: 'search', depth: 0, where: { - 'doc.value': { - equals: page.id, + id: { + equals: results[0].id, }, }, })