Skip to content
Open
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
49 changes: 43 additions & 6 deletions packages/plugin-search/src/utilities/generateReindexHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
initTransaction,
killTransaction,
} from 'payload'
import { hasLocalizeStatusEnabled } from 'payload/shared'

import type { SanitizedSearchPluginConfig } from '../types.js'

Expand Down Expand Up @@ -89,12 +90,29 @@ export const generateReindexHandler =
equals: 'published',
},
}
// Localized `_status` tracks published state per-locale; match docs published in any locale.
function buildPublishedWhere(collection: string): Where {
const collectionConfig = payload.collections[collection]?.config
const localeCodes = payload.config.localization ? payload.config.localization.localeCodes : []

if (collectionConfig && hasLocalizeStatusEnabled(collectionConfig) && localeCodes.length) {
return {
or: localeCodes.map((localeCode) => ({
[`_status.${localeCode}`]: {
equals: 'published',
},
})),
}
}

return whereStatusPublished
}
async function countDocuments(collection: string, drafts?: boolean): Promise<number> {
const { totalDocs } = await payload.count({
collection,
...defaultLocalApiProps,
req: undefined,
where: drafts ? undefined : whereStatusPublished,
where: drafts ? undefined : buildPublishedWhere(collection),
})
return totalDocs
}
Expand All @@ -112,7 +130,11 @@ export const generateReindexHandler =
async function reindexCollection(
collection: string,
): Promise<{ docs: number; docsWithDrafts: number; errors: number }> {
const draftsEnabled = Boolean(payload.collections[collection]?.config.versions?.drafts)
const collectionConfig = payload.collections[collection]?.config
const draftsEnabled = Boolean(collectionConfig?.versions?.drafts)
const localizeStatusEnabled = collectionConfig
? hasLocalizeStatusEnabled(collectionConfig)
: false

const totalDocsWithDrafts = await countDocuments(collection, true)
const totalDocs =
Expand All @@ -133,9 +155,10 @@ export const generateReindexHandler =
collection,
depth: 0,
limit: batchSize,
locale: defaultLocale,
// Fetch all locales so each locale's `_status` is available for gating below
locale: localizeStatusEnabled ? 'all' : defaultLocale,
page: i + 1,
where: syncDrafts || !draftsEnabled ? undefined : whereStatusPublished,
where: syncDrafts || !draftsEnabled ? undefined : buildPublishedWhere(collection),
...defaultLocalApiProps,
})

Expand Down Expand Up @@ -171,14 +194,28 @@ export const generateReindexHandler =
continue // Skip this locale
}

// Skip locales that aren't published (unless syncing drafts)
let docToSync = doc
if (localizeStatusEnabled) {
const localeStatus = (doc as { _status?: Record<string, string> })._status?.[
localeToSync as string
]

if (!syncDrafts && localeStatus !== 'published') {
continue
}

docToSync = { ...doc, _status: localeStatus }
}

// Sync this locale (create first index, then update with other locales accordingly)
const operation = firstAllowedLocale ? 'create' : 'update'
firstAllowedLocale = false

