@payloadcms/figma 0.1.0-alpha.0 → 0.1.0-internal.e071575
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.
- package/dist/db-content-api/generated/content-api-types.d.ts +644 -3
- package/dist/db-content-api/index.d.ts +3 -0
- package/dist/db-content-api/index.js +68 -123
- package/dist/db-content-api/utilities/clientVersionHeaders.d.ts +7 -0
- package/dist/db-content-api/utilities/clientVersionHeaders.js +15 -0
- package/dist/db-content-api/utilities/data/atomicOperations.d.ts +16 -0
- package/dist/db-content-api/utilities/data/atomicOperations.js +48 -0
- package/dist/db-content-api/utilities/data/convertRelationshipIds.d.ts +25 -0
- package/dist/db-content-api/utilities/data/convertRelationshipIds.js +109 -0
- package/dist/db-content-api/utilities/data/index.js +71 -55
- package/dist/db-content-api/utilities/data/isLocaleMap.d.ts +10 -0
- package/dist/db-content-api/utilities/data/isLocaleMap.js +13 -0
- package/dist/db-content-api/utilities/data/resolveFieldLocalization.d.ts +18 -0
- package/dist/db-content-api/utilities/data/resolveFieldLocalization.js +21 -0
- package/dist/db-content-api/utilities/data/validateRelationships.js +19 -12
- package/dist/db-content-api/utilities/joins.d.ts +6 -8
- package/dist/db-content-api/utilities/joins.js +8 -21
- package/dist/db-content-api/utilities/schema/buildDocumentSchema.d.ts +21 -4
- package/dist/db-content-api/utilities/schema/buildDocumentSchema.js +118 -36
- package/dist/db-content-api/utilities/where.d.ts +2 -12
- package/dist/db-content-api/utilities/where.js +1 -12
- package/dist/plugin/build-config.js +5 -1
- package/dist/plugin/sandbox-upload-fetch.d.ts +30 -0
- package/dist/plugin/sandbox-upload-fetch.js +59 -0
- package/dist/utils/load-payload-config.d.ts +2 -2
- package/package.json +6 -5
- package/dist/db-content-api/utilities/meta/buildLocalizedPaths.d.ts +0 -10
- package/dist/db-content-api/utilities/meta/buildLocalizedPaths.js +0 -71
- package/dist/db-content-api/utilities/meta/buildMeta.d.ts +0 -41
- package/dist/db-content-api/utilities/meta/buildMeta.js +0 -39
- package/dist/db-content-api/utilities/meta/buildPathTypes.d.ts +0 -16
- package/dist/db-content-api/utilities/meta/buildPathTypes.js +0 -243
- package/dist/db-content-api/utilities/meta/buildUniquePaths.d.ts +0 -12
- package/dist/db-content-api/utilities/meta/buildUniquePaths.js +0 -60
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { valueIsValueWithRelation } from 'payload/shared';
|
|
2
|
+
import { matchAtomicOperation, RELATIONSHIP_ATOMIC_OPERATORS } from './atomicOperations.js';
|
|
3
|
+
import { isLocaleMap } from './isLocaleMap.js';
|
|
4
|
+
export function convertRelationshipFieldIdsFromWire(options) {
|
|
5
|
+
return convertRelationshipFieldIds({
|
|
6
|
+
...options,
|
|
7
|
+
direction: 'fromWire'
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function convertRelationshipFieldIdsToWire(options) {
|
|
11
|
+
return convertRelationshipFieldIds({
|
|
12
|
+
...options,
|
|
13
|
+
direction: 'toWire'
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Converts the id(s) inside a relationship/upload field value between Payload's
|
|
18
|
+
* representation (numeric ids kept as numbers) and the Content API wire form
|
|
19
|
+
* (all ids as strings). No-op for uuid/text targets. Preserves has-many arrays,
|
|
20
|
+
* polymorphic shapes, localized locale-maps, and atomic operators.
|
|
21
|
+
*/ export function convertRelationshipFieldIds(options) {
|
|
22
|
+
const { direction, field, payload, value } = options;
|
|
23
|
+
if (value == null) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
const relationTo = field.relationTo;
|
|
27
|
+
const localized = field.localized ?? false;
|
|
28
|
+
const hasMany = field.hasMany ?? false;
|
|
29
|
+
// The value shape is fully determined by the localized × hasMany combination:
|
|
30
|
+
// !localized, !hasMany → a single value
|
|
31
|
+
// !localized, hasMany → an array of values (or a `$push`/`$remove` wrapper)
|
|
32
|
+
// localized, !hasMany → a locale-map of single values
|
|
33
|
+
// localized, hasMany → a locale-map of arrays
|
|
34
|
+
// Localized values arrive as a locale-map on write and all-locales reads, but
|
|
35
|
+
// as the bare per-locale value on single-locale reads, so a localized field
|
|
36
|
+
// dispatches through `convertLocalized`, which handles both.
|
|
37
|
+
const convertValue = (entry)=>hasMany ? convertHasManyValue(entry, relationTo, payload, direction) : convertSingleValue(entry, relationTo, payload, direction);
|
|
38
|
+
return localized ? convertLocalized(value, convertValue) : convertValue(value);
|
|
39
|
+
}
|
|
40
|
+
// Applies `convertValue` to each entry of a localized locale-map. A single-locale
|
|
41
|
+
// read delivers the bare per-locale value instead of a map (a plain object then
|
|
42
|
+
// is a polymorphic ref, not a locale-map), so that case is converted directly.
|
|
43
|
+
function convertLocalized(value, convertValue) {
|
|
44
|
+
if (!isLocaleMap(value)) {
|
|
45
|
+
return convertValue(value);
|
|
46
|
+
}
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [locale, localeValue] of Object.entries(value)){
|
|
49
|
+
out[locale] = localeValue == null ? localeValue : convertValue(localeValue);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
// A has-many value normalizes to an array of single values: Payload may pass a
|
|
54
|
+
// lone value (e.g. `children: 'uuid'`), which is wrapped into a single-element
|
|
55
|
+
// array. Writes may instead wrap the value in an atomic `$push`/`$remove`
|
|
56
|
+
// operator, which is preserved — its payload is converted but not array-wrapped,
|
|
57
|
+
// since those operators accept a single value or an array.
|
|
58
|
+
function convertHasManyValue(value, relationTo, payload, direction) {
|
|
59
|
+
const atomic = matchAtomicOperation(value, RELATIONSHIP_ATOMIC_OPERATORS);
|
|
60
|
+
if (atomic) {
|
|
61
|
+
return {
|
|
62
|
+
[atomic.operator]: convertAtomicPayload(atomic.inner, relationTo, payload, direction)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.map((entry)=>convertSingleValue(entry, relationTo, payload, direction));
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
convertSingleValue(value, relationTo, payload, direction)
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
// The payload of a `$push`/`$remove` operator may be a single value or an array;
|
|
73
|
+
// convert either without changing its arity.
|
|
74
|
+
function convertAtomicPayload(value, relationTo, payload, direction) {
|
|
75
|
+
return Array.isArray(value) ? value.map((entry)=>convertSingleValue(entry, relationTo, payload, direction)) : convertSingleValue(value, relationTo, payload, direction);
|
|
76
|
+
}
|
|
77
|
+
// A single relationship value is a polymorphic `{ relationTo, value }` (when the
|
|
78
|
+
// field targets multiple collections) or a bare id (when it targets one). The id
|
|
79
|
+
// type comes from the referenced collection.
|
|
80
|
+
function convertSingleValue(value, relationTo, payload, direction) {
|
|
81
|
+
if (value == null) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (valueIsValueWithRelation(value)) {
|
|
85
|
+
return {
|
|
86
|
+
...value,
|
|
87
|
+
value: convertLeafId(value.value, customIdTypeFor(payload, value.relationTo), direction)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// A non-polymorphic field targets a single collection (string `relationTo`).
|
|
91
|
+
const customIDType = typeof relationTo === 'string' ? customIdTypeFor(payload, relationTo) : undefined;
|
|
92
|
+
return convertLeafId(value, customIDType, direction);
|
|
93
|
+
}
|
|
94
|
+
function customIdTypeFor(payload, slug) {
|
|
95
|
+
return payload.collections?.[slug]?.customIDType;
|
|
96
|
+
}
|
|
97
|
+
function convertLeafId(id, customIDType, direction) {
|
|
98
|
+
if (id == null || id === '') {
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
101
|
+
// Only numeric custom ids differ between Payload (number) and the wire (string).
|
|
102
|
+
if (customIDType !== 'number') {
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
if (direction === 'toWire') {
|
|
106
|
+
return typeof id === 'number' ? String(id) : id;
|
|
107
|
+
}
|
|
108
|
+
return typeof id === 'string' ? Number(id) : id;
|
|
109
|
+
}
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { flattenAllFields, traverseFields } from 'payload';
|
|
2
2
|
import { applyDefaults } from './applyDefaults.js';
|
|
3
|
+
import { ATOMIC_OPERATORS, isAtomicOperation } from './atomicOperations.js';
|
|
3
4
|
import { castFieldValue } from './castFieldValue.js';
|
|
5
|
+
import { convertRelationshipFieldIds } from './convertRelationshipIds.js';
|
|
6
|
+
import { decodePointFromRead, encodePointForWrite } from './pointShape.js';
|
|
4
7
|
import { removeVirtualFields } from './removeVirtualFields.js';
|
|
8
|
+
import { resolveFieldLocalization } from './resolveFieldLocalization.js';
|
|
9
|
+
import { decodeRichTextFromRead, encodeRichTextForWrite } from './richTextShape.js';
|
|
5
10
|
import { stripFields } from './stripFields.js';
|
|
6
11
|
import { transformToLocalizeStatus } from './transformPublishedLocale.js';
|
|
7
|
-
const ATOMIC_OPERATION_KEYS = [
|
|
8
|
-
'$push',
|
|
9
|
-
'$remove',
|
|
10
|
-
'$inc'
|
|
11
|
-
];
|
|
12
|
-
function isAtomicOperation(value) {
|
|
13
|
-
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
const keys = Object.keys(value);
|
|
17
|
-
return keys.length === 1 && ATOMIC_OPERATION_KEYS.includes(keys[0]);
|
|
18
|
-
}
|
|
19
12
|
/**
|
|
20
13
|
* Transform data before sending to Content API (WRITE operations)
|
|
21
14
|
*
|
|
@@ -49,16 +42,21 @@ function isAtomicOperation(value) {
|
|
|
49
42
|
applyDefaults(transformed, collectionConfig.fields, payload.config.blocks);
|
|
50
43
|
}
|
|
51
44
|
// Use Payload's traverseFields to iterate over all fields for transformations
|
|
52
|
-
const callback = ({ field, ref })=>{
|
|
53
|
-
if (!('name' in
|
|
45
|
+
const callback = ({ field: declaredField, parentIsLocalized, ref })=>{
|
|
46
|
+
if (!('name' in declaredField) || declaredField.name == null) {
|
|
54
47
|
return;
|
|
55
48
|
}
|
|
56
49
|
if (!ref || typeof ref !== 'object') {
|
|
57
50
|
return;
|
|
58
51
|
}
|
|
52
|
+
const fieldName = declaredField.name;
|
|
53
|
+
const field = resolveFieldLocalization({
|
|
54
|
+
field: declaredField,
|
|
55
|
+
isAbsorbed: parentIsLocalized
|
|
56
|
+
});
|
|
59
57
|
const current = ref;
|
|
60
|
-
let value = current[
|
|
61
|
-
if (isAtomicOperation(value)) {
|
|
58
|
+
let value = current[fieldName];
|
|
59
|
+
if (isAtomicOperation(value, ATOMIC_OPERATORS) && field.type !== 'relationship' && field.type !== 'upload') {
|
|
62
60
|
return;
|
|
63
61
|
}
|
|
64
62
|
// null → [] for non-localized array-like fields. SQL adapters return null for empty join
|
|
@@ -68,25 +66,33 @@ function isAtomicOperation(value) {
|
|
|
68
66
|
const isArrayLike = field.type === 'array' || field.type === 'blocks';
|
|
69
67
|
const isHasManySelect = field.type === 'select' && 'hasMany' in field && field.hasMany;
|
|
70
68
|
if (isHasManyArray || isArrayLike || isHasManySelect) {
|
|
71
|
-
current[
|
|
69
|
+
current[fieldName] = [];
|
|
72
70
|
return;
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
if (value !== null && value !== undefined) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
74
|
+
if (field.type === 'relationship' || field.type === 'upload') {
|
|
75
|
+
// Convert ids that point at a numeric-custom-id collection to strings —
|
|
76
|
+
// the wire (and the server's relationshipSchema) only accepts string ids —
|
|
77
|
+
// and normalize a has-many value to an array.
|
|
78
|
+
current[fieldName] = convertRelationshipFieldIds({
|
|
79
|
+
direction: 'toWire',
|
|
80
|
+
field,
|
|
81
|
+
payload,
|
|
83
82
|
value
|
|
84
|
-
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
if (field.type === 'point') {
|
|
87
|
+
current[fieldName] = encodePointForWrite(field, value);
|
|
88
|
+
return;
|
|
89
89
|
}
|
|
90
|
+
if (field.type === 'richText') {
|
|
91
|
+
current[fieldName] = encodeRichTextForWrite(field, value);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
value = castFieldValue(field, value);
|
|
95
|
+
current[fieldName] = value;
|
|
90
96
|
}
|
|
91
97
|
};
|
|
92
98
|
traverseFields({
|
|
@@ -103,8 +109,7 @@ function isAtomicOperation(value) {
|
|
|
103
109
|
fields: collectionConfig.fields
|
|
104
110
|
}),
|
|
105
111
|
reservedKeys: [
|
|
106
|
-
'id'
|
|
107
|
-
'globalType'
|
|
112
|
+
'id'
|
|
108
113
|
]
|
|
109
114
|
});
|
|
110
115
|
removeVirtualFields(transformed, collectionConfig.fields);
|
|
@@ -169,21 +174,28 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
|
169
174
|
}
|
|
170
175
|
const isAllLocales = locale === 'all' || locale === '*';
|
|
171
176
|
// Use Payload's traverseFields to iterate over all fields
|
|
172
|
-
const callback = ({ field, parentPath, ref })=>{
|
|
173
|
-
if (!('name' in
|
|
177
|
+
const callback = ({ field: declaredField, parentIsLocalized, parentPath, ref })=>{
|
|
178
|
+
if (!('name' in declaredField) || !declaredField.name) {
|
|
174
179
|
return;
|
|
175
180
|
}
|
|
176
181
|
if (!ref || typeof ref !== 'object') {
|
|
177
182
|
return;
|
|
178
183
|
}
|
|
184
|
+
// Value shaping below depends on whether locale keys appear at this path, so
|
|
185
|
+
// work from the resolved field rather than the declared one.
|
|
186
|
+
const fieldName = declaredField.name;
|
|
187
|
+
const field = resolveFieldLocalization({
|
|
188
|
+
field: declaredField,
|
|
189
|
+
isAbsorbed: parentIsLocalized
|
|
190
|
+
});
|
|
179
191
|
const current = ref;
|
|
180
|
-
const value = current[
|
|
181
|
-
const isLocalized = 'localized' in field && field.localized;
|
|
192
|
+
const value = current[fieldName];
|
|
193
|
+
const isLocalized = 'localized' in field && field.localized === true;
|
|
182
194
|
// Group fields must always be an object so Payload can traverse sub-fields
|
|
183
195
|
// (e.g. join fields inside a group). A null/undefined group crashes afterRead.
|
|
184
196
|
if (field.type === 'group') {
|
|
185
197
|
if (value == null || typeof value !== 'object') {
|
|
186
|
-
current[
|
|
198
|
+
current[fieldName] = {};
|
|
187
199
|
}
|
|
188
200
|
return;
|
|
189
201
|
}
|
|
@@ -191,7 +203,7 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
|
191
203
|
// Content API returns join data directly at the field path (e.g. { docs: [...] }),
|
|
192
204
|
// but Payload's afterRead expects localized fields as { [locale]: value }.
|
|
193
205
|
if (field.type === 'join' && isLocalized && value !== undefined && !isAllLocales && locale) {
|
|
194
|
-
current[
|
|
206
|
+
current[fieldName] = {
|
|
195
207
|
[locale]: value
|
|
196
208
|
};
|
|
197
209
|
return;
|
|
@@ -202,22 +214,36 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
|
202
214
|
const isRequired = 'required' in field && field.required;
|
|
203
215
|
const isJoin = field.type === 'join';
|
|
204
216
|
if (!isRequired && !isJoin) {
|
|
205
|
-
current[
|
|
217
|
+
current[fieldName] = isLocalized && isAllLocales ? {} : null;
|
|
206
218
|
}
|
|
207
219
|
return;
|
|
208
220
|
}
|
|
209
221
|
// null localized field in all-locales mode → {} (empty locale map)
|
|
210
222
|
if (value === null && isLocalized && isAllLocales) {
|
|
211
|
-
current[
|
|
223
|
+
current[fieldName] = {};
|
|
212
224
|
return;
|
|
213
225
|
}
|
|
214
226
|
if (value !== null) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
227
|
+
if (field.type === 'relationship' || field.type === 'upload') {
|
|
228
|
+
// The Content API returns ids as strings; convert back to numbers for
|
|
229
|
+
// relationships that point at a numeric-custom-id collection so Payload
|
|
230
|
+
// sees its native type, and normalize a has-many value to an array.
|
|
231
|
+
current[fieldName] = convertRelationshipFieldIds({
|
|
232
|
+
direction: 'fromWire',
|
|
233
|
+
field,
|
|
234
|
+
payload,
|
|
219
235
|
value
|
|
220
|
-
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (field.type === 'point') {
|
|
240
|
+
current[fieldName] = decodePointFromRead(field, value);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (field.type === 'richText') {
|
|
244
|
+
const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
|
|
245
|
+
current[fieldName] = decodeRichTextFromRead(field, value, payload, fieldPath, collectionSlug);
|
|
246
|
+
return;
|
|
221
247
|
}
|
|
222
248
|
// Localized array/blocks: empty per-locale arrays → null in single-locale mode.
|
|
223
249
|
// Payload's afterRead fallback triggers on null (not []), matching SQL adapter behavior.
|
|
@@ -232,25 +258,15 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
|
|
|
232
258
|
}
|
|
233
259
|
// Localized fields: JSON string → parsed object (locale map).
|
|
234
260
|
// Simple strings that fail to parse as objects are kept as-is (expected for single-locale).
|
|
235
|
-
if (
|
|
261
|
+
if (isLocalized && typeof value === 'string') {
|
|
236
262
|
try {
|
|
237
263
|
const parsed = JSON.parse(value);
|
|
238
264
|
if (typeof parsed === 'object' && parsed !== null) {
|
|
239
|
-
current[
|
|
265
|
+
current[fieldName] = parsed;
|
|
240
266
|
}
|
|
241
267
|
} catch {
|
|
242
268
|
// Not valid JSON - keep original value
|
|
243
269
|
}
|
|
244
|
-
} else if (field.type === 'richText' && typeof value === 'string') {
|
|
245
|
-
try {
|
|
246
|
-
current[field.name] = JSON.parse(value);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
|
|
249
|
-
payload.logger.warn({
|
|
250
|
-
err: error instanceof Error ? error : new Error(String(error)),
|
|
251
|
-
msg: `Failed to parse richtext field '${fieldPath}' in collection '${collectionSlug}'`
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
270
|
}
|
|
255
271
|
// Date fields: Already ISO strings from Content API, no conversion needed
|
|
256
272
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A locale-map is a plain object keyed by locale (e.g. `{ en: 'id', fr: 'id' }`) —
|
|
3
|
+
* distinct from the other object shapes a field value can take: a polymorphic
|
|
4
|
+
* relationship ref (`{ relationTo, value }`) or an atomic wrapper (`{ $push: … }`).
|
|
5
|
+
*
|
|
6
|
+
* Localized values arrive as a locale-map on write and all-locales reads, but as
|
|
7
|
+
* the bare per-locale value on single-locale reads, so a localized field cannot
|
|
8
|
+
* assume the map and has to discriminate on the shape.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isLocaleMap(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { valueIsValueWithRelation } from 'payload/shared';
|
|
2
|
+
import { ATOMIC_OPERATORS, isAtomicOperation } from './atomicOperations.js';
|
|
3
|
+
/**
|
|
4
|
+
* A locale-map is a plain object keyed by locale (e.g. `{ en: 'id', fr: 'id' }`) —
|
|
5
|
+
* distinct from the other object shapes a field value can take: a polymorphic
|
|
6
|
+
* relationship ref (`{ relationTo, value }`) or an atomic wrapper (`{ $push: … }`).
|
|
7
|
+
*
|
|
8
|
+
* Localized values arrive as a locale-map on write and all-locales reads, but as
|
|
9
|
+
* the bare per-locale value on single-locale reads, so a localized field cannot
|
|
10
|
+
* assume the map and has to discriminate on the shape.
|
|
11
|
+
*/ export function isLocaleMap(value) {
|
|
12
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && !valueIsValueWithRelation(value) && !isAtomicOperation(value, ATOMIC_OPERATORS);
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Field, TabAsField } from 'payload';
|
|
2
|
+
interface ResolveFieldLocalizationOptions<TField extends Field | TabAsField> {
|
|
3
|
+
field: TField;
|
|
4
|
+
isAbsorbed: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns the field with `localized` collapsed to whether locale keys actually
|
|
8
|
+
* appear in its data — `false` for a field whose localized ancestor already
|
|
9
|
+
* absorbed them (see `createIsLocaleAbsorbed`).
|
|
10
|
+
*
|
|
11
|
+
* Every value-shaping helper keys off `field.localized` to decide whether it is
|
|
12
|
+
* looking at a locale map or a bare value, so handing them the resolved field
|
|
13
|
+
* keeps that decision correct without each one needing to know about absorption.
|
|
14
|
+
* The field is returned as-is when nothing changes, so the common path allocates
|
|
15
|
+
* nothing and any narrowing the caller has already done survives.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveFieldLocalization<TField extends Field | TabAsField>({ field, isAbsorbed, }: ResolveFieldLocalizationOptions<TField>): TField;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the field with `localized` collapsed to whether locale keys actually
|
|
3
|
+
* appear in its data — `false` for a field whose localized ancestor already
|
|
4
|
+
* absorbed them (see `createIsLocaleAbsorbed`).
|
|
5
|
+
*
|
|
6
|
+
* Every value-shaping helper keys off `field.localized` to decide whether it is
|
|
7
|
+
* looking at a locale map or a bare value, so handing them the resolved field
|
|
8
|
+
* keeps that decision correct without each one needing to know about absorption.
|
|
9
|
+
* The field is returned as-is when nothing changes, so the common path allocates
|
|
10
|
+
* nothing and any narrowing the caller has already done survives.
|
|
11
|
+
*/ export function resolveFieldLocalization({ field, isAbsorbed }) {
|
|
12
|
+
if (!isAbsorbed || !('localized' in field) || field.localized !== true) {
|
|
13
|
+
return field;
|
|
14
|
+
}
|
|
15
|
+
// Only `localized` changes, so the result is still the same field type; TypeScript
|
|
16
|
+
// cannot see that through a spread of a generic union member.
|
|
17
|
+
return {
|
|
18
|
+
...field,
|
|
19
|
+
localized: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { traverseFields } from 'payload';
|
|
2
|
+
import { valueIsValueWithRelation } from 'payload/shared';
|
|
3
|
+
import { RELATIONSHIP_ATOMIC_OPERATORS, unwrapAtomicOperation } from './atomicOperations.js';
|
|
4
|
+
import { resolveFieldLocalization } from './resolveFieldLocalization.js';
|
|
2
5
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
6
|
function collectRelationshipValues({ data, fields }) {
|
|
4
7
|
const relationships = [];
|
|
5
|
-
const callback = ({ field, ref })=>{
|
|
6
|
-
if (!('name' in
|
|
8
|
+
const callback = ({ field: declaredField, parentIsLocalized, ref })=>{
|
|
9
|
+
if (!('name' in declaredField) || !declaredField.name) {
|
|
7
10
|
return;
|
|
8
11
|
}
|
|
9
|
-
if (
|
|
12
|
+
if (declaredField.type !== 'relationship' && declaredField.type !== 'upload') {
|
|
10
13
|
return;
|
|
11
14
|
}
|
|
12
15
|
if (!ref || typeof ref !== 'object') {
|
|
13
16
|
return;
|
|
14
17
|
}
|
|
18
|
+
// Only a field whose locale keys were not absorbed by a localized ancestor
|
|
19
|
+
// holds a locale map; an absorbed one holds the relationship value directly.
|
|
20
|
+
const field = resolveFieldLocalization({
|
|
21
|
+
field: declaredField,
|
|
22
|
+
isAbsorbed: parentIsLocalized
|
|
23
|
+
});
|
|
15
24
|
const rawValue = ref[field.name];
|
|
16
25
|
if (rawValue == null || rawValue === '') {
|
|
17
26
|
return;
|
|
@@ -38,21 +47,19 @@ function collectRelationshipValues({ data, fields }) {
|
|
|
38
47
|
});
|
|
39
48
|
return relationships;
|
|
40
49
|
}
|
|
41
|
-
function parseRelationshipValues(
|
|
50
|
+
function parseRelationshipValues(rawValue, relationTo, relationships) {
|
|
51
|
+
const value = unwrapAtomicOperation(rawValue, RELATIONSHIP_ATOMIC_OPERATORS);
|
|
42
52
|
if (Array.isArray(relationTo)) {
|
|
43
53
|
// Polymorphic (single or hasMany): value is { relationTo, value } or array of them
|
|
44
54
|
const values = Array.isArray(value) ? value : [
|
|
45
55
|
value
|
|
46
56
|
];
|
|
47
57
|
for (const v of values){
|
|
48
|
-
if (v &&
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
value: obj.value
|
|
54
|
-
});
|
|
55
|
-
}
|
|
58
|
+
if (valueIsValueWithRelation(v) && v.value != null && v.value !== '') {
|
|
59
|
+
relationships.push({
|
|
60
|
+
collection: v.relationTo,
|
|
61
|
+
value: v.value
|
|
62
|
+
});
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
} else {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JoinQuery, Payload } from 'payload';
|
|
2
2
|
import type { components } from '../generated/content-api-types.js';
|
|
3
|
-
type ContentAPIJoin = components['schemas']['
|
|
3
|
+
type ContentAPIJoin = components['schemas']['JoinClauseV1'][number];
|
|
4
4
|
/**
|
|
5
5
|
* Convert Payload's JoinQuery to Content API's join format.
|
|
6
6
|
*
|
|
@@ -32,10 +32,8 @@ type ContentAPIJoin = components['schemas']['JoinClause'][number];
|
|
|
32
32
|
* }
|
|
33
33
|
* }
|
|
34
34
|
*
|
|
35
|
-
* Content API's join format:
|
|
35
|
+
* Content API's v1 join format:
|
|
36
36
|
* [{
|
|
37
|
-
* collectionId: 'posts', // collection to join FROM
|
|
38
|
-
* on: 'group.category', // field in joined collection pointing to this doc
|
|
39
37
|
* path: 'group.relatedPosts', // where to put results
|
|
40
38
|
* count: false,
|
|
41
39
|
* where: { ... }, // Content API where format
|
|
@@ -44,9 +42,9 @@ type ContentAPIJoin = components['schemas']['JoinClause'][number];
|
|
|
44
42
|
* page: 2
|
|
45
43
|
* }]
|
|
46
44
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* v1 resolves the joined collection and key from the document schema, so the
|
|
46
|
+
* adapter no longer emits collectionId/on. The collection config is still read
|
|
47
|
+
* to resolve default limit and sort for each join path.
|
|
50
48
|
*/
|
|
51
|
-
export declare function convertPayloadJoinsToContentAPI(payload: Payload, collectionSlug: string, joins: false | JoinQuery | undefined
|
|
49
|
+
export declare function convertPayloadJoinsToContentAPI(payload: Payload, collectionSlug: string, joins: false | JoinQuery | undefined): ContentAPIJoin[] | undefined;
|
|
52
50
|
export {};
|
|
@@ -31,10 +31,8 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
31
31
|
* }
|
|
32
32
|
* }
|
|
33
33
|
*
|
|
34
|
-
* Content API's join format:
|
|
34
|
+
* Content API's v1 join format:
|
|
35
35
|
* [{
|
|
36
|
-
* collectionId: 'posts', // collection to join FROM
|
|
37
|
-
* on: 'group.category', // field in joined collection pointing to this doc
|
|
38
36
|
* path: 'group.relatedPosts', // where to put results
|
|
39
37
|
* count: false,
|
|
40
38
|
* where: { ... }, // Content API where format
|
|
@@ -43,10 +41,10 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
43
41
|
* page: 2
|
|
44
42
|
* }]
|
|
45
43
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*/ export function convertPayloadJoinsToContentAPI(payload, collectionSlug, joins
|
|
44
|
+
* v1 resolves the joined collection and key from the document schema, so the
|
|
45
|
+
* adapter no longer emits collectionId/on. The collection config is still read
|
|
46
|
+
* to resolve default limit and sort for each join path.
|
|
47
|
+
*/ export function convertPayloadJoinsToContentAPI(payload, collectionSlug, joins) {
|
|
50
48
|
const collectionConfig = payload.config.collections.find((c)=>c.slug === collectionSlug);
|
|
51
49
|
if (!collectionConfig) {
|
|
52
50
|
return undefined;
|
|
@@ -72,14 +70,10 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
72
70
|
for (const [collectionSlug, sanitizedJoins] of Object.entries(collectionConfig.joins)){
|
|
73
71
|
for (const sanitizedJoin of sanitizedJoins){
|
|
74
72
|
if (sanitizedJoin.joinPath === joinPath) {
|
|
75
|
-
const on = locale && sanitizedJoin.getForeignPath ? sanitizedJoin.getForeignPath({
|
|
76
|
-
locale
|
|
77
|
-
}) : sanitizedJoin.field.on;
|
|
78
73
|
foundJoin = {
|
|
79
74
|
collectionSlug,
|
|
80
75
|
defaultLimit: sanitizedJoin.field.defaultLimit,
|
|
81
|
-
defaultSort: sanitizedJoin.field.defaultSort
|
|
82
|
-
on
|
|
76
|
+
defaultSort: sanitizedJoin.field.defaultSort
|
|
83
77
|
};
|
|
84
78
|
break;
|
|
85
79
|
}
|
|
@@ -92,14 +86,9 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
92
86
|
if (!foundJoin) {
|
|
93
87
|
for (const sanitizedJoin of collectionConfig.polymorphicJoins){
|
|
94
88
|
if (sanitizedJoin.joinPath === joinPath) {
|
|
95
|
-
const collections = sanitizedJoin.field.collection;
|
|
96
|
-
const on = locale && sanitizedJoin.getForeignPath ? sanitizedJoin.getForeignPath({
|
|
97
|
-
locale
|
|
98
|
-
}) : sanitizedJoin.field.on;
|
|
99
89
|
foundJoin = {
|
|
100
|
-
collectionSlug:
|
|
101
|
-
defaultSort: sanitizedJoin.field.defaultSort
|
|
102
|
-
on
|
|
90
|
+
collectionSlug: sanitizedJoin.field.collection,
|
|
91
|
+
defaultSort: sanitizedJoin.field.defaultSort
|
|
103
92
|
};
|
|
104
93
|
break;
|
|
105
94
|
}
|
|
@@ -114,9 +103,7 @@ import { convertPayloadWhereToContentAPI } from './where.js';
|
|
|
114
103
|
// Add fallback sort for the joined collection
|
|
115
104
|
const sortWithFallback = addFallbackSort(joinQuery.sort, payload, foundJoin.collectionSlug, foundJoin.defaultSort);
|
|
116
105
|
const contentAPIJoin = {
|
|
117
|
-
collectionId: foundJoin.collectionSlug,
|
|
118
106
|
count: joinQuery.count ?? false,
|
|
119
|
-
on: foundJoin.on,
|
|
120
107
|
path: joinPath,
|
|
121
108
|
...effectiveLimit !== undefined && {
|
|
122
109
|
limit: effectiveLimit
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import type { Config } from 'payload';
|
|
1
|
+
import type { Config, SanitizedConfig } from 'payload';
|
|
2
2
|
/**
|
|
3
3
|
* Wire form of the v1 `DocumentSchema` sent on every request so the server can
|
|
4
4
|
* classify each queried path against the index tables. Paths are
|
|
5
5
|
* period-delimited (e.g. `author.name`).
|
|
6
|
+
*
|
|
7
|
+
* `localized` on a path is the flag *declared* by the Payload field, not a
|
|
8
|
+
* statement about where locale keys appear in the data. Payload nests locale keys
|
|
9
|
+
* at the outermost localized field on a path and stores everything beneath it
|
|
10
|
+
* plainly; the Content API resolves that absorption itself while walking a path,
|
|
11
|
+
* because a block entity is shared across the schema and can be referenced from
|
|
12
|
+
* both a localized and a non-localized `blocks` field. Read paths that need the
|
|
13
|
+
* resolved flag on this side use `isLocaleAbsorbed`.
|
|
6
14
|
*/
|
|
7
15
|
export interface DocumentSchemaWire {
|
|
16
|
+
blocks: Record<string, WireBlockSchema>;
|
|
8
17
|
collections: Record<string, WireCollectionSchema>;
|
|
9
18
|
version: 1;
|
|
10
19
|
}
|
|
@@ -13,11 +22,19 @@ interface WireCollectionSchema {
|
|
|
13
22
|
name: string;
|
|
14
23
|
paths: Record<string, WireFieldSchema>;
|
|
15
24
|
}
|
|
25
|
+
interface WireBlockSchema {
|
|
26
|
+
name: string;
|
|
27
|
+
paths: Record<string, WireFieldSchema>;
|
|
28
|
+
}
|
|
16
29
|
type WireHint = {
|
|
17
30
|
paths: string[];
|
|
18
31
|
type: 'unique';
|
|
19
32
|
};
|
|
20
33
|
type WireFieldSchema = {
|
|
34
|
+
blocks: Record<string, string>;
|
|
35
|
+
localized: boolean;
|
|
36
|
+
type: 'blocks';
|
|
37
|
+
} | {
|
|
21
38
|
collection: string | string[];
|
|
22
39
|
on: string;
|
|
23
40
|
type: 'join';
|
|
@@ -42,14 +59,14 @@ type WireFieldSchema = {
|
|
|
42
59
|
unique: boolean;
|
|
43
60
|
} | {
|
|
44
61
|
localized: boolean;
|
|
45
|
-
type: 'array'
|
|
62
|
+
type: 'array';
|
|
46
63
|
} | {
|
|
47
64
|
localized: boolean;
|
|
48
65
|
type: 'checkbox' | 'date' | 'json' | 'radio';
|
|
49
66
|
} | {
|
|
50
67
|
localized: boolean;
|
|
51
|
-
type: 'code' | 'email' | 'point' | 'textarea';
|
|
68
|
+
type: 'code' | 'email' | 'point' | 'slug' | 'textarea';
|
|
52
69
|
unique: boolean;
|
|
53
70
|
};
|
|
54
|
-
export declare function buildDocumentSchema(config: Config): DocumentSchemaWire;
|
|
71
|
+
export declare function buildDocumentSchema(config: Config | SanitizedConfig): DocumentSchemaWire;
|
|
55
72
|
export {};
|