Skip to content

Commit b705af4

Browse files
chore: extract sanitizeField from sanitizeFields (#16133)
Extracts per-field sanitization logic into a standalone `sanitizeField` function that can be called independently. The `sanitizeFields` function now delegates to `sanitizeField` in a loop, maintaining identical behavior while exposing the core logic. This enables sanitizing individual fields after initial config sanitization, which is needed for the orderable optimization in the follow-up PR. Exports `sanitizeField` and `SanitizeFieldArgs` from the payload package.
1 parent b63198d commit b705af4

6 files changed

Lines changed: 594 additions & 438 deletions

File tree

packages/payload/src/collections/config/sanitize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Config, SanitizedConfig } from '../../config/types.js'
2+
import type { OrderableJoinInfo } from '../../fields/config/sanitizeJoinField.js'
23
import type {
34
CollectionConfig,
45
SanitizedCollectionConfig,
@@ -38,6 +39,10 @@ export const sanitizeCollection = async (
3839
*/
3940
richTextSanitizationPromises?: Array<(config: SanitizedConfig) => Promise<void>>,
4041
_validRelationships?: string[],
42+
/**
43+
* Tracker for orderable join fields - populated during sanitization
44+
*/
45+
orderableJoins?: OrderableJoinInfo[],
4146
): Promise<SanitizedCollectionConfig> => {
4247
if (collection._sanitized) {
4348
return collection as SanitizedCollectionConfig
@@ -67,6 +72,7 @@ export const sanitizeCollection = async (
6772
fields: sanitized.fields,
6873
joinPath: '',
6974
joins,
75+
orderableJoins,
7076
parentIsLocalized: false,
7177
polymorphicJoins,
7278
richTextSanitizationPromises,

packages/payload/src/config/orderable/index.ts

Lines changed: 25 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,31 @@
11
import { status as httpStatus } from 'http-status'
22

33
import type { BeforeChangeHook, CollectionConfig } from '../../collections/config/types.js'
4-
import type { Field } from '../../fields/config/types.js'
4+
import type { Config } from '../../config/types.js'
5+
import type { Field, TextField } from '../../fields/config/types.js'
56
import type { Endpoint, PayloadHandler, SanitizedConfig } from '../types.js'
67

78
import { executeAccess } from '../../auth/executeAccess.js'
89
import { APIError } from '../../errors/index.js'
10+
import { sanitizeField } from '../../fields/config/sanitize.js'
911
import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js'
1012
import { commitTransaction } from '../../utilities/commitTransaction.js'
1113
import { initTransaction } from '../../utilities/initTransaction.js'
1214
import { killTransaction } from '../../utilities/killTransaction.js'
13-
import { traverseFields } from '../../utilities/traverseFields.js'
1415
import { generateKeyBetween, generateNKeysBetween } from './fractional-indexing.js'
1516
import { getJoinScopeContext } from './utils/getJoinScopeContext.js'
1617
import { getJoinScopeWhereFromDocData } from './utils/getJoinScopeWhereFromDocData.js'
1718
import { resolvePendingTargetKey } from './utils/resolvePendingTargetKey.js'
1819

19-
/**
20-
* This function creates:
21-
* - N fields per collection, named `_order` or `_<collection>_<joinField>_order`
22-
* - 1 hook per collection
23-
* - 1 endpoint per app
24-
*
25-
* Also, if collection.defaultSort or joinField.defaultSort is not set, it will be set to the orderable field.
26-
*/
27-
export const setupOrderable = (config: SanitizedConfig) => {
28-
const fieldsToAdd = new Map<CollectionConfig, string[]>()
29-
const joinFieldPathsByCollection = new Map<string, Map<string, string>>()
30-
31-
config.collections.forEach((collection) => {
32-
if (collection.orderable) {
33-
const currentFields = fieldsToAdd.get(collection) || []
34-
fieldsToAdd.set(collection, [...currentFields, '_order'])
35-
collection.defaultSort = collection.defaultSort ?? '_order'
36-
}
37-
38-
traverseFields({
39-
callback: ({ field, parentRef, ref }) => {
40-
if (field.type === 'array' || field.type === 'blocks') {
41-
return false
42-
}
43-
if (field.type === 'group' || field.type === 'tab') {
44-
// @ts-expect-error ref is untyped
45-
const parentPrefix = parentRef?.prefix ? `${parentRef.prefix}_` : ''
46-
// @ts-expect-error ref is untyped
47-
ref.prefix = `${parentPrefix}${field.name}`
48-
}
49-
if (field.type === 'join' && field.orderable === true) {
50-
if (Array.isArray(field.collection)) {
51-
throw new APIError(
52-
'Orderable joins must target a single collection',
53-
httpStatus.BAD_REQUEST,
54-
{},
55-
true,
56-
)
57-
}
58-
const relationshipCollection = config.collections.find((c) => c.slug === field.collection)
59-
if (!relationshipCollection) {
60-
return false
61-
}
62-
field.defaultSort = field.defaultSort ?? `_${field.collection}_${field.name}_order`
63-
const currentFields = fieldsToAdd.get(relationshipCollection) || []
64-
// @ts-expect-error ref is untyped
65-
const prefix = parentRef?.prefix ? `${parentRef.prefix}_` : ''
66-
const joinOrderableFieldName = `_${field.collection}_${prefix}${field.name}_order`
67-
fieldsToAdd.set(relationshipCollection, [...currentFields, joinOrderableFieldName])
68-
69-
const currentJoinFieldPaths =
70-
joinFieldPathsByCollection.get(relationshipCollection.slug) || new Map<string, string>()
71-
currentJoinFieldPaths.set(joinOrderableFieldName, field.on)
72-
joinFieldPathsByCollection.set(relationshipCollection.slug, currentJoinFieldPaths)
73-
}
74-
},
75-
fields: collection.fields,
76-
})
77-
})
78-
79-
Array.from(fieldsToAdd.entries()).forEach(([collection, orderableFields]) => {
80-
addOrderableFieldsAndHook(collection, orderableFields, joinFieldPathsByCollection)
81-
})
82-
83-
if (fieldsToAdd.size > 0) {
84-
addOrderableEndpoint(config, joinFieldPathsByCollection)
85-
}
86-
}
87-
88-
export const addOrderableFieldsAndHook = (
20+
export const addOrderableFieldsAndHook = async (
8921
collection: CollectionConfig,
22+
config: Config,
9023
orderableFieldNames: string[],
9124
joinFieldPathsByCollection?: Map<string, Map<string, string>>,
9225
) => {
93-
// 1. Add field
94-
orderableFieldNames.forEach((orderableFieldName) => {
95-
const orderField: Field = {
26+
// 1. Add fields
27+
for (const orderableFieldName of orderableFieldNames) {
28+
const orderField: TextField = {
9629
name: orderableFieldName,
9730
type: 'text',
9831
admin: {
@@ -114,8 +47,24 @@ export const addOrderableFieldsAndHook = (
11447
index: true,
11548
}
11649

50+
// Sanitize the field using the standard sanitization logic
51+
await sanitizeField({
52+
collectionConfig: collection,
53+
config,
54+
existingFieldNames: new Set(),
55+
field: orderField,
56+
index: 0,
57+
isTopLevelField: true,
58+
joinPath: '',
59+
parentIndexPath: '',
60+
parentIsLocalized: false,
61+
parentSchemaPath: '',
62+
requireFieldLevelRichTextEditor: false,
63+
validRelationships: null,
64+
})
65+
11766
collection.fields.unshift(orderField)
118-
})
67+
}
11968

12069
// 2. Add hook
12170
if (!collection.hooks) {

packages/payload/src/config/sanitize.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AcceptedLanguages } from '@payloadcms/translations'
33
import { en } from '@payloadcms/translations/languages/en'
44
import { deepMergeSimple } from '@payloadcms/translations/utilities'
55

6+
import type { OrderableJoinInfo } from '../fields/config/sanitizeJoinField.js'
67
import type { CollectionSlug, GlobalSlug, SanitizedCollectionConfig } from '../index.js'
78
import type { SanitizedJobsConfig } from '../queues/config/types/index.js'
89
import type {
@@ -33,12 +34,12 @@ import { getPreferencesCollection, preferencesCollectionSlug } from '../preferen
3334
import { getQueryPresetsConfig, queryPresetsCollectionSlug } from '../query-presets/config.js'
3435
import { getDefaultJobsCollection, jobsCollectionSlug } from '../queues/config/collection.js'
3536
import { getJobStatsGlobal } from '../queues/config/global.js'
36-
import { flattenBlock } from '../utilities/flattenAllFields.js'
37+
import { flattenAllFields, flattenBlock } from '../utilities/flattenAllFields.js'
3738
import { hasScheduledPublishEnabled } from '../utilities/getVersionsConfig.js'
3839
import { validateTimezones } from '../utilities/validateTimezones.js'
3940
import { getSchedulePublishTask } from '../versions/schedule/job.js'
4041
import { addDefaultsToConfig } from './defaults.js'
41-
import { setupOrderable } from './orderable/index.js'
42+
import { addOrderableEndpoint, addOrderableFieldsAndHook } from './orderable/index.js'
4243

4344
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
4445
const sanitizedConfig = { ...configToSanitize }
@@ -118,9 +119,6 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
118119

119120
const config: Partial<SanitizedConfig> = sanitizeAdminConfig(configWithDefaults)
120121

121-
// Add orderable fields
122-
setupOrderable(config as SanitizedConfig)
123-
124122
if (!config.endpoints) {
125123
config.endpoints = []
126124
}
@@ -261,6 +259,9 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
261259

262260
const folderEnabledCollections: SanitizedCollectionConfig[] = []
263261

262+
// Track orderable join fields during sanitization
263+
const orderableJoins: OrderableJoinInfo[] = []
264+
264265
for (let i = 0; i < config.collections!.length; i++) {
265266
if (collectionSlugs.has(config.collections![i]!.slug)) {
266267
throw new DuplicateCollection('slug', config.collections![i]!.slug)
@@ -296,13 +297,60 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
296297
config.collections![i]!,
297298
richTextSanitizationPromises,
298299
validRelationships,
300+
orderableJoins,
299301
)
300302

301303
if (config.folders !== false && config.collections![i]!.folders) {
302304
folderEnabledCollections.push(config.collections![i]!)
303305
}
304306
}
305307

308+
// Process orderable features after all collections are sanitized
309+
const fieldsToAdd = new Map<SanitizedCollectionConfig, string[]>()
310+
const joinFieldPathsByCollection = new Map<string, Map<string, string>>()
311+
312+
// Handle collection.orderable
313+
for (const collection of config.collections!) {
314+
if (collection.orderable) {
315+
const currentFields = fieldsToAdd.get(collection) || []
316+
fieldsToAdd.set(collection, [...currentFields, '_order'])
317+
collection.defaultSort = collection.defaultSort ?? '_order'
318+
}
319+
}
320+
321+
// Handle orderable join fields (tracked during sanitization)
322+
for (const joinInfo of orderableJoins) {
323+
const targetCollection = config.collections!.find(
324+
(c) => c.slug === joinInfo.targetCollectionSlug,
325+
)
326+
if (targetCollection) {
327+
const currentFields = fieldsToAdd.get(targetCollection) || []
328+
fieldsToAdd.set(targetCollection, [...currentFields, joinInfo.orderFieldName])
329+
330+
const currentJoinFieldPaths =
331+
joinFieldPathsByCollection.get(targetCollection.slug) || new Map<string, string>()
332+
currentJoinFieldPaths.set(joinInfo.orderFieldName, joinInfo.joinFieldOn)
333+
joinFieldPathsByCollection.set(targetCollection.slug, currentJoinFieldPaths)
334+
}
335+
}
336+
337+
// Add fields, hooks, and update flattenedFields
338+
for (const [collection, orderableFields] of fieldsToAdd) {
339+
await addOrderableFieldsAndHook(
340+
collection,
341+
config as unknown as Config,
342+
orderableFields,
343+
joinFieldPathsByCollection,
344+
)
345+
// Regenerate flattenedFields since we added new fields
346+
collection.flattenedFields = flattenAllFields({ fields: collection.fields })
347+
}
348+
349+
// Add endpoint if any orderable features exist
350+
if (fieldsToAdd.size > 0) {
351+
addOrderableEndpoint(config as SanitizedConfig, joinFieldPathsByCollection)
352+
}
353+
306354
if (config.globals!.length > 0) {
307355
for (let i = 0; i < config.globals!.length; i++) {
308356
if (hasScheduledPublishEnabled(config.globals![i]!)) {

0 commit comments

Comments
 (0)