await syncDocAsSearchIndex({
collection,
data: doc,
doc,
data: docToSync,
doc: docToSync,
locale: localeToSync,
onSyncError: () => operation === 'create' && localErrors++,
operation,
Expand Down
37 changes: 37 additions & 0 deletions test/plugin-search/collections/LocalizedStatusPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { CollectionConfig } from 'payload'

import { localizedStatusPostsSlug } from '../shared.js'

export const LocalizedStatusPosts: CollectionConfig = {
slug: localizedStatusPostsSlug,
labels: {
singular: 'Localized Status Post',
plural: 'Localized Status Posts',
},
admin: {
useAsTitle: 'title',
},
versions: {
drafts: {
localizeStatus: true,
},
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
{
name: 'excerpt',
label: 'Excerpt',
type: 'text',
},
{
type: 'text',
name: 'slug',
localized: true,
},
],
}
14 changes: 13 additions & 1 deletion test/plugin-search/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ import type { Config } from './payload-types.js'

import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { LocalizedStatusPosts } from './collections/LocalizedStatusPosts.js'
import { Pages } from './collections/Pages.js'
import { Posts } from './collections/Posts.js'
import { Users } from './collections/Users.js'
import { seed } from './seed/index.js'

export default buildConfigWithDefaults({
experimental: {
localizeStatus: true,
},
collections: [
Users,
Pages,
Posts,
LocalizedStatusPosts,
{
slug: 'custom-ids-1',
fields: [{ type: 'text', name: 'id' }],
Expand Down Expand Up @@ -72,7 +77,14 @@ export default buildConfigWithDefaults({
slug: originalDoc.slug,
}
},
collections: ['pages', 'posts', 'custom-ids-1', 'custom-ids-2', 'filtered-locales'],
collections: [
'pages',
'posts',
'custom-ids-1',
'custom-ids-2',
'filtered-locales',
'localized-status-posts',
],
skipSync: ({ locale, doc, collectionSlug }) => {
if (collectionSlug === 'filtered-locales' && doc.syncEnglishOnly) {
return locale !== 'en'
Expand Down
73 changes: 72 additions & 1 deletion test/plugin-search/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { NextRESTClient } from '../__helpers/shared/NextRESTClient.js'

import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js'
import { devUser } from '../credentials.js'
import { pagesSlug, postsSlug } from './shared.js'
import { localizedStatusPostsSlug, pagesSlug, postsSlug } from './shared.js'

let payload: Payload
let restClient: NextRESTClient
Expand Down Expand Up @@ -63,6 +63,15 @@ describe('@payloadcms/plugin-search', () => {
},
},
}),
payload.delete({
collection: localizedStatusPostsSlug,
depth: 0,
where: {
id: {
exists: true,
},
},
}),
])
})

Expand Down Expand Up @@ -762,6 +771,68 @@ describe('@payloadcms/plugin-search', () => {
expect(postAfterReindex?.slug).toStrictEqual(postBeforeReindex?.slug)
})

it('should reindex documents published only in a non-default locale when status is localized', async () => {
// Draft in the default locale ('en'), published only in a non-default locale ('de')
const post = await payload.create({
collection: localizedStatusPostsSlug,
locale: 'en',
data: {
title: 'Published in DE only',
_status: 'draft',
slug: 'de-only-en',
},
})

await payload.update({
collection: localizedStatusPostsSlug,
id: post.id,
locale: 'de',
data: {
_status: 'published',
slug: 'de-only-de',
},
})

const endpointRes = await restClient.POST(`/search/reindex`, {
body: JSON.stringify({
collections: [localizedStatusPostsSlug],
}),
headers: {
Authorization: `JWT ${token}`,
},
})

expect(endpointRes.status).toBe(200)

const { docs } = await payload.find({
collection: 'search',
depth: 0,
pagination: false,
locale: 'all',
where: {
doc: {
equals: {
relationTo: localizedStatusPostsSlug,
value: post.id,
},
},
},
})

// The document is published in 'de', so it must be indexed even though 'en' is a draft
expect(docs).toHaveLength(1)

// The indexed 'de' locale must hold that locale's scalar value, not the whole `locale: 'all'` map
const deSearchDoc = await payload.findByID({
collection: 'search',
id: docs[0]!.id,
depth: 0,
locale: 'de',
})

expect(deSearchDoc.slug).toBe('de-only-de')
})

it('should sync trashed documents correctly with search plugin', async () => {
// Create a published post
const publishedPost = await payload.create({
Expand Down
48 changes: 48 additions & 0 deletions test/plugin-search/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface Config {
users: User;
pages: Page;
posts: Post;
'localized-status-posts': LocalizedStatusPost;
'custom-ids-1': CustomIds1;
'custom-ids-2': CustomIds2;
'filtered-locales': FilteredLocale;
Expand All @@ -84,6 +85,7 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
posts: PostsSelect<false> | PostsSelect<true>;
'localized-status-posts': LocalizedStatusPostsSelect<false> | LocalizedStatusPostsSelect<true>;
'custom-ids-1': CustomIds1Select<false> | CustomIds1Select<true>;
'custom-ids-2': CustomIds2Select<false> | CustomIds2Select<true>;
'filtered-locales': FilteredLocalesSelect<false> | FilteredLocalesSelect<true>;
Expand All @@ -100,6 +102,9 @@ export interface Config {
globals: {};
globalsSelect: {};
locale: 'en' | 'es' | 'de';
widgets: {
collections: CollectionsWidget;
};
user: User;
jobs: {
tasks: unknown;
Expand Down Expand Up @@ -175,6 +180,19 @@ export interface Post {
deletedAt?: string | null;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-status-posts".
*/
export interface LocalizedStatusPost {
id: string;
title: string;
excerpt?: string | null;
slug?: string | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-ids-1".
Expand Down Expand Up @@ -233,6 +251,10 @@ export interface Search {
| {
relationTo: 'filtered-locales';
value: string | FilteredLocale;
}
| {
relationTo: 'localized-status-posts';
value: string | LocalizedStatusPost;
};
id: string;
excerpt?: string | null;
Expand Down Expand Up @@ -276,6 +298,10 @@ export interface PayloadLockedDocument {
relationTo: 'posts';
value: string | Post;
} | null)
| ({
relationTo: 'localized-status-posts';
value: string | LocalizedStatusPost;
} | null)
| ({
relationTo: 'custom-ids-1';
value: string | CustomIds1;
Expand Down Expand Up @@ -380,6 +406,18 @@ export interface PostsSelect<T extends boolean = true> {
deletedAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-status-posts_select".
*/
export interface LocalizedStatusPostsSelect<T extends boolean = true> {
title?: T;
excerpt?: T;
slug?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-ids-1_select".
Expand Down Expand Up @@ -462,6 +500,16 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "collections_widget".
*/
export interface CollectionsWidget {
data?: {
[k: string]: unknown;
};
width: 'full';
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
Expand Down
2 changes: 2 additions & 0 deletions test/plugin-search/shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const pagesSlug = 'pages'

export const postsSlug = 'posts'

export const localizedStatusPostsSlug = 'localized-status-posts'
Loading