Skip to content

Commit

Permalink
fix: disallow duplicate fieldNames to be used on the same level in th…
Browse files Browse the repository at this point in the history
…e config (#4381)
  • Loading branch information
JarrodMFlesch committed Dec 11, 2023
1 parent 548e78c commit a1d66b8
Show file tree
Hide file tree
Showing 14 changed files with 755 additions and 276 deletions.
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 {}
}

0 comments on commit a1d66b8

Please sign in to comment.