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
This commit is contained in:
Dan Ribbens
2025-04-18 09:47:36 -04:00
committed by GitHub
parent b750ba4509
commit df7a3692f7
4 changed files with 31 additions and 55 deletions

View File

@@ -1,43 +1,28 @@
import type { DeleteFromSearch } from '../../types.js' import type { DeleteFromSearch } from '../../types.js'
export const deleteFromSearch: DeleteFromSearch = async ({ export const deleteFromSearch: DeleteFromSearch =
collection, (pluginConfig) =>
doc, async ({ id, collection, req: { payload }, req }) => {
pluginConfig,
req: { payload },
req,
}) => {
const searchSlug = pluginConfig?.searchOverrides?.slug || 'search' const searchSlug = pluginConfig?.searchOverrides?.slug || 'search'
try { try {
const searchDocQuery = await payload.find({ await payload.delete({
collection: searchSlug, collection: searchSlug,
depth: 0, depth: 0,
limit: 1,
pagination: false,
req, req,
where: { where: {
doc: { 'doc.relationTo': {
equals: { equals: collection.slug,
relationTo: collection.slug,
value: doc.id,
}, },
'doc.value': {
equals: id,
}, },
}, },
}) })
if (searchDocQuery?.docs?.[0]) {
await payload.delete({
id: searchDocQuery?.docs?.[0]?.id,
collection: searchSlug,
req,
})
}
} catch (err: unknown) { } catch (err: unknown) {
payload.logger.error({ payload.logger.error({
err, err,
msg: `Error deleting ${searchSlug} doc.`, msg: `Error deleting ${searchSlug} doc.`,
}) })
} }
}
return doc
}

View File

@@ -1,4 +1,4 @@
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, Config } from 'payload' import type { CollectionAfterChangeHook, Config } from 'payload'
import type { SanitizedSearchPluginConfig, SearchPluginConfig } from './types.js' import type { SanitizedSearchPluginConfig, SearchPluginConfig } from './types.js'
@@ -7,7 +7,6 @@ import { syncWithSearch } from './Search/hooks/syncWithSearch.js'
import { generateSearchCollection } from './Search/index.js' import { generateSearchCollection } from './Search/index.js'
type CollectionAfterChangeHookArgs = Parameters<CollectionAfterChangeHook>[0] type CollectionAfterChangeHookArgs = Parameters<CollectionAfterChangeHook>[0]
type CollectionAfterDeleteHookArgs = Parameters<CollectionAfterDeleteHook>[0]
export const searchPlugin = export const searchPlugin =
(incomingPluginConfig: SearchPluginConfig) => (incomingPluginConfig: SearchPluginConfig) =>
@@ -67,14 +66,9 @@ export const searchPlugin =
}) })
}, },
], ],
afterDelete: [ beforeDelete: [
...(existingHooks?.afterDelete || []), ...(existingHooks?.beforeDelete || []),
async (args: CollectionAfterDeleteHookArgs) => { deleteFromSearch(pluginConfig),
await deleteFromSearch({
...args,
pluginConfig,
})
},
], ],
}, },
} }

View File

@@ -1,6 +1,6 @@
import type { import type {
CollectionAfterChangeHook, CollectionAfterChangeHook,
CollectionAfterDeleteHook, CollectionBeforeDeleteHook,
CollectionConfig, CollectionConfig,
Field, Field,
Locale, Locale,
@@ -96,8 +96,4 @@ export type SyncDocArgs = {
// Convert the `collection` arg from `SanitizedCollectionConfig` to a string // Convert the `collection` arg from `SanitizedCollectionConfig` to a string
export type SyncWithSearch = (Args: SyncWithSearchArgs) => ReturnType<CollectionAfterChangeHook> export type SyncWithSearch = (Args: SyncWithSearchArgs) => ReturnType<CollectionAfterChangeHook>
export type DeleteFromSearch = ( export type DeleteFromSearch = (args: SearchPluginConfig) => CollectionBeforeDeleteHook
Args: {
pluginConfig: SearchPluginConfig
} & Parameters<CollectionAfterDeleteHook>[0],
) => ReturnType<CollectionAfterDeleteHook>

View File

@@ -1,5 +1,6 @@
import type { Payload } from 'payload'
import path from 'path' import path from 'path'
import { NotFound, type Payload } from 'payload'
import { wait } from 'payload/shared' import { wait } from 'payload/shared'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
@@ -300,8 +301,8 @@ describe('@payloadcms/plugin-search', () => {
collection: 'search', collection: 'search',
depth: 0, depth: 0,
where: { where: {
'doc.value': { id: {
equals: page.id, equals: results[0].id,
}, },
}, },
}) })