feat: add validation for useAsTitle to throw an error if it's an invalid or nested field (#8122)
This commit is contained in:
@@ -14,6 +14,7 @@ import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { versionDefaults } from '../../versions/defaults.js'
|
||||
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
import { validateUseAsTitle } from './useAsTitle.js'
|
||||
|
||||
export const sanitizeCollection = async (
|
||||
config: Config,
|
||||
@@ -44,6 +45,8 @@ export const sanitizeCollection = async (
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
validateUseAsTitle(sanitized)
|
||||
|
||||
if (sanitized.timestamps !== false) {
|
||||
// add default timestamps fields only as needed
|
||||
let hasUpdatedAt = null
|
||||
|
||||
204
packages/payload/src/collections/config/useAsTitle.spec.ts
Normal file
204
packages/payload/src/collections/config/useAsTitle.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { sanitizeCollection } from './sanitize.js'
|
||||
|
||||
describe('sanitize - collections -', () => {
|
||||
const config = {
|
||||
collections: [],
|
||||
globals: [],
|
||||
} as Partial<Config>
|
||||
|
||||
describe('validate useAsTitle -', () => {
|
||||
const defaultCollection: CollectionConfig = {
|
||||
slug: 'collection-with-defaults',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
it('should throw on invalid field', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'invalidField',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
|
||||
it('should not throw on valid field', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid field inside tabs', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'General',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid field inside collapsibles', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should throw on nested useAsTitle', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'content.title',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
|
||||
it('should not throw on default field: id', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'id',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on default field: email if auth is enabled', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
it('should throw on default field: email if auth is not enabled', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
})
|
||||
})
|
||||
43
packages/payload/src/collections/config/useAsTitle.ts
Normal file
43
packages/payload/src/collections/config/useAsTitle.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import flattenFields from '../../utilities/flattenTopLevelFields.js'
|
||||
|
||||
/**
|
||||
* Validate useAsTitle for collections.
|
||||
*/
|
||||
export const validateUseAsTitle = (config: CollectionConfig) => {
|
||||
if (config.admin.useAsTitle.includes('.')) {
|
||||
throw new InvalidConfiguration(
|
||||
`"useAsTitle" cannot be a nested field. Please specify a top-level field in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (config?.admin && config.admin?.useAsTitle && config.admin.useAsTitle !== 'id') {
|
||||
const fields = flattenFields(config.fields)
|
||||
const useAsTitleField = fields.find((field) => {
|
||||
if (fieldAffectsData(field) && config.admin) {
|
||||
return field.name === config.admin.useAsTitle
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// If auth is enabled then we don't need to
|
||||
if (config.auth) {
|
||||
if (config.admin.useAsTitle !== 'email') {
|
||||
if (!useAsTitleField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" does not exist in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!useAsTitleField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" does not exist in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,9 +220,6 @@ export default buildConfigWithDefaults({
|
||||
slug: relationWithTitleSlug,
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
fields: [
|
||||
|
||||
@@ -4,9 +4,6 @@ import { defaultEmail, emailFieldsSlug } from './shared.js'
|
||||
|
||||
const EmailFields: CollectionConfig = {
|
||||
slug: emailFieldsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
defaultSort: 'id',
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -3,8 +3,5 @@ import type { CollectionConfig } from 'payload'
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user