@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.
Files changed (34) hide show
  1. package/dist/db-content-api/generated/content-api-types.d.ts +644 -3
  2. package/dist/db-content-api/index.d.ts +3 -0
  3. package/dist/db-content-api/index.js +68 -123
  4. package/dist/db-content-api/utilities/clientVersionHeaders.d.ts +7 -0
  5. package/dist/db-content-api/utilities/clientVersionHeaders.js +15 -0
  6. package/dist/db-content-api/utilities/data/atomicOperations.d.ts +16 -0
  7. package/dist/db-content-api/utilities/data/atomicOperations.js +48 -0
  8. package/dist/db-content-api/utilities/data/convertRelationshipIds.d.ts +25 -0
  9. package/dist/db-content-api/utilities/data/convertRelationshipIds.js +109 -0
  10. package/dist/db-content-api/utilities/data/index.js +71 -55
  11. package/dist/db-content-api/utilities/data/isLocaleMap.d.ts +10 -0
  12. package/dist/db-content-api/utilities/data/isLocaleMap.js +13 -0
  13. package/dist/db-content-api/utilities/data/resolveFieldLocalization.d.ts +18 -0
  14. package/dist/db-content-api/utilities/data/resolveFieldLocalization.js +21 -0
  15. package/dist/db-content-api/utilities/data/validateRelationships.js +19 -12
  16. package/dist/db-content-api/utilities/joins.d.ts +6 -8
  17. package/dist/db-content-api/utilities/joins.js +8 -21
  18. package/dist/db-content-api/utilities/schema/buildDocumentSchema.d.ts +21 -4
  19. package/dist/db-content-api/utilities/schema/buildDocumentSchema.js +118 -36
  20. package/dist/db-content-api/utilities/where.d.ts +2 -12
  21. package/dist/db-content-api/utilities/where.js +1 -12
  22. package/dist/plugin/build-config.js +5 -1
  23. package/dist/plugin/sandbox-upload-fetch.d.ts +30 -0
  24. package/dist/plugin/sandbox-upload-fetch.js +59 -0
  25. package/dist/utils/load-payload-config.d.ts +2 -2
  26. package/package.json +6 -5
  27. package/dist/db-content-api/utilities/meta/buildLocalizedPaths.d.ts +0 -10
  28. package/dist/db-content-api/utilities/meta/buildLocalizedPaths.js +0 -71
  29. package/dist/db-content-api/utilities/meta/buildMeta.d.ts +0 -41
  30. package/dist/db-content-api/utilities/meta/buildMeta.js +0 -39
  31. package/dist/db-content-api/utilities/meta/buildPathTypes.d.ts +0 -16
  32. package/dist/db-content-api/utilities/meta/buildPathTypes.js +0 -243
  33. package/dist/db-content-api/utilities/meta/buildUniquePaths.d.ts +0 -12
  34. 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 field) || !field.name) {
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[field.name];
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[field.name] = [];
69
+ current[fieldName] = [];
72
70
  return;
73
71
  }
74
72
  }
75
73
  if (value !== null && value !== undefined) {
76
- value = castFieldValue(field, value);
77
- current[field.name] = value;
78
- // Normalize non-localized hasMany relationship/upload: single value array.
79
- // Payload may not wrap in array before calling db.update (e.g. children: 'uuid').
80
- const isNonLocalizedHasMany = !('localized' in field && field.localized) && (field.type === 'relationship' || field.type === 'upload') && 'hasMany' in field && field.hasMany;
81
- if (isNonLocalizedHasMany && !Array.isArray(value)) {
82
- current[field.name] = [
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
- // RichText: object -> JSON string
87
- if (field.type === 'richText' && typeof value !== 'string') {
88
- current[field.name] = JSON.stringify(value);
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 field) || !field.name) {
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[field.name];
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[field.name] = {};
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[field.name] = {
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[field.name] = isLocalized && isAllLocales ? {} : null;
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[field.name] = {};
223
+ current[fieldName] = {};
212
224
  return;
213
225
  }
214
226
  if (value !== null) {
215
- // hasMany relationships: normalize to array (non-localized only).
216
- // We store raw JSON so a single value stays as-is; Payload expects arrays.
217
- if (field.type === 'relationship' && field.hasMany && !Array.isArray(value) && !('localized' in field && field.localized)) {
218
- current[field.name] = [
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 ('localized' in field && field.localized && typeof value === 'string') {
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[field.name] = parsed;
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 field) || !field.name) {
8
+ const callback = ({ field: declaredField, parentIsLocalized, ref })=>{
9
+ if (!('name' in declaredField) || !declaredField.name) {
7
10
  return;
8
11
  }
9
- if (field.type !== 'relationship' && field.type !== 'upload') {
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(value, relationTo, relationships) {
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 && typeof v === 'object' && 'relationTo' in v && 'value' in v) {
49
- const obj = v;
50
- if (obj.value != null && obj.value !== '') {
51
- relationships.push({
52
- collection: obj.relationTo,
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']['JoinClause'][number];
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
- * The mapping requires the collection config to resolve:
48
- * - Which collection each join field refers to
49
- * - What the 'on' field is for each join
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, locale?: string): ContentAPIJoin[] | 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
- * The mapping requires the collection config to resolve:
47
- * - Which collection each join field refers to
48
- * - What the 'on' field is for each join
49
- */ export function convertPayloadJoinsToContentAPI(payload, collectionSlug, joins, locale) {
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: collections,
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' | 'blocks';
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 {};