From 4d682c34229732bd1f4f5865d953f7a4d252d876 Mon Sep 17 00:00:00 2001 From: Vijay Misal Date: Fri, 7 Aug 2026 22:50:36 +0530 Subject: [PATCH] fix: throw clear error for non-existent listSearchableFields Previously, referencing a field name in admin.listSearchableFields that does not exist on the collection would not surface any error during development (pnpm dev). In production builds, the same invalid config would cause the search query to hit the database with an unqueryable path, "imploding" the admin List View search with no usable error message to the user. This adds validateListSearchableFields, called during collection sanitization (mirroring the existing validateUseAsTitle pattern), which eagerly throws an InvalidConfiguration error naming the offending field and collection as soon as the config is loaded - consistently in both dev and production. Fixes #14904 --- .../config/listSearchableFields.spec.ts | 102 ++++++++++++++++++ .../config/listSearchableFields.ts | 47 ++++++++ .../src/collections/config/sanitize.ts | 2 + 3 files changed, 151 insertions(+) create mode 100644 packages/payload/src/collections/config/listSearchableFields.spec.ts create mode 100644 packages/payload/src/collections/config/listSearchableFields.ts diff --git a/packages/payload/src/collections/config/listSearchableFields.spec.ts b/packages/payload/src/collections/config/listSearchableFields.spec.ts new file mode 100644 index 00000000000..a10439f8591 --- /dev/null +++ b/packages/payload/src/collections/config/listSearchableFields.spec.ts @@ -0,0 +1,102 @@ +import type { Config } from '../../config/types.js' +import type { CollectionConfig } from '../../index.js' + +import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js' +import { sanitizeCollection } from './sanitize.js' +import { describe, it, expect } from 'vitest' + +describe('sanitize - collections -', () => { + const config = { + collections: [], + globals: [], + } as Partial + + describe('validate listSearchableFields -', () => { + const defaultCollection: CollectionConfig = { + slug: 'collection-with-defaults', + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'slug', + type: 'text', + }, + ], + } + + it('should throw on non-existent field', () => { + const collectionConfig: CollectionConfig = { + ...defaultCollection, + admin: { + listSearchableFields: ['title', 'nonExistentField'], + }, + } + expect(() => { + sanitizeCollection( + // @ts-expect-error + { + ...config, + collections: [collectionConfig], + }, + collectionConfig, + ) + }).toThrow(InvalidConfiguration) + }) + + it('should not throw when all fields exist', () => { + const collectionConfig: CollectionConfig = { + ...defaultCollection, + admin: { + listSearchableFields: ['title', 'slug'], + }, + } + expect(() => { + sanitizeCollection( + // @ts-expect-error + { + ...config, + collections: [collectionConfig], + }, + collectionConfig, + ) + }).not.toThrow() + }) + + it('should not throw on default field: id', () => { + const collectionConfig: CollectionConfig = { + ...defaultCollection, + admin: { + listSearchableFields: ['id'], + }, + } + expect(() => { + sanitizeCollection( + // @ts-expect-error + { + ...config, + collections: [collectionConfig], + }, + collectionConfig, + ) + }).not.toThrow() + }) + + it('should not throw when listSearchableFields is undefined', () => { + const collectionConfig: CollectionConfig = { + ...defaultCollection, + } + expect(() => { + sanitizeCollection( + // @ts-expect-error + { + ...config, + collections: [collectionConfig], + }, + collectionConfig, + ) + }).not.toThrow() + }) + }) +}) diff --git a/packages/payload/src/collections/config/listSearchableFields.ts b/packages/payload/src/collections/config/listSearchableFields.ts new file mode 100644 index 00000000000..1c8828ecbe1 --- /dev/null +++ b/packages/payload/src/collections/config/listSearchableFields.ts @@ -0,0 +1,47 @@ +import type { CollectionConfig } from '../../index.js' + +import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js' +import { fieldAffectsData } from '../../fields/config/types.js' +import { flattenTopLevelFields } from '../../utilities/flattenTopLevelFields.js' + +/** + * Validate listSearchableFields for collections. + * + * Previously, specifying a field name in `admin.listSearchableFields` that does not + * exist on the collection would not throw until the field was actually queried + * (e.g. when searching in the List View), and the resulting error was only + * ever surfaced in production builds - in development it silently failed to + * error at all. Validating eagerly here, at config sanitization time, ensures a + * clear, actionable error is thrown consistently in every environment. + */ +export const validateListSearchableFields = (config: CollectionConfig) => { + if (!config.admin?.listSearchableFields?.length) { + return + } + + const fields = flattenTopLevelFields(config.fields) + + for (const fieldName of config.admin.listSearchableFields) { + if (fieldName === 'id') { + continue + } + + // Only validate the top-level segment of the path - relationship/join + // paths such as `category.title` are resolved deeper in the query and + // are not flattened top-level fields of this collection. + const topLevelFieldName = fieldName.split('.')[0] + + const searchableField = fields.find((field) => { + if (fieldAffectsData(field)) { + return field.name === topLevelFieldName + } + return false + }) + + if (!searchableField) { + throw new InvalidConfiguration( + `The field "${fieldName}" specified in "admin.listSearchableFields" does not exist in the collection "${config.slug}"`, + ) + } + } +} diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts index 708ba0f89ea..b977d2cc016 100644 --- a/packages/payload/src/collections/config/sanitize.ts +++ b/packages/payload/src/collections/config/sanitize.ts @@ -26,6 +26,7 @@ import { baseVersionFields } from '../../versions/baseFields.js' import { versionDefaults } from '../../versions/defaults.js' import { defaultCollectionEndpoints } from '../endpoints/index.js' import { addDefaultsToAuthConfig, addDefaultsToCollectionConfig } from './defaults.js' +import { validateListSearchableFields } from './listSearchableFields.js' import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js' import { validateUseAsTitle } from './useAsTitle.js' @@ -338,6 +339,7 @@ export const sanitizeCollection = ( } validateUseAsTitle(sanitized) + validateListSearchableFields(sanitized) const sanitizedConfig = sanitized as SanitizedCollectionConfig