feat: validate field names for reserved names (#7130)
We now validate the names of the field against an array of protected field names. Also added JSDoc since we can't enforce type strictness yet if `string | const[]` as it always evaluates to `string`. ``` The name of the field. Must be alphanumeric and cannot contain ' . ' Must not be one of protected field names: ['__v', 'salt', 'hash', 'file'] @link — [https://payloadcms.com/docs/fields/overview#field-names](vscode-file://vscode-app/usr/share/code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html) ```
This commit is contained in:
@@ -143,6 +143,7 @@ The following field names are forbidden and cannot be used:
|
||||
- `__v`
|
||||
- `salt`
|
||||
- `hash`
|
||||
- `file`
|
||||
|
||||
### Field-level Hooks
|
||||
|
||||
|
||||
@@ -36,6 +36,18 @@ export const sanitizeCollection = async (
|
||||
isMergeableObject: isPlainObject,
|
||||
})
|
||||
|
||||
// /////////////////////////////////
|
||||
// Sanitize fields
|
||||
// /////////////////////////////////
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
sanitized.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: sanitized.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
if (sanitized.timestamps !== false) {
|
||||
// add default timestamps fields only as needed
|
||||
let hasUpdatedAt = null
|
||||
@@ -162,17 +174,5 @@ export const sanitizeCollection = async (
|
||||
sanitized.fields = mergeBaseFields(sanitized.fields, authFields)
|
||||
}
|
||||
|
||||
// /////////////////////////////////
|
||||
// Sanitize fields
|
||||
// /////////////////////////////////
|
||||
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
sanitized.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: sanitized.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
return sanitized as SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
9
packages/payload/src/errors/ReservedFieldName.ts
Normal file
9
packages/payload/src/errors/ReservedFieldName.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { FieldAffectingData } from '../fields/config/types.js'
|
||||
|
||||
import { APIError } from './APIError.js'
|
||||
|
||||
export class ReservedFieldName extends APIError {
|
||||
constructor(field: FieldAffectingData, fieldName: string) {
|
||||
super(`Field ${field.label} has reserved name '${fieldName}'.`)
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,5 @@ export { MissingFieldType } from './MissingFieldType.js'
|
||||
export { MissingFile } from './MissingFile.js'
|
||||
export { NotFound } from './NotFound.js'
|
||||
export { QueryError } from './QueryError.js'
|
||||
export { ReservedFieldName } from './ReservedFieldName.js'
|
||||
export { ValidationError } from './ValidationError.js'
|
||||
|
||||
@@ -9,7 +9,12 @@ import type {
|
||||
TextField,
|
||||
} from './types.js'
|
||||
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
|
||||
import {
|
||||
InvalidFieldName,
|
||||
InvalidFieldRelationship,
|
||||
MissingFieldType,
|
||||
ReservedFieldName,
|
||||
} from '../../errors/index.js'
|
||||
import { sanitizeFields } from './sanitize.js'
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
@@ -47,6 +52,23 @@ describe('sanitizeFields', () => {
|
||||
}).rejects.toThrow(InvalidFieldName)
|
||||
})
|
||||
|
||||
it('should throw on a reserved field name', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'hash',
|
||||
type: 'text',
|
||||
label: 'hash',
|
||||
},
|
||||
]
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).rejects.toThrow(ReservedFieldName)
|
||||
})
|
||||
|
||||
describe('auto-labeling', () => {
|
||||
it('should populate label if missing', async () => {
|
||||
const fields: Field[] = [
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
InvalidFieldName,
|
||||
InvalidFieldRelationship,
|
||||
MissingFieldType,
|
||||
ReservedFieldName,
|
||||
} from '../../errors/index.js'
|
||||
import { deepMerge } from '../../utilities/deepMerge.js'
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
@@ -39,6 +40,8 @@ type Args = {
|
||||
validRelationships: null | string[]
|
||||
}
|
||||
|
||||
export const reservedFieldNames = ['__v', 'salt', 'hash', 'file']
|
||||
|
||||
export const sanitizeFields = async ({
|
||||
config,
|
||||
existingFieldNames = new Set(),
|
||||
@@ -59,6 +62,11 @@ export const sanitizeFields = async ({
|
||||
throw new InvalidFieldName(field, field.name)
|
||||
}
|
||||
|
||||
// assert that field names are not one of reserved names
|
||||
if (fieldAffectsData(field) && reservedFieldNames.includes(field.name)) {
|
||||
throw new ReservedFieldName(field, field.name)
|
||||
}
|
||||
|
||||
// Auto-label
|
||||
if (
|
||||
'name' in field &&
|
||||
|
||||
@@ -229,6 +229,12 @@ export interface FieldBase {
|
||||
index?: boolean
|
||||
label?: LabelFunction | Record<string, string> | false | string
|
||||
localized?: boolean
|
||||
/**
|
||||
* The name of the field. Must be alphanumeric and cannot contain ' . '
|
||||
*
|
||||
* Must not be one of reserved field names: ['__v', 'salt', 'hash', 'file']
|
||||
* @link https://payloadcms.com/docs/fields/overview#field-names
|
||||
*/
|
||||
name: string
|
||||
required?: boolean
|
||||
saveToJWT?: boolean | string
|
||||
|
||||
@@ -41,6 +41,15 @@ export const sanitizeGlobals = async (
|
||||
if (!global.hooks.beforeRead) global.hooks.beforeRead = []
|
||||
if (!global.hooks.afterRead) global.hooks.afterRead = []
|
||||
|
||||
// Sanitize fields
|
||||
const validRelationships = collections.map((c) => c.slug) || []
|
||||
global.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: global.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
if (global.versions) {
|
||||
if (global.versions === true) global.versions = { drafts: false }
|
||||
|
||||
@@ -103,14 +112,6 @@ export const sanitizeGlobals = async (
|
||||
})
|
||||
}
|
||||
|
||||
const validRelationships = collections.map((c) => c.slug) || []
|
||||
global.fields = await sanitizeFields({
|
||||
config,
|
||||
fields: global.fields,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
globals[i] = global
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user