@payloadcms/figma 0.0.1-alpha.59 → 0.0.1-alpha.60

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.
@@ -28,6 +28,10 @@ export interface CreateTenantOptions {
28
28
  * Options for creating a new deployment
29
29
  */
30
30
  export interface CreateDeploymentOptions {
31
+ /** Pages keyed by route, each with its associated asset keys */
32
+ pages: Record<string, {
33
+ assets: string[];
34
+ }>;
31
35
  /** List of static asset paths that need upload URLs */
32
36
  staticAssets: string[];
33
37
  }
@@ -39,6 +43,8 @@ export interface CreateDeploymentResponse {
39
43
  codeUploadUrl: string;
40
44
  /** Unique deployment ID */
41
45
  deploymentId: string;
46
+ /** Map of page asset paths to their signed S3 upload URLs */
47
+ pageUploadUrls: Record<string, string>;
42
48
  /** Map of static asset paths to their signed S3 upload URLs */
43
49
  staticAssetUploadUrls: Record<string, string>;
44
50
  }
@@ -47,6 +53,7 @@ export interface CreateDeploymentApiResponse {
47
53
  meta: {
48
54
  deployment_id: string;
49
55
  lambda_zip_upload_url: string;
56
+ page_upload_urls: Record<string, string>;
50
57
  static_asset_upload_urls: Record<string, string>;
51
58
  };
52
59
  status: number;
@@ -158,21 +158,38 @@ import * as log from '../utils/log.js';
158
158
  const encodedPath = encodeURIComponent(assetPath);
159
159
  staticAssetUploadUrls[assetPath] = `${mockBaseUrl}/${tenantId}/${deploymentId}/static/${encodedPath}?X-Amz-Signature=mock`;
160
160
  }
161
+ // Generate mock signed URLs for page assets
162
+ const pageUploadUrls = {};
163
+ for (const pageData of Object.values(options.pages)){
164
+ for (const assetPath of pageData.assets){
165
+ const encodedPath = encodeURIComponent(assetPath);
166
+ pageUploadUrls[assetPath] = `${mockBaseUrl}/${tenantId}/${deploymentId}/pages/${encodedPath}?X-Amz-Signature=mock`;
167
+ }
168
+ }
161
169
  // Generate mock signed URL for code zip
162
170
  const codeUploadUrl = `${mockBaseUrl}/${tenantId}/${deploymentId}/lambda.zip?X-Amz-Signature=mock`;
163
171
  return {
164
172
  codeUploadUrl,
165
173
  deploymentId,
174
+ pageUploadUrls,
166
175
  staticAssetUploadUrls
167
176
  };
168
177
  }
169
178
  const url = `${getControlPlaneBaseUrl()}/v1/cms/tenant/${tenantId}/deploy/create`;
170
179
  log.debug(`Calling createDeployment API at ${url}`);
180
+ // Build pages payload: { route: { assets: [...] } }
181
+ const pages = {};
182
+ for (const [route, pageData] of Object.entries(options.pages)){
183
+ pages[route] = {
184
+ assets: pageData.assets
185
+ };
186
+ }
171
187
  // REAL API IMPLEMENTATION
