perf: remove duplicative deep loops during field sanitization (#12402)

Optimizes the field sanitization process by removing duplicative deep
loops over the config. We were previously iterating over all fields of
each collection potentially multiple times in order validate field
configs, check reserved field names, etc. Now, we perform all necessary
sanitization within a single loop.
This commit is contained in:
Jacob Fletcher
2025-05-14 15:25:44 -04:00
committed by GitHub
parent 9779cf7f7d
commit 93d79b9c62
8 changed files with 187 additions and 179 deletions

View File

@@ -1,151 +0,0 @@
// @ts-strict-ignore
import type { Field } from '../../fields/config/types.js'
import type { CollectionConfig } from '../../index.js'
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
import { fieldAffectsData } from '../../fields/config/types.js'
// Note for future reference: We've slimmed down the reserved field names but left them in here for reference in case it's needed in the future.
/**
* Reserved field names for collections with auth config enabled
*/
const reservedBaseAuthFieldNames = [
/* 'email',
'resetPasswordToken',
'resetPasswordExpiration', */
'salt',
'hash',
]
/**
* Reserved field names for auth collections with verify: true
*/
const reservedVerifyFieldNames = [
/* '_verified', '_verificationToken' */
]
/**
* Reserved field names for auth collections with useApiKey: true
*/
const reservedAPIKeyFieldNames = [
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
]
/**
* Reserved field names for collections with upload config enabled
*/
const reservedBaseUploadFieldNames = [
'file',
/* 'mimeType',
'thumbnailURL',
'width',
'height',
'filesize',
'filename',
'url',
'focalX',
'focalY',
'sizes', */
]
/**
* Reserved field names for collections with versions enabled
*/
const reservedVersionsFieldNames = [
/* '__v', '_status' */
]
/**
* Sanitize fields for collections with auth config enabled.
*
* Should run on top level fields only.
*/
export const sanitizeAuthFields = (fields: Field[], config: CollectionConfig) => {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (fieldAffectsData(field) && field.name) {
if (config.auth && typeof config.auth === 'object' && !config.auth.disableLocalStrategy) {
const auth = config.auth
if (reservedBaseAuthFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
if (auth.verify) {
if (reservedAPIKeyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
/* if (auth.maxLoginAttempts) {
if (field.name === 'loginAttempts' || field.name === 'lockUntil') {
throw new ReservedFieldName(field, field.name)
}
} */
/* if (auth.loginWithUsername) {
if (field.name === 'username') {
throw new ReservedFieldName(field, field.name)
}
} */
if (auth.verify) {
if (reservedVerifyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
}
}
// Handle tabs without a name
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
if (!('name' in tab)) {
sanitizeAuthFields(tab.fields, config)
}
}
}
// Handle presentational fields like rows and collapsibles
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
sanitizeAuthFields(field.fields, config)
}
}
}
/**
* Sanitize fields for collections with upload config enabled.
*
* Should run on top level fields only.
*/
export const sanitizeUploadFields = (fields: Field[], config: CollectionConfig) => {
if (config.upload && typeof config.upload === 'object') {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (fieldAffectsData(field) && field.name) {
if (reservedBaseUploadFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
// Handle tabs without a name
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
if (!('name' in tab)) {
sanitizeUploadFields(tab.fields, config)
}
}
}
// Handle presentational fields like rows and collapsibles
if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
sanitizeUploadFields(field.fields, config)
}
}
}
}

View File

