Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: disallow duplicate fieldNames to be used on the same level #4381

Merged
merged 10 commits into from
Dec 11, 2023
11 changes: 11 additions & 0 deletions packages/payload/src/errors/DuplicateFieldName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import APIError from './APIError'

class DuplicateFieldName extends APIError {
constructor(fieldName: string) {
super(
`A field with the name '${fieldName}' was found multiple times on the same level. Field names must be unique.`,
)
}
}

export default DuplicateFieldName
1 change: 1 addition & 0 deletions packages/payload/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as errorHandler } from '../express/middleware/errorHandler'
export { default as APIError } from './APIError'
export { default as AuthenticationError } from './AuthenticationError'
export { default as DuplicateCollection } from './DuplicateCollection'
export { default as DuplicateFieldName } from './DuplicateFieldName'
export { default as DuplicateGlobal } from './DuplicateGlobal'
export { default as ErrorDeletingFile } from './ErrorDeletingFile'
export { default as FileUploadError } from './FileUploadError'
Expand Down
24 changes: 22 additions & 2 deletions packages/payload/src/fields/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import type { Config } from '../../config/types'
import type { Field } from './types'

import withCondition from '../../admin/components/forms/withCondition'
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors'
import {
DuplicateFieldName,
InvalidFieldName,
InvalidFieldRelationship,
MissingFieldType,
} from '../../errors'
import { formatLabels, toWords } from '../../utilities/formatLabels'
import { baseBlockFields } from '../baseFields/baseBlockFields'
import { baseIDField } from '../baseFields/baseIDField'
Expand All @@ -11,6 +16,7 @@ import { fieldAffectsData, tabHasName } from './types'

type Args = {
config: Config
existingFieldNames?: Set<string>
fields: Field[]
/**
* If not null, will validate that upload and relationship fields do not relate to a collection that is not in this array.
Expand All @@ -19,7 +25,12 @@ type Args = {
validRelationships: null | string[]
}

export const sanitizeFields = ({ config, fields, validRelationships }: Args): Field[] => {
export const sanitizeFields = ({
config,
existingFieldNames = new Set(),
fields,
validRelationships,
}: Args): Field[] => {
if (!fields) return []

return fields.map((unsanitizedField) => {
Expand Down Expand Up @@ -100,6 +111,12 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi
}

if (fieldAffectsData(field)) {
if (existingFieldNames.has(field.name)) {
throw new DuplicateFieldName(field.name)
} else if (!['id', 'blockName'].includes(field.name)) {
existingFieldNames.add(field.name)
}

if (field.localized && !config.localization) delete field.localized

if (typeof field.validate === 'undefined') {
Expand All @@ -126,6 +143,7 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi
if ('fields' in field && field.fields) {
field.fields = sanitizeFields({
config,
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
fields: field.fields,
validRelationships,
})
Expand All @@ -140,6 +158,7 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi

unsanitizedTab.fields = sanitizeFields({
config,
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
fields: tab.fields,
validRelationships,
})
Expand All @@ -159,6 +178,7 @@ export const sanitizeFields = ({ config, fields, validRelationships }: Args): Fi
config,
fields: block.fields,
validRelationships,
existingFieldNames: new Set(),
})

return unsanitizedBlock
Expand Down
223 changes: 201 additions & 22 deletions test/field-error-states/payload-types.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,225 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload CMS.
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/

export interface Config {
collections: {
posts: Post
'error-fields': ErrorField
uploads: Upload
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {}
}
export interface Post {
export interface ErrorField {
id: string
arrayField?: {
group23field: {
arrayField: {
group23field: {
arrayField: {
textField: string
id?: string
parentArray?:
| {
childArray: {
childArrayText: string
id?: string | null
}[]
id?: string | null
}[]
| null
home: {
tabText: string
text: string
array?:
| {
requiredArrayText: string
arrayText?: string | null
group: {
text: string
number: number
date: string
checkbox: boolean
}
code: string
json:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
email: string
/**
* @minItems 2
* @maxItems 2
*/
point: [number, number]
radio: 'mint' | 'dark_gray'
relationship: string | User
richtext: {
[k: string]: unknown
}[]
select: 'mint' | 'dark_gray'
upload: string | Upload
text: string
textarea: string
id?: string | null
}[]
| null
}
tabText: string
text: string
array?:
| {
requiredArrayText: string
arrayText?: string | null
group: {
text: string
number: number
date: string
checkbox: boolean
}
id?: string
code: string
json:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
email: string
/**
* @minItems 2
* @maxItems 2
*/
point: [number, number]
radio: 'mint' | 'dark_gray'
relationship: string | User
richtext: {
[k: string]: unknown
}[]
select: 'mint' | 'dark_gray'
upload: string | Upload
text: string
textarea: string
id?: string | null
}[]
| null
layout?:
| {
tabText: string
text: string
array?:
| {
requiredArrayText: string
arrayText?: string | null
group: {
text: string
number: number
date: string
checkbox: boolean
}
code: string
json:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
email: string
/**
* @minItems 2
* @maxItems 2
*/
point: [number, number]
radio: 'mint' | 'dark_gray'
relationship: string | User
richtext: {
[k: string]: unknown
}[]
select: 'mint' | 'dark_gray'
upload: string | Upload
text: string
textarea: string
id?: string | null
}[]
| null
id?: string | null
blockName?: string | null
blockType: 'block1'
}[]
}
id?: string
}[]
| null
group: {
text: string
}
updatedAt: string
createdAt: string
}
export interface User {
id: string
updatedAt: string
createdAt: string
email?: string
resetPasswordToken?: string
resetPasswordExpiration?: string
salt?: string
hash?: string
loginAttempts?: number
lockUntil?: string
password?: string
email: string
resetPasswordToken?: string | null
resetPasswordExpiration?: string | null
salt?: string | null
hash?: string | null
loginAttempts?: number | null
lockUntil?: string | null
password: string | null
}
export interface Upload {
id: string
text?: string | null
media?: string | Upload | null
richText?:
| {
[k: string]: unknown
}[]
| null
updatedAt: string
createdAt: string
url?: string | null
filename?: string | null
mimeType?: string | null
filesize?: number | null
width?: number | null
height?: number | null
}
export interface PayloadPreference {
id: string
user: {
relationTo: 'users'
value: string | User
}
key?: string | null
value?:
| {
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null
updatedAt: string
createdAt: string
}
export interface PayloadMigration {
id: string
name?: string | null
batch?: number | null
updatedAt: string
createdAt: string
}

declare module 'payload' {
export interface GeneratedTypes extends Config {}
}
Loading
Loading