172
188
  const response = await controlPlaneFetch({
173
189
  context: 'create deployment',
174
190
  options: {
175
191
  body: JSON.stringify({
192
+ pages,
176
193
  static_assets: options.staticAssets
177
194
  }),
178
195
  headers: {
@@ -188,6 +205,7 @@ import * as log from '../utils/log.js';
188
205
  return {
189
206
  codeUploadUrl: data.meta.lambda_zip_upload_url,
190
207
  deploymentId: data.meta.deployment_id,
208
+ pageUploadUrls: data.meta.page_upload_urls,
191
209
  staticAssetUploadUrls: data.meta.static_asset_upload_urls
192
210
  };
193
211
  }
@@ -197,14 +197,6 @@ import { loginCommand } from './login.js';
197
197
  spinner.start('Collecting assets...');
198
198
  const pages = await adapter.collectPages(projectPath);
199
199
  const assets = await adapter.collectAssets(projectPath);
200
- const allAssetKeys = [
201
- ...pages.uploadKeys,
202
- ...assets.uploadKeys
203
- ];
204
- const combinedPathMap = {
205
- ...pages.pathMap,
206
- ...assets.pathMap
207
- };
208
200
  spinner.stop(pc.green(`✓ ${pages.routes.length} pages, ${assets.routes.length} assets detected`));
209
201
  // Create deployment package (if server adapter)
210
202
  let zipPath = null;
@@ -226,7 +218,7 @@ import { loginCommand } from './login.js';
226
218
  if (zipPath) {
227
219
  log.debug(`Lambda zip size: ${formatBytes(zipSize)}`);
228
220
  }
229
- log.debug(`Pages: ${pages.routes.length} routes`);
221
+ log.debug(`Pages: ${pages.routes.length} routes (${pages.uploadKeys.length} files)`);
230
222
  log.debug(`Assets: ${assets.routes.length} files`);
231
223
  // ===== DEPLOYMENT CONFIRMATION =====
232
224
  if (!options.yes) {
@@ -234,19 +226,28 @@ import { loginCommand } from './login.js';
234
226
  `${pc.cyan('Adapter:')} ${adapter.name}`,
235
227
  `${pc.cyan('Build ID:')} ${buildInfo.buildId}`,
236
228
  zipPath ? `${pc.cyan('Code size:')} ${formatBytes(zipSize)}` : `${pc.cyan('Code:')} No server`,
237
- `${pc.cyan('Pages:')} ${pages.routes.length} routes`,
238
- `${pc.cyan('Assets:')} ${assets.routes.length} files`
229
+ `${pc.cyan('Pages:')} ${pages.routes.length} routes (${pages.uploadKeys.length} files)`,
230
+ `${pc.cyan('Assets:')} ${assets.uploadKeys.length} files`
239
231
  ].join('\n'), 'Details');
240
232
  }
241
233
  // ===== DEPLOYMENT EXECUTION =====
242
234
  // Step 1: Create deployment
243
235
  spinner.start('Creating deployment...');
236
+ // Build pages payload from route → asset keys mapping
237
+ const pagesPayload = {};
238
+ for (const [route, assetKeys] of Object.entries(pages.routeAssetMap)){
239
+ pagesPayload[route] = {
240
+ assets: assetKeys
241
+ };
242
+ }
244
243
  const createResponse = await createDeployment(credential, tenantInstanceId, {
245
- staticAssets: allAssetKeys
244
+ pages: pagesPayload,
245
+ staticAssets: assets.uploadKeys
246
246
  });
247
247
  const deploymentId = createResponse.deploymentId;
248
248
  const codeUploadUrl = createResponse.codeUploadUrl;
249
249
  const staticAssetUploadUrls = createResponse.staticAssetUploadUrls;
250
+ const pageUploadUrls = createResponse.pageUploadUrls;
250
251
  log.debug(`Deployment ID: ${deploymentId}`);
251
252
  // Step 2: Upload code (skip for static sites)
252
253
  if (zipPath) {
@@ -254,23 +255,46 @@ import { loginCommand } from './login.js';
254
255
  await uploadLambdaZip(zipPath, codeUploadUrl);
255
256
  spinner.stop(pc.green(`✓ Code uploaded (${formatBytes(zipSize)})`));
256
257
  }
257
- // Step 3: Upload static assets
258
- if (allAssetKeys.length > 0) {
258
+ // Step 3: Upload page assets
259
+ const totalUploadCount = pages.uploadKeys.length + assets.uploadKeys.length;
260
+ let totalUploaded = 0;
261
+ let totalFailed = 0;
262
+ let totalBytes = 0;
263
+ if (pages.uploadKeys.length > 0) {
264
+ spinner.start('Uploading pages...');
265
+ const pageResults = await uploadStaticAssets({
266
+ assetUrls: pageUploadUrls,
267
+ onProgress: (uploaded, total)=>{
268
+ spinner.message(`Uploading pages (${uploaded}/${total})...`);
269
+ },
270
+ pathMap: pages.pathMap,
271
+ projectPath
272
+ });
273
+ totalUploaded += pageResults.assetsUploaded;
274
+ totalFailed += pageResults.assetsFailed;
275
+ totalBytes += pageResults.totalBytesUploaded;
276
+ spinner.stop(pc.green(`✓ ${pageResults.assetsUploaded} page files uploaded`));
277
+ }
278
+ // Step 4: Upload static assets
279
+ if (assets.uploadKeys.length > 0) {
259
280
  spinner.start('Uploading assets...');
260
- const uploadResults = await uploadStaticAssets({
281
+ const assetResults = await uploadStaticAssets({
261
282
  assetUrls: staticAssetUploadUrls,
262
283
  onProgress: (uploaded, total)=>{
263
284
  spinner.message(`Uploading assets (${uploaded}/${total})...`);
264
285
  },
265
- pathMap: combinedPathMap,
286
+ pathMap: assets.pathMap,
266
287
  projectPath
267
288
  });
268
- if (uploadResults.assetsFailed > 0) {
269
- spinner.stop(pc.yellow(`⚠ ${uploadResults.assetsUploaded}/${allAssetKeys.length} assets uploaded (${uploadResults.assetsFailed} failed)`));
270
- p.log.warning(`${uploadResults.assetsFailed} assets failed to upload. Deployment may be incomplete.`);
271
- } else {
272
- spinner.stop(pc.green(`✓ Assets uploaded (${uploadResults.assetsUploaded} files, ${formatBytes(uploadResults.totalBytesUploaded)})`));
273
- }
289
+ totalUploaded += assetResults.assetsUploaded;
290
+ totalFailed += assetResults.assetsFailed;
291
+ totalBytes += assetResults.totalBytesUploaded;
292
+ spinner.stop(pc.green(`✓ ${assetResults.assetsUploaded} asset files uploaded`));
293
+ }
294
+ if (totalFailed > 0) {
295
+ p.log.warning(`${totalFailed}/${totalUploadCount} files failed to upload. Deployment may be incomplete.`);
296
+ } else if (totalUploaded > 0) {
297
+ p.log.success(pc.green(`✓ All files uploaded (${totalUploaded} files, ${formatBytes(totalBytes)})`));
274
298
  }
275
299
  // Step 4: Perform deployment
276
300
  spinner.start('Performing deployment...');
@@ -3310,6 +3310,8 @@ export type components = {
3310
3310
  UniquePath: {
3311
3311
  paths: string[];
3312
3312
  };
3313
+ /** @enum {string} */
3314
+ PathTypeArray: 'array';
3313
3315
  PathTypeRelationship: {
3314
3316
  /** @enum {string} */
3315
3317
  type: 'relationship';
@@ -3326,7 +3328,7 @@ export type components = {
3326
3328
  /** @enum {string} */
3327
3329
  PathTypeBlocks: 'blocks';
3328
3330
  /** @example array */
3329
- PathType: 'array' | components['schemas']['PathTypeRelationship'] | components['schemas']['PathTypeJoin'] | components['schemas']['PathTypeBlocks'];
3331
+ PathType: components['schemas']['PathTypeArray'] | components['schemas']['PathTypeRelationship'] | components['schemas']['PathTypeJoin'] | components['schemas']['PathTypeBlocks'];
3330
3332
  /**
3331
3333
  * @example {
3332
3334
  * "author.tagIds": "array",
@@ -3441,7 +3443,7 @@ export type components = {
3441
3443
  * ]
3442
3444
  */
3443
3445
  JoinClause: {
3444
- collectionId: string;
3446
+ collectionId: string | string[];
3445
3447
  on: string;
3446
3448
  path: string;
3447
3449
  count: boolean;
@@ -3461,9 +3463,7 @@ export type components = {
3461
3463
  * }
3462
3464
  */
3463
3465
  IncludeSelectClause: {
3464
- [key: string]: true | {
3465
- [key: string]: components['schemas']['IncludeSelectClause'];
3466
- };
3466
+ [key: string]: true | components['schemas']['IncludeSelectClause'];
3467
3467
  };
3468
3468
  /**
3469
3469
  * @example {
@@ -3476,9 +3476,7 @@ export type components = {
3476
3476
  * }
3477
3477
  */
3478
3478
  ExcludeSelectClause: {
3479
- [key: string]: false | {
3480
- [key: string]: components['schemas']['ExcludeSelectClause'];
3481
- };
3479
+ [key: string]: false | components['schemas']['ExcludeSelectClause'];
3482
3480
  };
3483
3481
  /** @description Field selection - use include mode (true) or exclude mode (false), but not both */
3484
3482
  SelectClause: components['schemas']['IncludeSelectClause'] | components['schemas']['ExcludeSelectClause'];
@@ -3599,9 +3597,9 @@ export type components = {
3599
3597
  /** @example posts */
3600
3598
  collection: string;
3601
3599
  /** @example false */
3602
- createOnMissing: false | {
3600
+ createOnMissing: false | (components['schemas']['DocumentData'] & {
3603
3601
  id: string;
3604
- };
3602
+ });
3605
3603
  doc: components['schemas']['DataWithOperations'];
3606
3604
  /** @example 10 */
3607
3605
  limit?: number;
@@ -3772,8 +3770,15 @@ export type components = {
3772
3770
  collection: string;
3773
3771
  /** @example false */
3774
3772
  createOnMissing: false | {
3775
- parent: string;
3773
+ /** @example true */
3776
3774
  latest: boolean;
3775
+ /** @example doc-key */
3776
+ parent: string;
3777
+ /** @example 2024-01-01T00:00:00Z */
3778
+ createdAt?: string | null;
3779
+ /** @example 2024-01-01T00:00:00Z */
3780
+ updatedAt?: string | null;
3781
+ version?: components['schemas']['DocumentData'] & unknown;
3777
3782
  };
3778
3783
  versionDoc: {
3779
3784
  /** @example 2024-01-01T00:00:00Z */
@@ -11,6 +11,7 @@ import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.
11
11
  import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
12
12
  import { addFallbackLocale } from './utilities/locale/index.js';
13
13
  import { buildMeta } from './utilities/meta/buildMeta.js';
14
+ import { normalizeLocaleInWhere } from './utilities/meta/normalizeLocaleInWhere.js';
14
15
  import { convertPayloadWhereToContentAPI } from './utilities/where.js';
15
16
  async function syncCollections() {
16
17
  let existingIds;
@@ -93,6 +94,7 @@ async function createCollection(collectionId) {
93
94
  }
94
95
  async function findMany({ collection, joins, limit, locale: localeArg, page, pagination: _pagination, sort, where }) {
95
96
  const locale = addFallbackLocale(localeArg, this.payload);
97
+ const normalizedWhere = normalizeLocaleInWhere(where, this.payload, collection, locale);
96
98
  const { data: response, error } = await this.client.POST('/api/v0/documents:find', {
97
99
  body: {
98
100
  collection,
@@ -102,11 +104,11 @@ async function findMany({ collection, joins, limit, locale: localeArg, page, pag
102
104
  locale,
103
105
  page: page ?? 1,
104
106
  sort: addFallbackSort(sort, this.payload, collection),
105
- where: convertPayloadWhereToContentAPI(where ?? {}),
107
+ where: convertPayloadWhereToContentAPI(normalizedWhere ?? {}),
106
108
  ...buildMeta(this.payload, {
107
109
  collection,
108
110
  locale,
109
- where
111
+ where: normalizedWhere
110
112
  })
111
113
  }
112
114
  });
@@ -1,3 +1,3 @@
1
1
  import type { Payload } from 'payload';
2
- export declare function addFallbackSort(sort: string | string[] | undefined, payload: Payload, collectionSlug: string, defaultSort?: string | string[]): string | string[] | undefined;
2
+ export declare function addFallbackSort(sort: string | string[] | undefined, payload: Payload, collectionSlug: string | string[], defaultSort?: string | string[]): string | string[] | undefined;
3
3
  //# sourceMappingURL=sorting.d.ts.map
@@ -1,9 +1,14 @@
1
1
  // Add fallback sort to ensure consistent ordering when sorting by non-unique fields
2
2
  // Matches MongoDB adapter behavior
3
+ // If multiple collections are joined, we need to ensure fallback sort fields are consistent across them to avoid conflicts.
4
+ // If all collections have timestamps, use createdAt, otherwise use id.
3
5
  // TO-DECIDE: Should this live here or in content-api?
4
6
  export function addFallbackSort(sort, payload, collectionSlug, defaultSort) {
5
- const collectionConfig = payload.config.collections.find((c)=>c.slug === collectionSlug);
6
- if (!sort || !collectionConfig) {
7
+ const collectionsArray = Array.isArray(collectionSlug) ? collectionSlug : [
8
+ collectionSlug
9
+ ];
10
+ const collectionConfigs = collectionsArray.map((slug)=>payload.config.collections.find((c)=>c.slug === slug)).filter((collection)=>collection != null);
11
+ if (sort == null || collectionConfigs.length === 0) {
7
12
  if (defaultSort) {
8
13
  return addFallbackSort(defaultSort, payload, collectionSlug);
9
14
  }
@@ -13,12 +18,11 @@ export function addFallbackSort(sort, payload, collectionSlug, defaultSort) {
13
18
  sort
14
19
  ];
15
20
  // Determine fallback sort field
16
- let fallbackSort = '-id';
17
- if (collectionConfig.timestamps !== false) {
18
- fallbackSort = '-createdAt';
19
- }
21
+ // If all collections have timestamps, use createdAt, otherwise use id
22
+ const fallbackSort = collectionConfigs.every((config)=>config.timestamps) ? '-createdAt' : '-id';
23
+ const fallbackInverse = fallbackSort.startsWith('-') ? fallbackSort.substring(1) : `-${fallbackSort}`;
20
24
  // Check if fallback sort is already included
21
- const hasFallback = sortArray.some((item)=>item === fallbackSort || item === fallbackSort.replace('-', ''));
25
+ const hasFallback = sortArray.some((item)=>item === fallbackSort || item === fallbackInverse);
22
26
  if (hasFallback) {
23
27
  return sort;
24
28
  }
@@ -4,6 +4,18 @@ import { castFieldValue } from './castFieldValue.js';
4
4
  import { removeVirtualFields } from './removeVirtualFields.js';
5
5
  import { stripFields } from './stripFields.js';
6
6
  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
+ }
7
19
  /**
8
20
  * Transform data before sending to Content API (WRITE operations)
9
21
  *
@@ -46,6 +58,9 @@ import { transformToLocalizeStatus } from './transformPublishedLocale.js';
46
58
  }
47
59
  const current = ref;
48
60
  let value = current[field.name];
61
+ if (isAtomicOperation(value)) {
62
+ return;
63
+ }
49
64
  // null → [] for non-localized array-like fields. SQL adapters return null for empty join
50
65
  // tables; Content API stores JSON null which breaks jsonb_array_elements queries.
51
66
  if (value === null && !('localized' in field && field.localized)) {
@@ -168,7 +183,8 @@ export function dataFromContentAPI(payload, collectionSlug, data, locale) {
168
183
  // In all-locales mode, localized fields use {} so afterRead can safely iterate locale keys.
169
184
  if (value === undefined) {
170
185
  const isRequired = 'required' in field && field.required;
171
- if (!isRequired) {
186
+ const isJoin = field.type === 'join';
187
+ if (!isRequired && !isJoin) {
172
188
  current[field.name] = isLocalized && isAllLocales ? {} : null;
173
189
  }
174
190
  return;
@@ -43,7 +43,7 @@ type ContentAPIJoin = components['schemas']['JoinClause'][number];
43
43
  * where: { ... }, // Content API where format
44
44
  * sort: '-title',
45
45
  * limit: 10,
46
- * offset: 10 // offset-based pagination
46
+ * page: 2
47
47
  * }]
48
48
  *
49
49
  * The mapping requires the collection config to resolve:
@@ -42,7 +42,7 @@ import { convertPayloadWhereToContentAPI } from './where.js';
42
42
  * where: { ... }, // Content API where format
43
43
  * sort: '-title',
44
44
  * limit: 10,
45
- * offset: 10 // offset-based pagination
45
+ * page: 2
46
46
  * }]
47
47
  *
48
48
  * The mapping requires the collection config to resolve:
@@ -91,10 +91,9 @@ import { convertPayloadWhereToContentAPI } from './where.js';
91
91
  if (!foundJoin) {
92
92
  for (const sanitizedJoin of collectionConfig.polymorphicJoins){
93
93
  if (sanitizedJoin.joinPath === joinPath) {
94
- // For polymorphic joins, collection is an array - use first one
95
94
  const collections = sanitizedJoin.field.collection;
96
95
  foundJoin = {
97
- collectionSlug: Array.isArray(collections) ? collections[0] : collections,
96
+ collectionSlug: collections,
98
97
  defaultSort: sanitizedJoin.field.defaultSort,
99
98
  on: sanitizedJoin.field.on
100
99
  };
@@ -106,12 +105,8 @@ import { convertPayloadWhereToContentAPI } from './where.js';
106
105
  payload.logger.warn(`Join path '${joinPath}' not found in collection config for '${collectionConfig.slug}'`);
107
106
  continue;
108
107
  }
109
- // Convert pagination: Payload uses page, Content API JoinClause uses offset
108
+ // Fall back to the default limit if not specified in the query
110
109
  const effectiveLimit = joinQuery.limit ?? foundJoin.defaultLimit;
111
- let offset;
112
- if (joinQuery.page && joinQuery.page > 1 && effectiveLimit) {
113
- offset = (joinQuery.page - 1) * effectiveLimit;
114
- }
115
110
  // Add fallback sort for the joined collection
116
111
  const sortWithFallback = addFallbackSort(joinQuery.sort, payload, foundJoin.collectionSlug, foundJoin.defaultSort);
117
112
  const contentAPIJoin = {
@@ -119,13 +114,11 @@ import { convertPayloadWhereToContentAPI } from './where.js';
119
114
  count: joinQuery.count ?? false,
120
115
  on: foundJoin.on,
121
116
  path: joinPath,
122
- ...joinQuery.limit !== undefined ? {
123
- limit: joinQuery.limit
124
- } : foundJoin.defaultLimit !== undefined && {
125
- limit: foundJoin.defaultLimit
117
+ ...effectiveLimit !== undefined && {
118
+ limit: effectiveLimit
126
119
  },
127
- ...offset !== undefined && {
128
- offset
120
+ ...joinQuery.page && {
121
+ page: joinQuery.page
129
122
  },
130
123
  ...sortWithFallback && {
131
124
  sort: sortWithFallback
@@ -8,6 +8,10 @@ import type { PathTypesRecord } from '../../temp-utilities/types.js';
8
8
  * - Array fields (type: "array")
9
9
  * - Relationship fields (type: { type: "relationship", collection, hasMany })
10
10
  * - Join fields (type: { type: "join", collection, hasMany, on })
11
+ *
12
+ * Localized array/blocks fields (stored as locale maps) are handled specially:
13
+ * - The localized field itself is NOT marked as 'array' (it's a locale map object)
14
+ * - The locale-specific path (e.g. "localizedBlocks.en") IS marked as 'array'
11
15
  */
12
16
  export declare function buildPathTypes(payload: Payload, collectionSlug: string, where: undefined | Where): PathTypesRecord;
13
17
  //# sourceMappingURL=buildPathTypes.d.ts.map
@@ -90,19 +90,31 @@
90
90
  return null;
91
91
  }
92
92
  /**
93
- * Finds a field in the collection schema by its path
94
- * Supports nested paths like "access.read.users"
93
+ * Finds a field in the collection schema by its path.
94
+ * Supports nested paths like "access.read.users".
95
+ * For localized array/blocks fields, locale key segments (e.g. "en") are skipped transparently,
96
+ * so "localizedBlocks.en.richText" resolves to the richText field inside localizedBlocks.
95
97
  */ function findFieldByPath(payload, collectionSlug, path) {
96
98
  const collectionConfig = payload.config.collections.find((c)=>c.slug === collectionSlug);
97
99
  if (!collectionConfig) {
98
100
  return undefined;
99
101
  }
102
+ const localeCodes = payload.config.localization ? payload.config.localization.localeCodes : [];
100
103
  const pathSegments = path.split('.');
101
104
  // Walk through the path segments to find the field
102
105
  let currentFields = collectionConfig.fields;
103
106
  let currentField = undefined;
104
107
  for(let i = 0; i < pathSegments.length; i++){
105
108
  const segment = pathSegments[i];
109
+ // If this segment is a locale key for the previously found localized array/blocks field,
110
+ // skip it transparently (the locale key is the wrapping object key, not a real field).
111
+ if (localeCodes.includes(segment) && currentField && 'localized' in currentField && currentField.localized && (currentField.type === 'blocks' || currentField.type === 'array')) {
112
+ if (i === pathSegments.length - 1) {
113
+ // Last segment is a locale key → return the localized field (represents locale-specific array)
114
+ return currentField;
115
+ }
116
+ continue;
117
+ }
106
118
  // First try to find a direct match
107
119
  currentField = currentFields.find((f)=>'name' in f && f.name === segment);
108
120
  // If not found, check if any unnamed container fields (collapsible, row) contain it
@@ -118,8 +130,19 @@
118
130
  } else if ('tabs' in field && field.type === 'tabs') {
119
131
  // Check all tabs
120
132
  for (const tab of field.tabs){
121
- currentField = tab.fields.find((f)=>'name' in f && f.name === segment);
122
- if (currentField) {
133
+ // Named tab: the tab name itself is a path segment (acts like a group)
134
+ if ('name' in tab && tab.name === segment) {
135
+ currentField = {
136
+ name: tab.name,
137
+ type: 'group',
138
+ fields: tab.fields
139
+ };
140
+ break;
141
+ }
142
+ // Unnamed tab: search its fields for the segment
143
+ const found = tab.fields.find((f)=>'name' in f && f.name === segment);
144
+ if (found) {
145
+ currentField = found;
123
146
  break;
124
147
  }
125
148
  }
@@ -140,9 +163,12 @@
140
163
  } else if (currentField.type === 'array' && 'fields' in currentField) {
141
164
  currentFields = currentField.fields;
142
165
  } else if (currentField.type === 'blocks' && 'blocks' in currentField) {
143
- // For blocks, we can't determine the exact field without knowing the block type
144
- // Return the blocks field itself
145
- return currentField;
166
+ // Collect all fields from all block definitions to continue traversal
167
+ const allBlockFields = [];
168
+ for (const block of currentField.blocks){
169
+ allBlockFields.push(...block.fields);
170
+ }
171
+ currentFields = allBlockFields;
146
172
  } else if ((currentField.type === 'relationship' || currentField.type === 'upload') && typeof currentField.relationTo === 'string') {
147
173
  // Traverse into the related collection's fields
148
174
  const { relationTo } = currentField;
@@ -168,10 +194,15 @@
168
194
  * - Array fields (type: "array")
169
195
  * - Relationship fields (type: { type: "relationship", collection, hasMany })
170
196
  * - Join fields (type: { type: "join", collection, hasMany, on })
197
+ *
198
+ * Localized array/blocks fields (stored as locale maps) are handled specially:
199
+ * - The localized field itself is NOT marked as 'array' (it's a locale map object)
200
+ * - The locale-specific path (e.g. "localizedBlocks.en") IS marked as 'array'
171
201
  */ export function buildPathTypes(payload, collectionSlug, where) {
172
202
  if (!where) {
173
203
  return {};
174
204
  }
205
+ const localeCodes = payload.config.localization ? payload.config.localization.localeCodes : [];
175
206
  const pathTypes = {};
176
207
  const paths = extractPathsFromWhere(where);
177
208
  for (const path of paths){
@@ -183,8 +214,23 @@
183
214
  if (partialPath in pathTypes) {
184
215
  continue;
185
216
  }
217
+ // If the current segment is a locale code and the parent field is a localized array/blocks,
218
+ // mark this locale-specific path as 'array' (the per-locale content is the actual array).
219
+ if (i > 0 && localeCodes.includes(segments[i])) {
220
+ const parentPath = segments.slice(0, i).join('.');
221
+ const parentField = findFieldByPath(payload, collectionSlug, parentPath);
222
+ if (parentField && 'localized' in parentField && parentField.localized && (parentField.type === 'blocks' || parentField.type === 'array')) {
223
+ pathTypes[partialPath] = 'array';
224
+ continue;
225
+ }
226
+ }
186
227
  const field = findFieldByPath(payload, collectionSlug, partialPath);
187
228
  if (field) {
229
+ // Localized array/blocks fields are stored as locale maps (not arrays).
230
+ // Skip them here — the locale-specific path (e.g. "localizedBlocks.en") is marked above.
231
+ if ('localized' in field && field.localized && (field.type === 'blocks' || field.type === 'array')) {
232
+ continue;
233
+ }
188
234
  const fieldType = getFieldType(field);
189
235
  if (fieldType !== null) {
190
236
  pathTypes[partialPath] = fieldType;
@@ -0,0 +1,10 @@
1
+ import type { Payload, Where } from 'payload';
2
+ /**
3
+ * Normalizes a Where clause by inserting locale keys into paths that traverse
4
+ * localized array/blocks fields without an explicit locale.
5
+ *
6
+ * Only runs when a locale is provided and the collection has localized array/blocks fields.
7
+ * Returns the original where clause if no transformation is needed.
8
+ */
9
+ export declare function normalizeLocaleInWhere(where: undefined | Where, payload: Payload, collectionSlug: string, locale: string | undefined): undefined | Where;
10
+ //# sourceMappingURL=normalizeLocaleInWhere.d.ts.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Collects paths of localized array/blocks fields in a collection.
3
+ * These fields are stored as locale maps (e.g. {"en": [...], "es": [...]}) rather than
4
+ * direct arrays, so queries must include the locale key in the path.
5
+ */ function collectLocalizedArrayPaths(fields, prefix, paths) {
6
+ for (const field of fields){
7
+ if (!('name' in field) || !field.name) {
8
+ // Unnamed container (row, collapsible) — recurse into its fields
9
+ if ('fields' in field && Array.isArray(field.fields)) {
10
+ collectLocalizedArrayPaths(field.fields, prefix, paths);
11
+ }
12
+ if ('tabs' in field && Array.isArray(field.tabs)) {
13
+ for (const tab of field.tabs){
14
+ const tabPath = tab.name ? prefix ? `${prefix}.${tab.name}` : tab.name : prefix;
15
+ if (tab.fields) {
16
+ collectLocalizedArrayPaths(tab.fields, tabPath, paths);
17
+ }
18
+ }
19
+ }
20
+ continue;
21
+ }
22
+ const fieldPath = prefix ? `${prefix}.${field.name}` : field.name;
23
+ if ('localized' in field && field.localized && (field.type === 'array' || field.type === 'blocks')) {
24
+ paths.push(fieldPath);
25
+ // Don't recurse further — child paths are accessed via locale key, handled separately
26
+ } else if (field.type === 'group' && 'fields' in field) {
27
+ collectLocalizedArrayPaths(field.fields, fieldPath, paths);
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Returns the paths of all localized array/blocks fields for the given collection.
33
+ */ function getLocalizedArrayPaths(payload, collectionSlug) {
34
+ const config = payload.config.collections.find((c)=>c.slug === collectionSlug);
35
+ if (!config) {
36
+ return [];
37
+ }
38
+ const paths = [];
39
+ collectLocalizedArrayPaths(config.fields, '', paths);
40
+ return paths;
41
+ }
42
+ /**
43
+ * Transforms a single path by inserting the locale after a localized array/blocks prefix
44
+ * if the path goes through one without an explicit locale key.
45
+ *
46
+ * Example:
47
+ * path "localizedBlocks.richText.children.text", localizedArrayPaths ["localizedBlocks"],
48
+ * locale "en", localeCodes ["en", "es"]
49
+ * → "localizedBlocks.en.richText.children.text"
50
+ *
51
+ * path "localizedBlocks.en.richText.children.text" (already has locale)
52
+ * → unchanged
53
+ */ function insertLocaleInPath(path, localizedArrayPaths, locale, localeCodes) {
54
+ for (const lpath of localizedArrayPaths){
55
+ if (path.startsWith(lpath + '.')) {
56
+ const rest = path.slice(lpath.length + 1);
57
+ const firstSegment = rest.split('.')[0];
58
+ if (!localeCodes.includes(firstSegment)) {
59
+ return `${lpath}.${locale}.${rest}`;
60
+ }
61
+ }
62
+ }
63
+ return path;
64
+ }
65
+ /**
66
+ * Recursively transforms all condition paths in a Where clause by inserting the locale key
67
+ * after any localized array/blocks field prefix that is missing one.
68
+ *
69
+ * This is needed because localized array/blocks fields are stored as locale maps
70
+ * (e.g. data.localizedBlocks.en = [...]) rather than flat arrays. Without the locale
71
+ * key, the path would point to the locale map object, causing jsonb_array_elements to fail.
72
+ */ function transformWhere(where, localizedArrayPaths, locale, localeCodes) {
73
+ const result = {};
74
+ for (const [key, value] of Object.entries(where)){
75
+ if (key === 'and' || key === 'or') {
76
+ result[key] = value.map((clause)=>transformWhere(clause, localizedArrayPaths, locale, localeCodes));
77
+ } else {
78
+ const normalizedKey = insertLocaleInPath(key, localizedArrayPaths, locale, localeCodes);
79
+ result[normalizedKey] = value;
80
+ }
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Normalizes a Where clause by inserting locale keys into paths that traverse
86
+ * localized array/blocks fields without an explicit locale.
87
+ *
88
+ * Only runs when a locale is provided and the collection has localized array/blocks fields.
89
+ * Returns the original where clause if no transformation is needed.
90
+ */ export function normalizeLocaleInWhere(where, payload, collectionSlug, locale) {
91
+ if (!where || !locale || !payload.config.localization) {
92
+ return where;
93
+ }
94
+ const localeCodes = payload.config.localization.localeCodes;
95
+ const localizedArrayPaths = getLocalizedArrayPaths(payload, collectionSlug);
96
+ if (localizedArrayPaths.length === 0) {
97
+ return where;
98
+ }
99
+ return transformWhere(where, localizedArrayPaths, locale, localeCodes);
100
+ }
101
+
102
+ //# sourceMappingURL=normalizeLocaleInWhere.js.map
@@ -8,7 +8,8 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
8
8
  }
9
9
  const conditions = [];
10
10
  for (const [key, value] of Object.entries(where)){
11
- if (key === 'and' || key === 'or') {
11
+ const keyLower = key.toLowerCase();
12
+ if (keyLower === 'and' || keyLower === 'or') {
12
13
  const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, {
13
14
  ...options,
14
15
  insideLogicalOperator: true
@@ -21,7 +22,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
21
22
  }
22
23
  return true;
23
24
  });
24
- if (key === 'and') {
25
+ if (keyLower === 'and') {
25
26
  conditions.push(...nestedConditions);
26
27
  } else if (nestedConditions.length > 0) {
27
28
  conditions.push({
@@ -36,16 +36,21 @@ export class NextjsAdapter {
36
36
  }
37
37
  async collectPages(projectPath) {
38
38
  const ssg = await collectSSGAssets(projectPath);
39
- // Extract route paths from S3 keys: "products.html" "/products", "index.html" → "/"
40
- const routes = [
41
- ...new Set(ssg.keys.filter((k)=>k.endsWith('.html')).map((k)=>{
42
- const stripped = k.replace(/\.html$/, '');
43
- return stripped === 'index' ? '/' : `/${stripped}`;
44
- }))
45
- ];
39
+ // Build route asset keys mapping and extract unique routes
40
+ const routeAssetMap = {};
41
+ for (const key of ssg.keys){
42
+ // Strip extension to get the base, then derive the route
43
+ const base = key.replace(/\.(?:html|rsc|meta)$/, '');
44
+ const route = base === 'index' ? '/' : `/${base}`;
45
+ if (!routeAssetMap[route]) {
46
+ routeAssetMap[route] = [];
47
+ }
48
+ routeAssetMap[route].push(key);
49
+ }
46
50
  return {
47
51
  pathMap: ssg.pathMap,
48
- routes,
52
+ routeAssetMap,
53
+ routes: Object.keys(routeAssetMap),
49
54
  uploadKeys: ssg.keys
50
55
  };
51
56
  }
@@ -55,11 +55,16 @@ export class NitroAdapter {
55
55
  const routes = pages.map((p)=>p === 'index' ? '/' : `/${p}`);
56
56
  const uploadKeys = pages.map((p)=>p === 'index' ? 'index.html' : `${p}/index.html`);
57
57
  const pathMap = {};
58
+ const routeAssetMap = {};
58
59
  for(let i = 0; i < uploadKeys.length; i++){
59
60
  pathMap[uploadKeys[i]] = path.join('.output', 'public', uploadKeys[i]);
61
+ routeAssetMap[routes[i]] = [
62
+ uploadKeys[i]
63
+ ];
60
64
  }
61
65
  return {
62
66
  pathMap,
67
+ routeAssetMap,
63
68
  routes,
64
69
  uploadKeys
65
70
  };
@@ -19,6 +19,7 @@ export class ViteAdapter {
19
19
  collectPages() {
20
20
  return Promise.resolve({
21
21
  pathMap: {},
22
+ routeAssetMap: {},
22
23
  routes: [],
23
24
  uploadKeys: []
24
25
  });
@@ -1,6 +1,8 @@
1
1
  export interface PageCollection {
2
2
  /** S3 key → relative filesystem path from project root */
3
3
  pathMap: Record<string, string>;
4
+ /** Route → S3 upload keys for that route (e.g., { "/products": ["products.html", "products.rsc"] }) */
5
+ routeAssetMap: Record<string, string[]>;
4
6
  /** Route paths for the manifest pages array (e.g., ["/products", "/blog"]) */
5
7
  routes: string[];
6
8
  /** S3 keys to upload (e.g., ["products.html", "products.rsc", "products.meta"]) */
@@ -11,6 +11,37 @@ const INITIAL_RETRY_DELAY = 1000 // 1 second
11
11
  */ function sleep(ms) {
12
12
  return new Promise((resolve)=>setTimeout(resolve, ms));
13
13
  }
14
+ /** Common MIME types for web assets, keyed by file extension */ const MIME_TYPES = {
15
+ '.avif': 'image/avif',
16
+ '.css': 'text/css',
17
+ '.csv': 'text/csv',
18
+ '.eot': 'application/vnd.ms-fontobject',
19
+ '.gif': 'image/gif',
20
+ '.html': 'text/html',
21
+ '.ico': 'image/x-icon',
22
+ '.jpeg': 'image/jpeg',
23
+ '.jpg': 'image/jpeg',
24
+ '.js': 'application/javascript',
25
+ '.json': 'application/json',
26
+ '.map': 'application/json',
27
+ '.meta': 'application/json',
28
+ '.mjs': 'application/javascript',
29
+ '.mp4': 'video/mp4',
30
+ '.otf': 'font/otf',
31
+ '.pdf': 'application/pdf',
32
+ '.png': 'image/png',
33
+ '.rsc': 'text/x-component',
34
+ '.svg': 'image/svg+xml',
35
+ '.ttf': 'font/ttf',
36
+ '.txt': 'text/plain',
37
+ '.wasm': 'application/wasm',
38
+ '.webm': 'video/webm',
39
+ '.webp': 'image/webp',
40
+ '.woff': 'font/woff',
41
+ '.woff2': 'font/woff2',
42
+ '.xml': 'application/xml',
43
+ '.zip': 'application/zip'
44
+ };
14
45
  /**
15
46
  * Check if a signed URL is a mock URL
16
47
  */ function isMockUrl(signedUrl) {
@@ -44,14 +75,8 @@ const INITIAL_RETRY_DELAY = 1000 // 1 second
44
75
  let lastError = null;
45
76
  for(let attempt = 0; attempt < MAX_RETRIES; attempt++){
46
77
  try {
47
- // Content-type only needed if css or js, else let S3 infer
48
78
  const ext = path.extname(filePath).toLowerCase();
49
- let determinedContentType = contentType;
50
- if (ext === '.js') {
51
- determinedContentType = 'application/javascript';
52
- } else if (ext === '.css') {
53
- determinedContentType = 'text/css';
54
- }
79
+ const determinedContentType = contentType ?? MIME_TYPES[ext];
55
80
  const response = await fetch(signedUrl, {
56
81
  body: fileBuffer,
57
82
  headers: determinedContentType ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/figma",
3
- "version": "0.0.1-alpha.59",
3
+ "version": "0.0.1-alpha.60",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "exports": {