@@ -26,7 +26,6 @@ import {
addDefaultsToCollectionConfig, addDefaultsToCollectionConfig,
addDefaultsToLoginWithUsernameConfig, addDefaultsToLoginWithUsernameConfig,
} from './defaults.js' } from './defaults.js'
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js' import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js'
import { validateUseAsTitle } from './useAsTitle.js' import { validateUseAsTitle } from './useAsTitle.js'
@@ -43,7 +42,9 @@ export const sanitizeCollection = async (
if (collection._sanitized) { if (collection._sanitized) {
return collection as SanitizedCollectionConfig return collection as SanitizedCollectionConfig
} }
collection._sanitized = true collection._sanitized = true
// ///////////////////////////////// // /////////////////////////////////
// Make copy of collection config // Make copy of collection config
// ///////////////////////////////// // /////////////////////////////////
@@ -57,7 +58,9 @@ export const sanitizeCollection = async (
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? [] const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
const joins: SanitizedJoins = {} const joins: SanitizedJoins = {}
const polymorphicJoins: SanitizedJoin[] = [] const polymorphicJoins: SanitizedJoin[] = []
sanitized.fields = await sanitizeFields({ sanitized.fields = await sanitizeFields({
collectionConfig: sanitized, collectionConfig: sanitized,
config, config,
@@ -96,17 +99,21 @@ export const sanitizeCollection = async (
// add default timestamps fields only as needed // add default timestamps fields only as needed
let hasUpdatedAt: boolean | null = null let hasUpdatedAt: boolean | null = null
let hasCreatedAt: boolean | null = null let hasCreatedAt: boolean | null = null
sanitized.fields.some((field) => { sanitized.fields.some((field) => {
if (fieldAffectsData(field)) { if (fieldAffectsData(field)) {
if (field.name === 'updatedAt') { if (field.name === 'updatedAt') {
hasUpdatedAt = true hasUpdatedAt = true
} }
if (field.name === 'createdAt') { if (field.name === 'createdAt') {
hasCreatedAt = true hasCreatedAt = true
} }
} }
return hasCreatedAt && hasUpdatedAt return hasCreatedAt && hasUpdatedAt
}) })
if (!hasUpdatedAt) { if (!hasUpdatedAt) {
sanitized.fields.push({ sanitized.fields.push({
name: 'updatedAt', name: 'updatedAt',
@@ -119,6 +126,7 @@ export const sanitizeCollection = async (
label: ({ t }) => t('general:updatedAt'), label: ({ t }) => t('general:updatedAt'),
}) })
} }
if (!hasCreatedAt) { if (!hasCreatedAt) {
sanitized.fields.push({ sanitized.fields.push({
name: 'createdAt', name: 'createdAt',
@@ -175,9 +183,6 @@ export const sanitizeCollection = async (
sanitized.upload = {} sanitized.upload = {}
} }
// sanitize fields for reserved names
sanitizeUploadFields(sanitized.fields, sanitized)
sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
@@ -195,9 +200,6 @@ export const sanitizeCollection = async (
} }
if (sanitized.auth) { if (sanitized.auth) {
// sanitize fields for reserved names
sanitizeAuthFields(sanitized.fields, sanitized)
sanitized.auth = addDefaultsToAuthConfig( sanitized.auth = addDefaultsToAuthConfig(
typeof sanitized.auth === 'boolean' ? {} : sanitized.auth, typeof sanitized.auth === 'boolean' ? {} : sanitized.auth,
) )

View File

@@ -1,7 +1,7 @@
import type { CollectionConfig } from '../../index.js' import type { CollectionConfig } from '../../index.js'
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js' import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js' import { fieldAffectsData } from '../../fields/config/types.js'
import flattenFields from '../../utilities/flattenTopLevelFields.js' import flattenFields from '../../utilities/flattenTopLevelFields.js'
/** /**

View File

@@ -58,6 +58,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
// add default user collection if none provided // add default user collection if none provided
if (!sanitizedConfig?.admin?.user) { if (!sanitizedConfig?.admin?.user) {
const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth)) const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth))
if (firstCollectionWithAuth) { if (firstCollectionWithAuth) {
sanitizedConfig.admin.user = firstCollectionWithAuth.slug sanitizedConfig.admin.user = firstCollectionWithAuth.slug
} else { } else {
@@ -69,6 +70,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
const userCollection = sanitizedConfig.collections.find( const userCollection = sanitizedConfig.collections.find(
({ slug }) => slug === sanitizedConfig.admin.user, ({ slug }) => slug === sanitizedConfig.admin.user,
) )
if (!userCollection || !userCollection.auth) { if (!userCollection || !userCollection.auth) {
throw new InvalidConfiguration( throw new InvalidConfiguration(
`${sanitizedConfig.admin.user} is not a valid admin user collection`, `${sanitizedConfig.admin.user} is not a valid admin user collection`,

View File

@@ -2,7 +2,7 @@ import type { Config } from '../../config/types.js'
import type { CollectionConfig, Field } from '../../index.js' import type { CollectionConfig, Field } from '../../index.js'
import { ReservedFieldName } from '../../errors/index.js' import { ReservedFieldName } from '../../errors/index.js'
import { sanitizeCollection } from './sanitize.js' import { sanitizeCollection } from '../../collections/config/sanitize.js'
describe('reservedFieldNames - collections -', () => { describe('reservedFieldNames - collections -', () => {
const config = { const config = {
@@ -25,6 +25,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeCollection( await sanitizeCollection(
// @ts-expect-error // @ts-expect-error
@@ -53,6 +54,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeCollection( await sanitizeCollection(
// @ts-expect-error // @ts-expect-error
@@ -93,6 +95,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeCollection( await sanitizeCollection(
// @ts-expect-error // @ts-expect-error
@@ -121,6 +124,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeCollection( await sanitizeCollection(
// @ts-expect-error // @ts-expect-error
@@ -149,6 +153,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeCollection( await sanitizeCollection(
// @ts-expect-error // @ts-expect-error

View File

@@ -0,0 +1,48 @@
/**
* Reserved field names for collections with auth config enabled
*/
export const reservedBaseAuthFieldNames = [
/* 'email',
'resetPasswordToken',
'resetPasswordExpiration', */
'salt',
'hash',
]
/**
* Reserved field names for auth collections with verify: true
*/
export const reservedVerifyFieldNames = [
/* '_verified', '_verificationToken' */
]
/**
* Reserved field names for auth collections with useApiKey: true
*/
export const reservedAPIKeyFieldNames = [
/* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
]
/**
* Reserved field names for collections with upload config enabled
*/
export const reservedBaseUploadFieldNames = [
'file',
/* 'mimeType',
'thumbnailURL',
'width',
'height',
'filesize',
'filename',
'url',
'focalX',
'focalY',
'sizes', */
]
/**
* Reserved field names for collections with versions enabled
*/
export const reservedVersionsFieldNames = [
/* '__v', '_status' */
]

View File

@@ -11,9 +11,12 @@ import type {
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js' import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
import { sanitizeFields } from './sanitize.js' import { sanitizeFields } from './sanitize.js'
import { CollectionConfig } from '../../index.js'
describe('sanitizeFields', () => { describe('sanitizeFields', () => {
const config = {} as Config const config = {} as Config
const collectionConfig = {} as CollectionConfig
it('should throw on missing type field', async () => { it('should throw on missing type field', async () => {
const fields: Field[] = [ const fields: Field[] = [
// @ts-expect-error // @ts-expect-error
@@ -22,14 +25,17 @@ describe('sanitizeFields', () => {
label: 'some-collection', label: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
}).rejects.toThrow(MissingFieldType) }).rejects.toThrow(MissingFieldType)
}) })
it('should throw on invalid field name', async () => { it('should throw on invalid field name', async () => {
const fields: Field[] = [ const fields: Field[] = [
{ {
@@ -38,9 +44,11 @@ describe('sanitizeFields', () => {
label: 'some.collection', label: 'some.collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
@@ -55,17 +63,21 @@ describe('sanitizeFields', () => {
type: 'text', type: 'text',
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as TextField )[0] as TextField
expect(sanitizedField.name).toStrictEqual('someField') expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual('Some Field') expect(sanitizedField.label).toStrictEqual('Some Field')
expect(sanitizedField.type).toStrictEqual('text') expect(sanitizedField.type).toStrictEqual('text')
}) })
it('should allow auto-label override', async () => { it('should allow auto-label override', async () => {
const fields: Field[] = [ const fields: Field[] = [
{ {
@@ -74,13 +86,16 @@ describe('sanitizeFields', () => {
label: 'Do not label', label: 'Do not label',
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as TextField )[0] as TextField
expect(sanitizedField.name).toStrictEqual('someField') expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual('Do not label') expect(sanitizedField.label).toStrictEqual('Do not label')
expect(sanitizedField.type).toStrictEqual('text') expect(sanitizedField.type).toStrictEqual('text')
@@ -95,13 +110,16 @@ describe('sanitizeFields', () => {
label: false, label: false,
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as TextField )[0] as TextField
expect(sanitizedField.name).toStrictEqual('someField') expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual(false) expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('text') expect(sanitizedField.type).toStrictEqual('text')
@@ -119,18 +137,22 @@ describe('sanitizeFields', () => {
], ],
label: false, label: false,
} }
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields: [arrayField], fields: [arrayField],
validRelationships: [], validRelationships: [],
}) })
)[0] as ArrayField )[0] as ArrayField
expect(sanitizedField.name).toStrictEqual('items') expect(sanitizedField.name).toStrictEqual('items')
expect(sanitizedField.label).toStrictEqual(false) expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('array') expect(sanitizedField.type).toStrictEqual('array')
expect(sanitizedField.labels).toBeUndefined() expect(sanitizedField.labels).toBeUndefined()
}) })
it('should allow label opt-out for blocks', async () => { it('should allow label opt-out for blocks', async () => {
const fields: Field[] = [ const fields: Field[] = [
{ {
@@ -150,13 +172,16 @@ describe('sanitizeFields', () => {
label: false, label: false,
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as BlocksField )[0] as BlocksField
expect(sanitizedField.name).toStrictEqual('noLabelBlock') expect(sanitizedField.name).toStrictEqual('noLabelBlock')
expect(sanitizedField.label).toStrictEqual(false) expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('blocks') expect(sanitizedField.type).toStrictEqual('blocks')
@@ -177,13 +202,16 @@ describe('sanitizeFields', () => {
], ],
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as ArrayField )[0] as ArrayField
expect(sanitizedField.name).toStrictEqual('items') expect(sanitizedField.name).toStrictEqual('items')
expect(sanitizedField.label).toStrictEqual('Items') expect(sanitizedField.label).toStrictEqual('Items')
expect(sanitizedField.type).toStrictEqual('array') expect(sanitizedField.type).toStrictEqual('array')
@@ -203,13 +231,16 @@ describe('sanitizeFields', () => {
], ],
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as BlocksField )[0] as BlocksField
expect(sanitizedField.name).toStrictEqual('specialBlock') expect(sanitizedField.name).toStrictEqual('specialBlock')
expect(sanitizedField.label).toStrictEqual('Special Block') expect(sanitizedField.label).toStrictEqual('Special Block')
expect(sanitizedField.type).toStrictEqual('blocks') expect(sanitizedField.type).toStrictEqual('blocks')
@@ -217,6 +248,7 @@ describe('sanitizeFields', () => {
plural: 'Special Blocks', plural: 'Special Blocks',
singular: 'Special Block', singular: 'Special Block',
}) })
expect((sanitizedField.blocks[0].fields[0] as NumberField).label).toStrictEqual('Test Number') expect((sanitizedField.blocks[0].fields[0] as NumberField).label).toStrictEqual('Test Number')
}) })
}) })
@@ -232,8 +264,9 @@ describe('sanitizeFields', () => {
relationTo: 'some-collection', relationTo: 'some-collection',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow() }).not.toThrow()
}) })
@@ -247,8 +280,9 @@ describe('sanitizeFields', () => {
relationTo: ['some-collection', 'another-collection'], relationTo: ['some-collection', 'another-collection'],
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow() }).not.toThrow()
}) })
@@ -265,6 +299,7 @@ describe('sanitizeFields', () => {
}, },
], ],
} }
const fields: Field[] = [ const fields: Field[] = [
{ {
name: 'layout', name: 'layout',
@@ -273,8 +308,9 @@ describe('sanitizeFields', () => {
label: 'Layout Blocks', label: 'Layout Blocks',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow() }).not.toThrow()
}) })
@@ -288,8 +324,9 @@ describe('sanitizeFields', () => {
relationTo: 'not-valid', relationTo: 'not-valid',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship) }).rejects.toThrow(InvalidFieldRelationship)
}) })
@@ -303,8 +340,9 @@ describe('sanitizeFields', () => {
relationTo: ['some-collection', 'not-valid'], relationTo: ['some-collection', 'not-valid'],
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship) }).rejects.toThrow(InvalidFieldRelationship)
}) })
@@ -321,6 +359,7 @@ describe('sanitizeFields', () => {
}, },
], ],
} }
const fields: Field[] = [ const fields: Field[] = [
{ {
name: 'layout', name: 'layout',
@@ -329,8 +368,9 @@ describe('sanitizeFields', () => {
label: 'Layout Blocks', label: 'Layout Blocks',
}, },
] ]
await expect(async () => { await expect(async () => {
await sanitizeFields({ config, fields, validRelationships }) await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship) }).rejects.toThrow(InvalidFieldRelationship)
}) })
@@ -346,19 +386,23 @@ describe('sanitizeFields', () => {
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
)[0] as CheckboxField )[0] as CheckboxField
expect(sanitizedField.defaultValue).toStrictEqual(false) expect(sanitizedField.defaultValue).toStrictEqual(false)
}) })
it('should return empty field array if no fields', async () => { it('should return empty field array if no fields', async () => {
const sanitizedFields = await sanitizeFields({ const sanitizedFields = await sanitizeFields({
config, config,
collectionConfig,
fields: [], fields: [],
validRelationships: [], validRelationships: [],
}) })
expect(sanitizedFields).toStrictEqual([]) expect(sanitizedFields).toStrictEqual([])
}) })
}) })
@@ -385,9 +429,11 @@ describe('sanitizeFields', () => {
label: false, label: false,
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })
@@ -416,9 +462,11 @@ describe('sanitizeFields', () => {
label: false, label: false,
}, },
] ]
const sanitizedField = ( const sanitizedField = (
await sanitizeFields({ await sanitizeFields({
config, config,
collectionConfig,
fields, fields,
validRelationships: [], validRelationships: [],
}) })

View File

@@ -17,6 +17,7 @@ import {
MissingEditorProp, MissingEditorProp,
MissingFieldType, MissingFieldType,
} from '../../errors/index.js' } from '../../errors/index.js'
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js' import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js' import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js' import { baseIDField } from '../baseFields/baseIDField.js'
@@ -24,14 +25,24 @@ import { baseTimezoneField } from '../baseFields/timezone/baseField.js'
import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js' import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js'
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js' import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
import { validations } from '../validations.js' import { validations } from '../validations.js'
import {
reservedAPIKeyFieldNames,
reservedBaseAuthFieldNames,
reservedBaseUploadFieldNames,
reservedVerifyFieldNames,
} from './reservedFieldNames.js'
import { sanitizeJoinField } from './sanitizeJoinField.js' import { sanitizeJoinField } from './sanitizeJoinField.js'
import { fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js' import { fieldAffectsData as _fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
type Args = { type Args = {
collectionConfig?: CollectionConfig collectionConfig?: CollectionConfig
config: Config config: Config
existingFieldNames?: Set<string> existingFieldNames?: Set<string>
fields: Field[] fields: Field[]
/**
* Used to prevent unnecessary sanitization of fields that are not top-level.
*/
isTopLevelField?: boolean
joinPath?: string joinPath?: string
/** /**
* When not passed in, assume that join are not supported (globals, arrays, blocks) * When not passed in, assume that join are not supported (globals, arrays, blocks)
@@ -39,7 +50,6 @@ type Args = {
joins?: SanitizedJoins joins?: SanitizedJoins
parentIsLocalized: boolean parentIsLocalized: boolean
polymorphicJoins?: SanitizedJoin[] polymorphicJoins?: SanitizedJoin[]
/** /**
* If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present. * If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present.
* *
@@ -59,9 +69,11 @@ type Args = {
} }
export const sanitizeFields = async ({ export const sanitizeFields = async ({
collectionConfig,
config, config,
existingFieldNames = new Set(), existingFieldNames = new Set(),
fields, fields,
isTopLevelField = true,
joinPath = '', joinPath = '',
joins, joins,
parentIsLocalized, parentIsLocalized,
@@ -80,6 +92,7 @@ export const sanitizeFields = async ({
if ('_sanitized' in field && field._sanitized === true) { if ('_sanitized' in field && field._sanitized === true) {
continue continue
} }
if ('_sanitized' in field) { if ('_sanitized' in field) {
field._sanitized = true field._sanitized = true
} }
@@ -88,8 +101,39 @@ export const sanitizeFields = async ({
throw new MissingFieldType(field) throw new MissingFieldType(field)
} }
const fieldAffectsData = _fieldAffectsData(field)
if (isTopLevelField && fieldAffectsData && field.name) {
if (collectionConfig && collectionConfig.upload) {
if (reservedBaseUploadFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
if (
collectionConfig &&
collectionConfig.auth &&
typeof collectionConfig.auth === 'object' &&
!collectionConfig.auth.disableLocalStrategy
) {
if (reservedBaseAuthFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
if (collectionConfig.auth.verify) {
if (reservedAPIKeyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
if (reservedVerifyFieldNames.includes(field.name)) {
throw new ReservedFieldName(field, field.name)
}
}
}
}
// assert that field names do not contain forbidden characters // assert that field names do not contain forbidden characters
if (fieldAffectsData(field) && field.name.includes('.')) { if (fieldAffectsData && field.name.includes('.')) {
throw new InvalidFieldName(field, field.name) throw new InvalidFieldName(field, field.name)
} }
@@ -122,6 +166,7 @@ export const sanitizeFields = async ({
const relationships = Array.isArray(field.relationTo) const relationships = Array.isArray(field.relationTo)
? field.relationTo ? field.relationTo
: [field.relationTo] : [field.relationTo]
relationships.forEach((relationship: string) => { relationships.forEach((relationship: string) => {
if (!validRelationships.includes(relationship)) { if (!validRelationships.includes(relationship)) {
throw new InvalidFieldRelationship(field, relationship) throw new InvalidFieldRelationship(field, relationship)
@@ -135,6 +180,7 @@ export const sanitizeFields = async ({
) )
field.minRows = field.min field.minRows = field.min
} }
if (field.max && !field.maxRows) { if (field.max && !field.maxRows) {
console.warn( console.warn(
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`, `(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
@@ -160,7 +206,7 @@ export const sanitizeFields = async ({
field.labels = field.labels || formatLabels(field.name) field.labels = field.labels || formatLabels(field.name)
} }
if (fieldAffectsData(field)) { if (fieldAffectsData) {
if (existingFieldNames.has(field.name)) { if (existingFieldNames.has(field.name)) {
throw new DuplicateFieldName(field.name) throw new DuplicateFieldName(field.name)
} else if (!['blockName', 'id'].includes(field.name)) { } else if (!['blockName', 'id'].includes(field.name)) {
@@ -254,9 +300,11 @@ export const sanitizeFields = async ({
block.fields = block.fields.concat(baseBlockFields) block.fields = block.fields.concat(baseBlockFields)
block.labels = !block.labels ? formatLabels(block.slug) : block.labels block.labels = !block.labels ? formatLabels(block.slug) : block.labels
block.fields = await sanitizeFields({ block.fields = await sanitizeFields({
collectionConfig,
config, config,
existingFieldNames: new Set(), existingFieldNames: new Set(),
fields: block.fields, fields: block.fields,
isTopLevelField: false,
parentIsLocalized: parentIsLocalized || field.localized, parentIsLocalized: parentIsLocalized || field.localized,
requireFieldLevelRichTextEditor, requireFieldLevelRichTextEditor,
richTextSanitizationPromises, richTextSanitizationPromises,
@@ -267,12 +315,12 @@ export const sanitizeFields = async ({
if ('fields' in field && field.fields) { if ('fields' in field && field.fields) {
field.fields = await sanitizeFields({ field.fields = await sanitizeFields({
collectionConfig,
config, config,
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames, existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
fields: field.fields, fields: field.fields,
joinPath: fieldAffectsData(field) isTopLevelField: isTopLevelField && !fieldAffectsData,
? `${joinPath ? joinPath + '.' : ''}${field.name}` joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
: joinPath,
joins, joins,
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field), parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
polymorphicJoins, polymorphicJoins,
@@ -285,7 +333,10 @@ export const sanitizeFields = async ({
if (field.type === 'tabs') { if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) { for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j] const tab = field.tabs[j]
if (tabHasName(tab) && typeof tab.label === 'undefined') {
const isNamedTab = tabHasName(tab)
if (isNamedTab && typeof tab.label === 'undefined') {
tab.label = toWords(tab.name) tab.label = toWords(tab.name)
} }
@@ -296,21 +347,24 @@ export const sanitizeFields = async ({
!tab.id !tab.id
) { ) {
// Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names // Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names
tab.id = tabHasName(tab) ? `${tab.name}_${uuid()}` : uuid() tab.id = isNamedTab ? `${tab.name}_${uuid()}` : uuid()
} }
tab.fields = await sanitizeFields({ tab.fields = await sanitizeFields({
collectionConfig,
config, config,
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames, existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
fields: tab.fields, fields: tab.fields,
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath, isTopLevelField: isTopLevelField && !isNamedTab,
joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
joins, joins,
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized), parentIsLocalized: parentIsLocalized || (isNamedTab && tab.localized),
polymorphicJoins, polymorphicJoins,
requireFieldLevelRichTextEditor, requireFieldLevelRichTextEditor,
richTextSanitizationPromises, richTextSanitizationPromises,
validRelationships, validRelationships,
}) })
field.tabs[j] = tab field.tabs[j] = tab
} }
} }