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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
addDefaultsToCollectionConfig,
|
||||
addDefaultsToLoginWithUsernameConfig,
|
||||
} from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js'
|
||||
import { validateUseAsTitle } from './useAsTitle.js'
|
||||
|
||||
@@ -43,7 +42,9 @@ export const sanitizeCollection = async (
|
||||
if (collection._sanitized) {
|
||||
return collection as SanitizedCollectionConfig
|
||||
}
|
||||
|
||||
collection._sanitized = true
|
||||
|
||||
// /////////////////////////////////
|
||||
// Make copy of collection config
|
||||
// /////////////////////////////////
|
||||
@@ -57,7 +58,9 @@ export const sanitizeCollection = async (
|
||||
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
|
||||
|
||||
const joins: SanitizedJoins = {}
|
||||
|
||||
const polymorphicJoins: SanitizedJoin[] = []
|
||||
|
||||
sanitized.fields = await sanitizeFields({
|
||||
collectionConfig: sanitized,
|
||||
config,
|
||||
@@ -96,17 +99,21 @@ export const sanitizeCollection = async (
|
||||
// add default timestamps fields only as needed
|
||||
let hasUpdatedAt: boolean | null = null
|
||||
let hasCreatedAt: boolean | null = null
|
||||
|
||||
sanitized.fields.some((field) => {
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.name === 'updatedAt') {
|
||||
hasUpdatedAt = true
|
||||
}
|
||||
|
||||
if (field.name === 'createdAt') {
|
||||
hasCreatedAt = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasCreatedAt && hasUpdatedAt
|
||||
})
|
||||
|
||||
if (!hasUpdatedAt) {
|
||||
sanitized.fields.push({
|
||||
name: 'updatedAt',
|
||||
@@ -119,6 +126,7 @@ export const sanitizeCollection = async (
|
||||
label: ({ t }) => t('general:updatedAt'),
|
||||
})
|
||||
}
|
||||
|
||||
if (!hasCreatedAt) {
|
||||
sanitized.fields.push({
|
||||
name: 'createdAt',
|
||||
@@ -175,9 +183,6 @@ export const sanitizeCollection = async (
|
||||
sanitized.upload = {}
|
||||
}
|
||||
|
||||
// sanitize fields for reserved names
|
||||
sanitizeUploadFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
|
||||
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
|
||||
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
|
||||
@@ -195,9 +200,6 @@ export const sanitizeCollection = async (
|
||||
}
|
||||
|
||||
if (sanitized.auth) {
|
||||
// sanitize fields for reserved names
|
||||
sanitizeAuthFields(sanitized.fields, sanitized)
|
||||
|
||||
sanitized.auth = addDefaultsToAuthConfig(
|
||||
typeof sanitized.auth === 'boolean' ? {} : sanitized.auth,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionConfig } from '../../index.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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
// add default user collection if none provided
|
||||
if (!sanitizedConfig?.admin?.user) {
|
||||
const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth))
|
||||
|
||||
if (firstCollectionWithAuth) {
|
||||
sanitizedConfig.admin.user = firstCollectionWithAuth.slug
|
||||
} else {
|
||||
@@ -69,6 +70,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
|
||||
const userCollection = sanitizedConfig.collections.find(
|
||||
({ slug }) => slug === sanitizedConfig.admin.user,
|
||||
)
|
||||
|
||||
if (!userCollection || !userCollection.auth) {
|
||||
throw new InvalidConfiguration(
|
||||
`${sanitizedConfig.admin.user} is not a valid admin user collection`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Config } from '../../config/types.js'
|
||||
import type { CollectionConfig, Field } from '../../index.js'
|
||||
|
||||
import { ReservedFieldName } from '../../errors/index.js'
|
||||
import { sanitizeCollection } from './sanitize.js'
|
||||
import { sanitizeCollection } from '../../collections/config/sanitize.js'
|
||||
|
||||
describe('reservedFieldNames - collections -', () => {
|
||||
const config = {
|
||||
@@ -25,6 +25,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -53,6 +54,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -93,6 +95,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -121,6 +124,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
@@ -149,6 +153,7 @@ describe('reservedFieldNames - collections -', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
48
packages/payload/src/fields/config/reservedFieldNames.ts
Normal file
48
packages/payload/src/fields/config/reservedFieldNames.ts
Normal 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' */
|
||||
]
|
||||
@@ -11,9 +11,12 @@ import type {
|
||||
|
||||
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
|
||||
import { sanitizeFields } from './sanitize.js'
|
||||
import { CollectionConfig } from '../../index.js'
|
||||
|
||||
describe('sanitizeFields', () => {
|
||||
const config = {} as Config
|
||||
const collectionConfig = {} as CollectionConfig
|
||||
|
||||
it('should throw on missing type field', async () => {
|
||||
const fields: Field[] = [
|
||||
// @ts-expect-error
|
||||
@@ -22,14 +25,17 @@ describe('sanitizeFields', () => {
|
||||
label: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
}).rejects.toThrow(MissingFieldType)
|
||||
})
|
||||
|
||||
it('should throw on invalid field name', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -38,9 +44,11 @@ describe('sanitizeFields', () => {
|
||||
label: 'some.collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
@@ -55,17 +63,21 @@ describe('sanitizeFields', () => {
|
||||
type: 'text',
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Some Field')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
})
|
||||
|
||||
it('should allow auto-label override', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -74,13 +86,16 @@ describe('sanitizeFields', () => {
|
||||
label: 'Do not label',
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual('Do not label')
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
@@ -95,13 +110,16 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as TextField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('someField')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('text')
|
||||
@@ -119,18 +137,22 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
label: false,
|
||||
}
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields: [arrayField],
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as ArrayField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
expect(sanitizedField.labels).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow label opt-out for blocks', async () => {
|
||||
const fields: Field[] = [
|
||||
{
|
||||
@@ -150,13 +172,16 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as BlocksField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('noLabelBlock')
|
||||
expect(sanitizedField.label).toStrictEqual(false)
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -177,13 +202,16 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as ArrayField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('items')
|
||||
expect(sanitizedField.label).toStrictEqual('Items')
|
||||
expect(sanitizedField.type).toStrictEqual('array')
|
||||
@@ -203,13 +231,16 @@ describe('sanitizeFields', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as BlocksField
|
||||
|
||||
expect(sanitizedField.name).toStrictEqual('specialBlock')
|
||||
expect(sanitizedField.label).toStrictEqual('Special Block')
|
||||
expect(sanitizedField.type).toStrictEqual('blocks')
|
||||
@@ -217,6 +248,7 @@ describe('sanitizeFields', () => {
|
||||
plural: 'Special Blocks',
|
||||
singular: 'Special Block',
|
||||
})
|
||||
|
||||
expect((sanitizedField.blocks[0].fields[0] as NumberField).label).toStrictEqual('Test Number')
|
||||
})
|
||||
})
|
||||
@@ -232,8 +264,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: 'some-collection',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -247,8 +280,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: ['some-collection', 'another-collection'],
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -265,6 +299,7 @@ describe('sanitizeFields', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'layout',
|
||||
@@ -273,8 +308,9 @@ describe('sanitizeFields', () => {
|
||||
label: 'Layout Blocks',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
@@ -288,8 +324,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: 'not-valid',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -303,8 +340,9 @@ describe('sanitizeFields', () => {
|
||||
relationTo: ['some-collection', 'not-valid'],
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -321,6 +359,7 @@ describe('sanitizeFields', () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
name: 'layout',
|
||||
@@ -329,8 +368,9 @@ describe('sanitizeFields', () => {
|
||||
label: 'Layout Blocks',
|
||||
},
|
||||
]
|
||||
|
||||
await expect(async () => {
|
||||
await sanitizeFields({ config, fields, validRelationships })
|
||||
await sanitizeFields({ config, collectionConfig, fields, validRelationships })
|
||||
}).rejects.toThrow(InvalidFieldRelationship)
|
||||
})
|
||||
|
||||
@@ -346,19 +386,23 @@ describe('sanitizeFields', () => {
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
)[0] as CheckboxField
|
||||
|
||||
expect(sanitizedField.defaultValue).toStrictEqual(false)
|
||||
})
|
||||
|
||||
it('should return empty field array if no fields', async () => {
|
||||
const sanitizedFields = await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields: [],
|
||||
validRelationships: [],
|
||||
})
|
||||
|
||||
expect(sanitizedFields).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
@@ -385,9 +429,11 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
@@ -416,9 +462,11 @@ describe('sanitizeFields', () => {
|
||||
label: false,
|
||||
},
|
||||
]
|
||||
|
||||
const sanitizedField = (
|
||||
await sanitizeFields({
|
||||
config,
|
||||
collectionConfig,
|
||||
fields,
|
||||
validRelationships: [],
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MissingEditorProp,
|
||||
MissingFieldType,
|
||||
} from '../../errors/index.js'
|
||||
import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
|
||||
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
|
||||
import { baseBlockFields } from '../baseFields/baseBlockFields.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 { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
|
||||
import { validations } from '../validations.js'
|
||||
import {
|
||||
reservedAPIKeyFieldNames,
|
||||
reservedBaseAuthFieldNames,
|
||||
reservedBaseUploadFieldNames,
|
||||
reservedVerifyFieldNames,
|
||||
} from './reservedFieldNames.js'
|
||||
import { sanitizeJoinField } from './sanitizeJoinField.js'
|
||||
import { fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
|
||||
import { fieldAffectsData as _fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
|
||||
|
||||
type Args = {
|
||||
collectionConfig?: CollectionConfig
|
||||
config: Config
|
||||
existingFieldNames?: Set<string>
|
||||
fields: Field[]
|
||||
/**
|
||||
* Used to prevent unnecessary sanitization of fields that are not top-level.
|
||||
*/
|
||||
isTopLevelField?: boolean
|
||||
joinPath?: string
|
||||
/**
|
||||
* When not passed in, assume that join are not supported (globals, arrays, blocks)
|
||||
@@ -39,7 +50,6 @@ type Args = {
|
||||
joins?: SanitizedJoins
|
||||
parentIsLocalized: boolean
|
||||
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.
|
||||
*
|
||||
@@ -59,9 +69,11 @@ type Args = {
|
||||
}
|
||||
|
||||
export const sanitizeFields = async ({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames = new Set(),
|
||||
fields,
|
||||
isTopLevelField = true,
|
||||
joinPath = '',
|
||||
joins,
|
||||
parentIsLocalized,
|
||||
@@ -80,6 +92,7 @@ export const sanitizeFields = async ({
|
||||
if ('_sanitized' in field && field._sanitized === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ('_sanitized' in field) {
|
||||
field._sanitized = true
|
||||
}
|
||||
@@ -88,8 +101,39 @@ export const sanitizeFields = async ({
|
||||
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
|
||||
if (fieldAffectsData(field) && field.name.includes('.')) {
|
||||
if (fieldAffectsData && field.name.includes('.')) {
|
||||
throw new InvalidFieldName(field, field.name)
|
||||
}
|
||||
|
||||
@@ -122,6 +166,7 @@ export const sanitizeFields = async ({
|
||||
const relationships = Array.isArray(field.relationTo)
|
||||
? field.relationTo
|
||||
: [field.relationTo]
|
||||
|
||||
relationships.forEach((relationship: string) => {
|
||||
if (!validRelationships.includes(relationship)) {
|
||||
throw new InvalidFieldRelationship(field, relationship)
|
||||
@@ -135,6 +180,7 @@ export const sanitizeFields = async ({
|
||||
)
|
||||
field.minRows = field.min
|
||||
}
|
||||
|
||||
if (field.max && !field.maxRows) {
|
||||
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.`,
|
||||
@@ -160,7 +206,7 @@ export const sanitizeFields = async ({
|
||||
field.labels = field.labels || formatLabels(field.name)
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field)) {
|
||||
if (fieldAffectsData) {
|
||||
if (existingFieldNames.has(field.name)) {
|
||||
throw new DuplicateFieldName(field.name)
|
||||
} else if (!['blockName', 'id'].includes(field.name)) {
|
||||
@@ -254,9 +300,11 @@ export const sanitizeFields = async ({
|
||||
block.fields = block.fields.concat(baseBlockFields)
|
||||
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
|
||||
block.fields = await sanitizeFields({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: new Set(),
|
||||
fields: block.fields,
|
||||
isTopLevelField: false,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
@@ -267,12 +315,12 @@ export const sanitizeFields = async ({
|
||||
|
||||
if ('fields' in field && field.fields) {
|
||||
field.fields = await sanitizeFields({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
|
||||
existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
|
||||
fields: field.fields,
|
||||
joinPath: fieldAffectsData(field)
|
||||
? `${joinPath ? joinPath + '.' : ''}${field.name}`
|
||||
: joinPath,
|
||||
isTopLevelField: isTopLevelField && !fieldAffectsData,
|
||||
joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
|
||||
polymorphicJoins,
|
||||
@@ -285,7 +333,10 @@ export const sanitizeFields = async ({
|
||||
if (field.type === 'tabs') {
|
||||
for (let j = 0; j < field.tabs.length; 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)
|
||||
}
|
||||
|
||||
@@ -296,21 +347,24 @@ export const sanitizeFields = async ({
|
||||
!tab.id
|
||||
) {
|
||||
// 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({
|
||||
collectionConfig,
|
||||
config,
|
||||
existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
|
||||
existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
|
||||
fields: tab.fields,
|
||||
joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||
isTopLevelField: isTopLevelField && !isNamedTab,
|
||||
joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
|
||||
joins,
|
||||
parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
|
||||
parentIsLocalized: parentIsLocalized || (isNamedTab && tab.localized),
|
||||
polymorphicJoins,
|
||||
requireFieldLevelRichTextEditor,
|
||||
richTextSanitizationPromises,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
field.tabs[j] = tab
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user