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 04a395598c..2c7fbcb64a 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 05a4939dc1..fc4be4bd5d 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 4b0e4c1f36..bd6dd0d15c 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 faaa3bdb3d..cf34f031be 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, }, }, })