@depup/payload 3.81.0-depup.0 → 3.82.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +4 -4
  2. package/changes.json +3 -3
  3. package/dist/bin/generateTypes.d.ts.map +1 -1
  4. package/dist/bin/generateTypes.js +8 -0
  5. package/dist/bin/generateTypes.js.map +1 -1
  6. package/dist/collections/config/sanitize.d.ts +2 -1
  7. package/dist/collections/config/sanitize.d.ts.map +1 -1
  8. package/dist/collections/config/sanitize.js +4 -1
  9. package/dist/collections/config/sanitize.js.map +1 -1
  10. package/dist/collections/operations/utilities/update.d.ts.map +1 -1
  11. package/dist/collections/operations/utilities/update.js +2 -1
  12. package/dist/collections/operations/utilities/update.js.map +1 -1
  13. package/dist/config/orderable/index.d.ts +2 -10
  14. package/dist/config/orderable/index.d.ts.map +1 -1
  15. package/dist/config/orderable/index.js +20 -67
  16. package/dist/config/orderable/index.js.map +1 -1
  17. package/dist/config/sanitize.d.ts.map +1 -1
  18. package/dist/config/sanitize.js +45 -5
  19. package/dist/config/sanitize.js.map +1 -1
  20. package/dist/config/types.d.ts +20 -0
  21. package/dist/config/types.d.ts.map +1 -1
  22. package/dist/config/types.js.map +1 -1
  23. package/dist/fields/config/sanitize.d.ts +54 -2
  24. package/dist/fields/config/sanitize.d.ts.map +1 -1
  25. package/dist/fields/config/sanitize.js +333 -285
  26. package/dist/fields/config/sanitize.js.map +1 -1
  27. package/dist/fields/config/sanitize.spec.js +82 -0
  28. package/dist/fields/config/sanitize.spec.js.map +1 -1
  29. package/dist/fields/config/sanitizeJoinField.d.ts +15 -1
  30. package/dist/fields/config/sanitizeJoinField.d.ts.map +1 -1
  31. package/dist/fields/config/sanitizeJoinField.js +17 -1
  32. package/dist/fields/config/sanitizeJoinField.js.map +1 -1
  33. package/dist/globals/operations/findOne.d.ts +1 -0
  34. package/dist/globals/operations/findOne.d.ts.map +1 -1
  35. package/dist/globals/operations/findOne.js +10 -3
  36. package/dist/globals/operations/findOne.js.map +1 -1
  37. package/dist/globals/operations/local/findOne.d.ts +8 -0
  38. package/dist/globals/operations/local/findOne.d.ts.map +1 -1
  39. package/dist/globals/operations/local/findOne.js +2 -1
  40. package/dist/globals/operations/local/findOne.js.map +1 -1
  41. package/dist/globals/operations/update.d.ts.map +1 -1
  42. package/dist/globals/operations/update.js +2 -1
  43. package/dist/globals/operations/update.js.map +1 -1
  44. package/dist/index.bundled.d.ts +112 -18
  45. package/dist/index.d.ts +3 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/uploads/checkFileAccess.d.ts +2 -1
  50. package/dist/uploads/checkFileAccess.d.ts.map +1 -1
  51. package/dist/uploads/checkFileAccess.js +25 -14
  52. package/dist/uploads/checkFileAccess.js.map +1 -1
  53. package/dist/uploads/checkFileAccess.spec.js +109 -0
  54. package/dist/uploads/checkFileAccess.spec.js.map +1 -0
  55. package/dist/uploads/endpoints/getFile.d.ts.map +1 -1
  56. package/dist/uploads/endpoints/getFile.js +4 -1
  57. package/dist/uploads/endpoints/getFile.js.map +1 -1
  58. package/dist/uploads/types.d.ts +2 -1
  59. package/dist/uploads/types.d.ts.map +1 -1
  60. package/dist/uploads/types.js.map +1 -1
  61. package/dist/utilities/slugify.d.ts.map +1 -1
  62. package/dist/utilities/slugify.js +1 -1
  63. package/dist/utilities/slugify.js.map +1 -1
  64. package/dist/utilities/validateTimezones.d.ts.map +1 -1
  65. package/dist/utilities/validateTimezones.js +7 -0
  66. package/dist/utilities/validateTimezones.js.map +1 -1
  67. package/dist/versions/types.d.ts +1 -1
  68. package/dist/versions/types.js.map +1 -1
  69. package/package.json +8 -8
@@ -14,132 +14,146 @@ import { validations } from '../validations.js';
14
14
  import { reservedAPIKeyFieldNames, reservedBaseAuthFieldNames, reservedBaseUploadFieldNames, reservedVerifyFieldNames } from './reservedFieldNames.js';
15
15
  import { sanitizeJoinField } from './sanitizeJoinField.js';
16
16
  import { fieldAffectsData as _fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js';
17
- export const sanitizeFields = async ({ collectionConfig, config, existingFieldNames = new Set(), fields, globalConfig, isTopLevelField = true, joinPath = '', joins, parentIndexPath = '', parentIsLocalized, parentSchemaPath = '', polymorphicJoins, requireFieldLevelRichTextEditor = false, richTextSanitizationPromises, validRelationships })=>{
18
- if (!fields) {
19
- return [];
17
+ /**
18
+ * Sanitize a single field. Handles all per-field logic including:
19
+ * - Validation setup
20
+ * - Hooks/access/admin defaults
21
+ * - Type-specific handling
22
+ * - Recursive sanitization of nested fields
23
+ *
24
+ * @returns Result containing any fields to insert after this one
25
+ */ export const sanitizeField = async ({ collectionConfig, config, existingFieldNames, field, globalConfig, index, isTopLevelField, joinPath, joins, orderableJoins, parentIndexPath, parentIsLocalized, parentSchemaPath, polymorphicJoins, requireFieldLevelRichTextEditor, richTextSanitizationPromises, validRelationships })=>{
26
+ const result = {};
27
+ if ('_sanitized' in field && field._sanitized === true) {
28
+ return result;
20
29
  }
21
- for(let i = 0; i < fields.length; i++){
22
- const field = fields[i];
23
- if ('_sanitized' in field && field._sanitized === true) {
24
- continue;
25
- }
26
- if ('_sanitized' in field) {
27
- field._sanitized = true;
28
- }
29
- if (!field.type) {
30
- throw new MissingFieldType(field);
30
+ if ('_sanitized' in field) {
31
+ field._sanitized = true;
32
+ }
33
+ if (!field.type) {
34
+ throw new MissingFieldType(field);
35
+ }
36
+ const fieldAffectsData = _fieldAffectsData(field);
37
+ const { indexPath, schemaPath } = getFieldPaths({
38
+ field,
39
+ index,
40
+ parentIndexPath,
41
+ parentSchemaPath
42
+ });
43
+ // Reserved field name checks
44
+ if (isTopLevelField && fieldAffectsData && field.name) {
45
+ if (collectionConfig && collectionConfig.upload) {
46
+ if (reservedBaseUploadFieldNames.includes(field.name)) {
47
+ throw new ReservedFieldName(field, field.name);
48
+ }
31
49
  }
32
- const fieldAffectsData = _fieldAffectsData(field);
33
- const { indexPath, schemaPath } = getFieldPaths({
34
- field,
35
- index: i,
36
- parentIndexPath,
37
- parentSchemaPath
38
- });
39
- if (isTopLevelField && fieldAffectsData && field.name) {
40
- if (collectionConfig && collectionConfig.upload) {
41
- if (reservedBaseUploadFieldNames.includes(field.name)) {
42
- throw new ReservedFieldName(field, field.name);
43
- }
50
+ if (collectionConfig && collectionConfig.auth && typeof collectionConfig.auth === 'object' && !collectionConfig.auth.disableLocalStrategy) {
51
+ if (reservedBaseAuthFieldNames.includes(field.name)) {
52
+ throw new ReservedFieldName(field, field.name);
44
53
  }
45
- if (collectionConfig && collectionConfig.auth && typeof collectionConfig.auth === 'object' && !collectionConfig.auth.disableLocalStrategy) {
46
- if (reservedBaseAuthFieldNames.includes(field.name)) {
54
+ if (collectionConfig.auth.verify) {
55
+ // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
56
+ if (reservedAPIKeyFieldNames.includes(field.name)) {
47
57
  throw new ReservedFieldName(field, field.name);
48
58
  }
49
- if (collectionConfig.auth.verify) {
50
- // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
51
- if (reservedAPIKeyFieldNames.includes(field.name)) {
52
- throw new ReservedFieldName(field, field.name);
53
- }
54
- // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
55
- if (reservedVerifyFieldNames.includes(field.name)) {
56
- throw new ReservedFieldName(field, field.name);
57
- }
59
+ // @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
60
+ if (reservedVerifyFieldNames.includes(field.name)) {
61
+ throw new ReservedFieldName(field, field.name);
58
62
  }
59
63
  }
60
64
  }
61
- // assert that field names do not contain forbidden characters
62
- if (fieldAffectsData && field.name.includes('.')) {
63
- throw new InvalidFieldName(field, field.name);
64
- }
65
- // Auto-label
66
- if ('name' in field && field.name && typeof field.label !== 'object' && typeof field.label !== 'string' && typeof field.label !== 'function' && field.label !== false) {
67
- field.label = toWords(field.name);
68
- }
69
- if (field.type === 'checkbox' && typeof field.defaultValue === 'undefined' && field.required === true) {
70
- field.defaultValue = false;
65
+ }
66
+ // Invalid field name check
67
+ if (fieldAffectsData && field.name.includes('.')) {
68
+ throw new InvalidFieldName(field, field.name);
69
+ }
70
+ // Auto-label
71
+ if ('name' in field && field.name && typeof field.label !== 'object' && typeof field.label !== 'string' && typeof field.label !== 'function' && field.label !== false) {
72
+ field.label = toWords(field.name);
73
+ }
74
+ // Checkbox default
75
+ if (field.type === 'checkbox' && typeof field.defaultValue === 'undefined' && field.required === true) {
76
+ field.defaultValue = false;
77
+ }
78
+ // Join field sanitization
79
+ if (field.type === 'join') {
80
+ sanitizeJoinField({
81
+ config,
82
+ field,
83
+ joinPath,
84
+ joins,
85
+ orderableJoins,
86
+ parentIsLocalized,
87
+ polymorphicJoins
88
+ });
89
+ }
90
+ // Relationship/upload validation
91
+ if (field.type === 'relationship' || field.type === 'upload') {
92
+ if (Array.isArray(field.relationTo) && field.relationTo.length === 0) {
93
+ throw new Error(`Field "${field.name}" of type "${field.type}" has an empty relationTo array. At least one collection must be specified.`);
71
94
  }
72
- if (field.type === 'join') {
73
- sanitizeJoinField({
74
- config,
75
- field,
76
- joinPath,
77
- joins,
78
- parentIsLocalized,
79
- polymorphicJoins
95
+ if (validRelationships) {
96
+ const relationships = Array.isArray(field.relationTo) ? field.relationTo : [
97
+ field.relationTo
98
+ ];
99
+ relationships.forEach((relationship)=>{
100
+ if (!validRelationships.includes(relationship)) {
101
+ throw new InvalidFieldRelationship(field, relationship);
102
+ }
80
103
  });
81
104
  }
82
- if (field.type === 'relationship' || field.type === 'upload') {
83
- // Validate that relationTo is not empty
84
- if (Array.isArray(field.relationTo) && field.relationTo.length === 0) {
85
- throw new Error(`Field "${field.name}" of type "${field.type}" has an empty relationTo array. At least one collection must be specified.`);
86
- }
87
- if (validRelationships) {
88
- const relationships = Array.isArray(field.relationTo) ? field.relationTo : [
89
- field.relationTo
90
- ];
91
- relationships.forEach((relationship)=>{
92
- if (!validRelationships.includes(relationship)) {
93
- throw new InvalidFieldRelationship(field, relationship);
94
- }
95
- });
96
- }
97
- if (field.min && !field.minRows) {
98
- console.warn(`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`);
99
- field.minRows = field.min;
100
- }
101
- if (field.max && !field.maxRows) {
102
- 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.`);
103
- field.maxRows = field.max;
104
- }
105
+ if (field.min && !field.minRows) {
106
+ console.warn(`(payload): The "min" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "minRows" instead.`);
107
+ field.minRows = field.min;
105
108
  }
106
- if (field.type === 'upload') {
107
- if (!field.admin || !('isSortable' in field.admin)) {
108
- field.admin = {
109
- isSortable: true,
110
- ...field.admin
111
- };
112
- }
109
+ if (field.max && !field.maxRows) {
110
+ 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.`);
111
+ field.maxRows = field.max;
113
112
  }
114
- if (field.type === 'array' && field.fields) {
115
- const hasCustomID = field.fields.some((f)=>'name' in f && f.name === 'id');
116
- if (!hasCustomID) {
117
- field.fields.push(baseIDField);
118
- }
113
+ }
114
+ // Upload isSortable default
115
+ if (field.type === 'upload') {
116
+ if (!field.admin || !('isSortable' in field.admin)) {
117
+ field.admin = {
118
+ isSortable: true,
119
+ ...field.admin
120
+ };
119
121
  }
120
- if ((field.type === 'blocks' || field.type === 'array') && field.label) {
121
- field.labels = field.labels || formatLabels(field.name);
122
+ }
123
+ // Array ID field
124
+ if (field.type === 'array' && field.fields) {
125
+ const hasCustomID = field.fields.some((f)=>'name' in f && f.name === 'id');
126
+ if (!hasCustomID) {
127
+ field.fields.push(baseIDField);
128
+ }
129
+ }
130
+ // Blocks/array labels
131
+ if ((field.type === 'blocks' || field.type === 'array') && field.label) {
132
+ field.labels = field.labels || formatLabels(field.name);
133
+ }
134
+ if (fieldAffectsData) {
135
+ if (existingFieldNames.has(field.name)) {
136
+ throw new DuplicateFieldName(field.name);
137
+ } else if (![
138
+ 'blockName',
139
+ 'id'
140
+ ].includes(field.name)) {
141
+ existingFieldNames.add(field.name);
122
142
  }
123
- if (fieldAffectsData) {
124
- if (existingFieldNames.has(field.name)) {
125
- throw new DuplicateFieldName(field.name);
126
- } else if (![
127
- 'blockName',
128
- 'id'
129
- ].includes(field.name)) {
130
- existingFieldNames.add(field.name);
143
+ if (typeof field.localized !== 'undefined') {
144
+ let shouldDisableLocalized = !config.localization;
145
+ if (process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized !== 'true' && parentIsLocalized && // @todo PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY=true will be the default in 4.0
146
+ process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY !== 'true') {
147
+ shouldDisableLocalized = true;
131
148
  }
132
- if (typeof field.localized !== 'undefined') {
133
- let shouldDisableLocalized = !config.localization;
134
- if (process.env.NEXT_PUBLIC_PAYLOAD_COMPATIBILITY_allowLocalizedWithinLocalized !== 'true' && parentIsLocalized && // @todo PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY=true will be the default in 4.0
135
- process.env.PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY !== 'true') {
136
- shouldDisableLocalized = true;
137
- }
138
- if (shouldDisableLocalized) {
139
- delete field.localized;
140
- }
149
+ if (shouldDisableLocalized) {
150
+ delete field.localized;
141
151
  }
142
- if (typeof field.validate === 'undefined') {
152
+ }
153
+ if (typeof field.validate === 'undefined') {
154
+ if ('virtual' in field && field.virtual) {
155
+ field.validate = ()=>true;
156
+ } else {
143
157
  const defaultValidate = validations[field.type];
144
158
  if (defaultValidate) {
145
159
  field.validate = (val, options)=>defaultValidate(val, {
@@ -150,207 +164,241 @@ export const sanitizeFields = async ({ collectionConfig, config, existingFieldNa
150
164
  field.validate = ()=>true;
151
165
  }
152
166
  }
153
- if (!field.hooks) {
154
- field.hooks = {};
155
- }
156
- if (!field.access) {
157
- field.access = {};
158
- }
159
- setDefaultBeforeDuplicate(field, parentIsLocalized);
160
167
  }
161
- if (!field.admin) {
162
- field.admin = {};
168
+ if (!field.hooks) {
169
+ field.hooks = {};
163
170
  }
164
- // Make sure that the richText field has an editor
165
- if (field.type === 'richText') {
166
- const sanitizeRichText = async (_config)=>{
167
- if (!field.editor) {
168
- if (_config.editor && !requireFieldLevelRichTextEditor) {
169
- // config.editor should be sanitized at this point
170
- field.editor = _config.editor;
171
- } else {
172
- throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
173
- ;
174
- }
175
- }
176
- if (typeof field.editor === 'function') {
177
- field.editor = await field.editor({
178
- config: _config,
179
- isRoot: requireFieldLevelRichTextEditor,
180
- parentIsLocalized: parentIsLocalized || field.localized
181
- });
182
- }
183
- if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
184
- config.i18n.translations = deepMergeSimple(config.i18n.translations, field.editor.i18n);
185
- }
186
- };
187
- if (richTextSanitizationPromises) {
188
- richTextSanitizationPromises.push(sanitizeRichText);
189
- } else {
190
- await sanitizeRichText(config);
191
- }
171
+ if (!field.access) {
172
+ field.access = {};
192
173
  }
193
- if (field.type === 'blocks' && field.blocks) {
194
- if (field.blockReferences && field.blocks?.length) {
195
- throw new Error('You cannot have both blockReferences and blocks in the same blocks field');
196
- }
197
- const blockSlugs = [];
198
- for (const block of field.blockReferences ?? field.blocks){
199
- const blockSlug = typeof block === 'string' ? block : block.slug;
200
- if (blockSlugs.includes(blockSlug)) {
201
- throw new DuplicateFieldName(blockSlug);
202
- }
203
- blockSlugs.push(blockSlug);
204
- if (typeof block === 'string') {
205
- continue;
206
- }
207
- if (block._sanitized === true) {
208
- continue;
174
+ setDefaultBeforeDuplicate(field, parentIsLocalized);
175
+ }
176
+ if (!field.admin) {
177
+ field.admin = {};
178
+ }
179
+ // Make sure that the richText field has an editor
180
+ if (field.type === 'richText') {
181
+ const sanitizeRichText = async (_config)=>{
182
+ if (!field.editor) {
183
+ if (_config.editor && !requireFieldLevelRichTextEditor) {
184
+ // config.editor should be sanitized at this point
185
+ field.editor = _config.editor;
186
+ } else {
187
+ throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
188
+ ;
209
189
  }
210
- block._sanitized = true;
211
- block.fields = block.fields.concat(baseBlockFields);
212
- block.labels = !block.labels ? formatLabels(block.slug) : block.labels;
213
- block.fields = await sanitizeFields({
214
- collectionConfig,
215
- config,
216
- existingFieldNames: new Set(),
217
- fields: block.fields,
218
- isTopLevelField: false,
219
- parentIndexPath: '',
220
- parentIsLocalized: parentIsLocalized || field.localized,
221
- parentSchemaPath: schemaPath + '.' + block.slug,
222
- requireFieldLevelRichTextEditor,
223
- richTextSanitizationPromises,
224
- validRelationships
190
+ }
191
+ if (typeof field.editor === 'function') {
192
+ field.editor = await field.editor({
193
+ config: _config,
194
+ isRoot: requireFieldLevelRichTextEditor,
195
+ parentIsLocalized: parentIsLocalized || field.localized
225
196
  });
226
197
  }
198
+ if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
199
+ config.i18n.translations = deepMergeSimple(config.i18n.translations, field.editor.i18n);
200
+ }
201
+ };
202
+ if (richTextSanitizationPromises) {
203
+ richTextSanitizationPromises.push(sanitizeRichText);
204
+ } else {
205
+ await sanitizeRichText(config);
227
206
  }
228
- if ('fields' in field && field.fields) {
229
- field.fields = await sanitizeFields({
207
+ }
208
+ if (field.type === 'blocks' && field.blocks) {
209
+ if (field.blockReferences && field.blocks?.length) {
210
+ throw new Error('You cannot have both blockReferences and blocks in the same blocks field');
211
+ }
212
+ const blockSlugs = [];
213
+ for (const block of field.blockReferences ?? field.blocks){
214
+ const blockSlug = typeof block === 'string' ? block : block.slug;
215
+ if (blockSlugs.includes(blockSlug)) {
216
+ throw new DuplicateFieldName(blockSlug);
217
+ }
218
+ blockSlugs.push(blockSlug);
219
+ if (typeof block === 'string') {
220
+ continue;
221
+ }
222
+ if (block._sanitized === true) {
223
+ continue;
224
+ }
225
+ block._sanitized = true;
226
+ block.fields = block.fields.concat(baseBlockFields);
227
+ block.labels = !block.labels ? formatLabels(block.slug) : block.labels;
228
+ block.fields = await sanitizeFields({
230
229
  collectionConfig,
231
230
  config,
232
- existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
233
- fields: field.fields,
234
- isTopLevelField: isTopLevelField && !fieldAffectsData,
235
- joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
236
- joins,
237
- parentIndexPath: fieldAffectsData ? '' : indexPath,
238
- parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
239
- parentSchemaPath: schemaPath,
240
- polymorphicJoins,
231
+ existingFieldNames: new Set(),
232
+ fields: block.fields,
233
+ isTopLevelField: false,
234
+ parentIndexPath: '',
235
+ parentIsLocalized: parentIsLocalized || field.localized,
236
+ parentSchemaPath: schemaPath + '.' + block.slug,
241
237
  requireFieldLevelRichTextEditor,
242
238
  richTextSanitizationPromises,
243
239
  validRelationships
244
240
  });
245
241
  }
246
- if (field.type === 'tabs') {
247
- for(let j = 0; j < field.tabs.length; j++){
248
- const tab = field.tabs[j];
249
- const isNamedTab = tabHasName(tab);
250
- if (isNamedTab && typeof tab.label === 'undefined') {
251
- tab.label = toWords(tab.name);
252
- }
253
- const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
254
- field: tab,
255
- index: j,
256
- parentIndexPath: indexPath,
257
- parentSchemaPath: schemaPath
258
- });
259
- if ('admin' in tab && tab.admin?.condition && typeof tab.admin.condition === 'function' && !tab.id) {
260
- tab.id = tabSchemaPath;
261
- }
262
- tab.fields = await sanitizeFields({
263
- collectionConfig,
264
- config,
265
- existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
266
- fields: tab.fields,
267
- isTopLevelField: isTopLevelField && !isNamedTab,
268
- joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
269
- joins,
270
- parentIndexPath: isNamedTab ? '' : tabIndexPath,
271
- parentIsLocalized: parentIsLocalized || isNamedTab && tab.localized,
272
- parentSchemaPath: tabSchemaPath,
273
- polymorphicJoins,
274
- requireFieldLevelRichTextEditor,
275
- richTextSanitizationPromises,
276
- validRelationships
277
- });
278
- field.tabs[j] = tab;
242
+ }
243
+ if ('fields' in field && field.fields) {
244
+ field.fields = await sanitizeFields({
245
+ collectionConfig,
246
+ config,
247
+ existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
248
+ fields: field.fields,
249
+ isTopLevelField: isTopLevelField && !fieldAffectsData,
250
+ joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
251
+ joins,
252
+ orderableJoins,
253
+ parentIndexPath: fieldAffectsData ? '' : indexPath,
254
+ parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
255
+ parentSchemaPath: schemaPath,
256
+ polymorphicJoins,
257
+ requireFieldLevelRichTextEditor,
258
+ richTextSanitizationPromises,
259
+ validRelationships
260
+ });
261
+ }
262
+ if (field.type === 'tabs') {
263
+ for(let j = 0; j < field.tabs.length; j++){
264
+ const tab = field.tabs[j];
265
+ const isNamedTab = tabHasName(tab);
266
+ if (isNamedTab && typeof tab.label === 'undefined') {
267
+ tab.label = toWords(tab.name);
279
268
  }
280
- }
281
- if (field.type === 'ui' && typeof field.admin.disableBulkEdit === 'undefined') {
282
- field.admin.disableBulkEdit = true;
283
- }
284
- fields[i] = field;
285
- // Insert our field after assignment
286
- if (field.type === 'date' && field.timezone) {
287
- const name = field.name + '_tz';
288
- let defaultTimezone = field.timezone && typeof field.timezone === 'object' ? field.timezone.defaultTimezone : config.admin?.timezones?.defaultTimezone;
289
- const required = field.required || field.timezone && typeof field.timezone === 'object' && field.timezone.required;
290
- const supportedTimezones = field.timezone && typeof field.timezone === 'object' && field.timezone.supportedTimezones ? field.timezone.supportedTimezones : config.admin?.timezones?.supportedTimezones;
291
- const options = typeof supportedTimezones === 'function' ? supportedTimezones({
292
- defaultTimezones
293
- }) : supportedTimezones;
294
- validateTimezones({
295
- source: `field "${field.name}" timezone.supportedTimezones`,
296
- timezones: options
269
+ const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
270
+ field: tab,
271
+ index: j,
272
+ parentIndexPath: indexPath,
273
+ parentSchemaPath: schemaPath
297
274
  });
298
- if (options && options.length === 1 && options[0]?.value) {
299
- defaultTimezone = options[0].value;
275
+ if ('admin' in tab && tab.admin?.condition && typeof tab.admin.condition === 'function' && !tab.id) {
276
+ tab.id = tabSchemaPath;
300
277
  }
301
- // Generate label for timezone field
302
- // Use parent field's label + ' Tz' if it's a simple string, otherwise fallback to name
303
- const timezoneLabel = typeof field.label === 'string' ? `${field.label} Tz` : toWords(name);
304
- // Need to set the options here manually so that any database enums are generated correctly
305
- // The UI component will import the options from the config
306
- const baseField = baseTimezoneField({
307
- name,
308
- defaultValue: defaultTimezone,
309
- label: timezoneLabel,
310
- options,
311
- required
278
+ tab.fields = await sanitizeFields({
279
+ collectionConfig,
280
+ config,
281
+ existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
282
+ fields: tab.fields,
283
+ isTopLevelField: isTopLevelField && !isNamedTab,
284
+ joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
285
+ joins,
286
+ orderableJoins,
287
+ parentIndexPath: isNamedTab ? '' : tabIndexPath,
288
+ parentIsLocalized: parentIsLocalized || isNamedTab && tab.localized,
289
+ parentSchemaPath: tabSchemaPath,
290
+ polymorphicJoins,
291
+ requireFieldLevelRichTextEditor,
292
+ richTextSanitizationPromises,
293
+ validRelationships
312
294
  });
313
- // Apply override if provided
314
- const timezoneField = typeof field.timezone === 'object' && typeof field.timezone.override === 'function' ? field.timezone.override({
315
- baseField
316
- }) : baseField;
317
- fields.splice(++i, 0, timezoneField);
295
+ field.tabs[j] = tab;
318
296
  }
319
- if ('virtual' in field && typeof field.virtual === 'string') {
320
- const virtualField = field;
321
- const fields = (collectionConfig || globalConfig)?.fields;
322
- if (fields) {
323
- let flattenFields = flattenAllFields({
324
- fields
325
- });
326
- const paths = field.virtual.split('.');
327
- let isHasMany = false;
328
- for (const [i, segment] of paths.entries()){
329
- const field = flattenFields.find((e)=>e.name === segment);
330
- if (!field) {
331
- break;
332
- }
333
- if (field.type === 'group' || field.type === 'tab' || field.type === 'array') {
334
- flattenFields = field.flattenedFields;
335
- } else if ((field.type === 'relationship' || field.type === 'upload') && i !== paths.length - 1 && typeof field.relationTo === 'string') {
336
- if (field.hasMany && (virtualField.type === 'text' || virtualField.type === 'number' || virtualField.type === 'select')) {
337
- if (isHasMany) {
338
- throw new InvalidConfiguration(`Virtual field ${virtualField.name} in ${globalConfig ? `global ${globalConfig.slug}` : `collection ${collectionConfig?.slug}`} references 2 or more hasMany relationships on the path ${virtualField.virtual} which is not allowed.`);
339
- }
340
- isHasMany = true;
341
- virtualField.hasMany = true;
342
- }
343
- const relatedCollection = config.collections?.find((e)=>e.slug === field.relationTo);
344
- if (relatedCollection) {
345
- flattenFields = flattenAllFields({
346
- fields: relatedCollection.fields
347
- });
297
+ }
298
+ if (field.type === 'ui' && typeof field.admin.disableBulkEdit === 'undefined') {
299
+ field.admin.disableBulkEdit = true;
300
+ }
301
+ // Timezone field insertion
302
+ if (field.type === 'date' && field.timezone) {
303
+ const name = field.name + '_tz';
304
+ let defaultTimezone = field.timezone && typeof field.timezone === 'object' ? field.timezone.defaultTimezone : config.admin?.timezones?.defaultTimezone;
305
+ const required = field.required || field.timezone && typeof field.timezone === 'object' && field.timezone.required;
306
+ const supportedTimezones = field.timezone && typeof field.timezone === 'object' && field.timezone.supportedTimezones ? field.timezone.supportedTimezones : config.admin?.timezones?.supportedTimezones;
307
+ const options = typeof supportedTimezones === 'function' ? supportedTimezones({
308
+ defaultTimezones
309
+ }) : supportedTimezones;
310
+ validateTimezones({
311
+ source: `field "${field.name}" timezone.supportedTimezones`,
312
+ timezones: options
313
+ });
314
+ if (options && options.length === 1 && options[0]?.value) {
315
+ defaultTimezone = options[0].value;
316
+ }
317
+ // Generate label for timezone field
318
+ const timezoneLabel = typeof field.label === 'string' ? `${field.label} Tz` : toWords(name);
319
+ const baseField = baseTimezoneField({
320
+ name,
321
+ defaultValue: defaultTimezone,
322
+ label: timezoneLabel,
323
+ options,
324
+ required
325
+ });
326
+ // Apply override if provided
327
+ const timezoneField = typeof field.timezone === 'object' && typeof field.timezone.override === 'function' ? field.timezone.override({
328
+ baseField
329
+ }) : baseField;
330
+ result.fieldsToInsert = [
331
+ timezoneField
332
+ ];
333
+ }
334
+ // Virtual field handling
335
+ if ('virtual' in field && typeof field.virtual === 'string') {
336
+ const virtualField = field;
337
+ const configFields = (collectionConfig || globalConfig)?.fields;
338
+ if (configFields) {
339
+ let flattenFields = flattenAllFields({
340
+ fields: configFields
341
+ });
342
+ const paths = field.virtual.split('.');
343
+ let isHasMany = false;
344
+ for (const [idx, segment] of paths.entries()){
345
+ const foundField = flattenFields.find((e)=>e.name === segment);
346
+ if (!foundField) {
347
+ break;
348
+ }
349
+ if (foundField.type === 'group' || foundField.type === 'tab' || foundField.type === 'array') {
350
+ flattenFields = foundField.flattenedFields;
351
+ } else if ((foundField.type === 'relationship' || foundField.type === 'upload') && idx !== paths.length - 1 && typeof foundField.relationTo === 'string') {
352
+ if (foundField.hasMany && (virtualField.type === 'text' || virtualField.type === 'number' || virtualField.type === 'select')) {
353
+ if (isHasMany) {
354
+ throw new InvalidConfiguration(`Virtual field ${virtualField.name} in ${globalConfig ? `global ${globalConfig.slug}` : `collection ${collectionConfig?.slug}`} references 2 or more hasMany relationships on the path ${virtualField.virtual} which is not allowed.`);
348
355
  }
356
+ isHasMany = true;
357
+ virtualField.hasMany = true;
358
+ }
359
+ const relatedCollection = config.collections?.find((e)=>e.slug === foundField.relationTo);
360
+ if (relatedCollection) {
361
+ flattenFields = flattenAllFields({
362
+ fields: relatedCollection.fields
363
+ });
349
364
  }
350
365
  }
351
366
  }
352
367
  }
353
368
  }
369
+ return result;
370
+ };
371
+ export const sanitizeFields = async ({ collectionConfig, config, existingFieldNames = new Set(), fields, globalConfig, isTopLevelField = true, joinPath = '', joins, orderableJoins, parentIndexPath = '', parentIsLocalized, parentSchemaPath = '', polymorphicJoins, requireFieldLevelRichTextEditor = false, richTextSanitizationPromises, validRelationships })=>{
372
+ if (!fields) {
373
+ return [];
374
+ }
375
+ for(let i = 0; i < fields.length; i++){
376
+ const field = fields[i];
377
+ const result = await sanitizeField({
378
+ collectionConfig,
379
+ config,
380
+ existingFieldNames,
381
+ field,
382
+ globalConfig,
383
+ index: i,
384
+ isTopLevelField,
385
+ joinPath,
386
+ joins,
387
+ orderableJoins,
388
+ parentIndexPath,
389
+ parentIsLocalized,
390
+ parentSchemaPath,
391
+ polymorphicJoins,
392
+ requireFieldLevelRichTextEditor,
393
+ richTextSanitizationPromises,
394
+ validRelationships
395
+ });
396
+ fields[i] = field;
397
+ if (result.fieldsToInsert?.length) {
398
+ fields.splice(i + 1, 0, ...result.fieldsToInsert);
399
+ i += result.fieldsToInsert.length;
400
+ }
401
+ }
354
402
  return fields;
355
403
  };
356
404