@payloadcms/figma 0.1.0-alpha.3 → 0.1.0-alpha.5

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 (116) hide show
  1. package/dist/api/check-project-subdomain-availability.d.ts +12 -0
  2. package/dist/api/check-project-subdomain-availability.js +32 -0
  3. package/dist/api/control-plane-error.d.ts +6 -0
  4. package/dist/api/control-plane-error.js +8 -0
  5. package/dist/api/control-plane-fetch.d.ts +7 -0
  6. package/dist/api/control-plane-fetch.js +37 -0
  7. package/dist/api/control-plane.d.ts +0 -8
  8. package/dist/api/control-plane.js +1 -47
  9. package/dist/api/create-project.d.ts +18 -0
  10. package/dist/api/create-project.js +37 -0
  11. package/dist/api/list-project-workspaces.d.ts +8 -0
  12. package/dist/api/list-project-workspaces.js +28 -0
  13. package/dist/api/project-resource-type.d.ts +1 -0
  14. package/dist/api/project-resource-type.js +1 -0
  15. package/dist/auth/headless-oauth-flow.d.ts +23 -0
  16. package/dist/auth/headless-oauth-flow.js +69 -0
  17. package/dist/auth/oauth-flow.d.ts +2 -0
  18. package/dist/auth/oauth-flow.js +13 -8
  19. package/dist/auth/token-store.d.ts +4 -1
  20. package/dist/auth/token-store.js +14 -0
  21. package/dist/auth/types.d.ts +12 -0
  22. package/dist/cli.d.ts +10 -1
  23. package/dist/cli.js +39 -4
  24. package/dist/commands/bootstrap.js +2 -1
  25. package/dist/commands/deploy.d.ts +10 -0
  26. package/dist/commands/deploy.js +39 -11
  27. package/dist/commands/detect-project-resource-type.d.ts +3 -0
  28. package/dist/commands/detect-project-resource-type.js +28 -0
  29. package/dist/commands/init.d.ts +2 -2
  30. package/dist/commands/init.js +163 -78
  31. package/dist/commands/login.d.ts +11 -1
  32. package/dist/commands/login.js +66 -4
  33. package/dist/commands/prompt-to-create-project/format-project-subdomain.d.ts +1 -0
  34. package/dist/commands/prompt-to-create-project/format-project-subdomain.js +3 -0
  35. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.d.ts +1 -0
  36. package/dist/commands/prompt-to-create-project/format-unavailable-subdomain-reason.js +7 -0
  37. package/dist/commands/prompt-to-create-project/get-default-project-name.d.ts +1 -0
  38. package/dist/commands/prompt-to-create-project/get-default-project-name.js +13 -0
  39. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.d.ts +7 -0
  40. package/dist/commands/prompt-to-create-project/prompt-for-available-subdomain.js +32 -0
  41. package/dist/commands/prompt-to-create-project/resolve-project-region.d.ts +8 -0
  42. package/dist/commands/prompt-to-create-project/resolve-project-region.js +27 -0
  43. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.d.ts +9 -0
  44. package/dist/commands/prompt-to-create-project/resolve-project-subdomain.js +54 -0
  45. package/dist/commands/prompt-to-create-project/resolve-text-input.d.ts +9 -0
  46. package/dist/commands/prompt-to-create-project/resolve-text-input.js +23 -0
  47. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.d.ts +12 -0
  48. package/dist/commands/prompt-to-create-project/resolve-workspace-plan-key.js +36 -0
  49. package/dist/commands/prompt-to-create-project/validate-subdomain.d.ts +1 -0
  50. package/dist/commands/prompt-to-create-project/validate-subdomain.js +9 -0
  51. package/dist/commands/prompt-to-create-project.d.ts +24 -0
  52. package/dist/commands/prompt-to-create-project.js +69 -0
  53. package/dist/commands/setup-agent.d.ts +12 -0
  54. package/dist/commands/setup-agent.js +123 -0
  55. package/dist/commands/write-project-configuration.d.ts +12 -0
  56. package/dist/commands/write-project-configuration.js +16 -0
  57. package/dist/db-content-api/index.js +52 -33
  58. package/dist/db-content-api/utilities/data/injectGlobalType.d.ts +6 -0
  59. package/dist/db-content-api/utilities/data/injectGlobalType.js +11 -0
  60. package/dist/db-content-api/utilities/isRecord.d.ts +9 -0
  61. package/dist/db-content-api/utilities/isRecord.js +19 -0
  62. package/dist/db-content-api/utilities/joins.js +5 -1
  63. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.d.ts +10 -0
  64. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.js +95 -0
  65. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.d.ts +9 -0
  66. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.js +112 -0
  67. package/dist/db-content-api/utilities/where.d.ts +2 -6
  68. package/dist/db-content-api/utilities/where.js +20 -9
  69. package/dist/exports/views.d.ts +1 -0
  70. package/dist/exports/views.js +1 -0
  71. package/dist/imageSizeOptions.d.ts +23 -0
  72. package/dist/imageSizeOptions.js +26 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/lib/write-agent-config.d.ts +12 -0
  75. package/dist/lib/write-agent-config.js +52 -0
  76. package/dist/oauth/defaults.d.ts +1 -0
  77. package/dist/oauth/defaults.js +1 -0
  78. package/dist/oauth/endpoints/getLoginEndpoint.js +7 -2
  79. package/dist/oauth/index.js +24 -9
  80. package/dist/oauth/utilities/getAdminCollectionSlug.d.ts +1 -1
  81. package/dist/oauth/utilities/getAuthorizeURL.d.ts +3 -0
  82. package/dist/oauth/utilities/getAuthorizeURL.js +8 -2
  83. package/dist/plugin/build-config.js +22 -9
  84. package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +2 -3
  85. package/dist/plugin/cloud-limits/CloudLimitModal/index.js +4 -16
  86. package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +5 -0
  87. package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +0 -3
  88. package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +65 -124
  89. package/dist/plugin/cloud-limits/LimitReachedView/index.css +14 -0
  90. package/dist/plugin/cloud-limits/LimitReachedView/index.d.ts +4 -0
  91. package/dist/plugin/cloud-limits/LimitReachedView/index.js +52 -0
  92. package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +7 -2
  93. package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +0 -8
  94. package/dist/plugin/cloud-limits/SlugTracker/index.js +14 -10
  95. package/dist/plugin/cloud-limits/routes.d.ts +2 -0
  96. package/dist/plugin/cloud-limits/routes.js +4 -0
  97. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.d.ts +9 -0
  98. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.js +28 -0
  99. package/dist/plugin/cloud-limits/shared.d.ts +1 -1
  100. package/dist/plugin/cloud-limits/shared.js +25 -0
  101. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.d.ts +2 -0
  102. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.js +134 -0
  103. package/dist/plugin/cloud-limits/utilities/toasts.d.ts +4 -0
  104. package/dist/plugin/cloud-limits/utilities/toasts.js +19 -0
  105. package/dist/skills/deploy-to-figma/SKILL.md +234 -0
  106. package/dist/skills/deploy-to-figma/references/cli-reference.md +146 -0
  107. package/dist/types.d.ts +9 -0
  108. package/dist/utils/lambda-config.js +4 -14
  109. package/dist/utils/messages.js +22 -3
  110. package/dist/utils/payload-generate.d.ts +10 -0
  111. package/dist/utils/payload-generate.js +59 -0
  112. package/dist/utils/pnpm-builds.d.ts +14 -0
  113. package/dist/utils/pnpm-builds.js +125 -0
  114. package/package.json +9 -1
  115. package/dist/lib/download-skill.d.ts +0 -12
  116. package/dist/lib/download-skill.js +0 -77
@@ -3,12 +3,14 @@ import { APIError, createDatabaseAdapter, ValidationError } from 'payload';
3
3
  import { v4 as uuid } from 'uuid';
4
4
  import { FigmaApiError } from '../api/figma-api.js';
5
5
  import { CONTENT_SYSTEM_NOT_FOUND_MESSAGE } from '../constants.js';
6
+ import { redirectInitialAutosaveCreate } from '../plugin/cloud-limits/server/redirectInitialAutosaveCreate.js';
6
7
  import { getGlobalSlug } from './temp-utilities/slug.js';
7
8
  import { addFallbackSort } from './temp-utilities/sorting.js';
8
9
  import { unwrapDocument, unwrapFindResponse } from './temp-utilities/unwrapDocument.js';
9
10
  import { createAuthMiddleware, createErrorMiddleware } from './utilities/auth.js';
10
11
  import { getClientVersionHeaders } from './utilities/clientVersionHeaders.js';
11
12
  import { dataToContentAPI, resolveVersionContent } from './utilities/data/index.js';
13
+ import { injectGlobalType } from './utilities/data/injectGlobalType.js';
12
14
  import { validateRelationshipIds } from './utilities/data/validateRelationships.js';
13
15
  import { convertPayloadJoinsToContentAPI } from './utilities/joins.js';
14
16
  import { addFallbackLocale } from './utilities/locale/index.js';
@@ -121,7 +123,7 @@ async function createCollection(collectionId) {
121
123
  }
122
124
  async function findMany({ collection, joins, limit, locale: localeArg, page, pagination: _pagination, sort, where }) {
123
125
  const locale = addFallbackLocale(localeArg, this.payload);
124
- const normalizedWhere = normalizeLocaleInWhere(where, this.payload, collection, locale);
126
+ const localeNormalizedWhere = normalizeLocaleInWhere(where, this.payload, collection, locale);
125
127
  const joinClause = convertPayloadJoinsToContentAPI(this.payload, collection, joins);
126
128
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/documents:find', {
127
129
  body: {
@@ -133,7 +135,7 @@ async function findMany({ collection, joins, limit, locale: localeArg, page, pag
133
135
  page: page ?? 1,
134
136
  schema: this.documentSchema(),
135
137
  sort: addFallbackSort(sort, this.payload, collection),
136
- where: convertPayloadWhereToContentAPI(normalizedWhere ?? {})
138
+ where: convertPayloadWhereToContentAPI(localeNormalizedWhere, this.payload, collection, false)
137
139
  }
138
140
  });
139
141
  if (error) {
@@ -165,7 +167,7 @@ async function find(args) {
165
167
  // TODO: remove Omit when this PR is released: https://github.com/payloadcms/payload/pull/15183
166
168
  async function findVersions(args) {
167
169
  const sortWithFallback = addFallbackSort(args.sort || '-updatedAt', this.payload, args.collection);
168
- const whereClause = convertPayloadWhereToContentAPI(args.where ?? {});
170
+ const whereClause = convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, true);
169
171
  const locale = addFallbackLocale(args.locale, this.payload);
170
172
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/document_versions:find', {
171
173
  body: {
@@ -280,7 +282,7 @@ async function updateVersion(args) {
280
282
  updatedAt: versionMeta.updatedAt,
281
283
  version: versionContent
282
284
  },
283
- where: convertPayloadWhereToContentAPI(where)
285
+ where: convertPayloadWhereToContentAPI(where, this.payload, args.collection, true)
284
286
  }
285
287
  });
286
288
  if (error) {
@@ -301,9 +303,12 @@ async function updateVersion(args) {
301
303
  });
302
304
  }
303
305
  async function deleteVersions(args) {
304
- const collection = args.collection;
306
+ // Globals are addressed by `globalSlug` and map to the `_global-<slug>` collection.
307
+ // Only one of `collection`/`globalSlug` is set.
308
+ const globalSlug = args.globalSlug != null ? getGlobalSlug(args.globalSlug) : undefined;
309
+ const collection = args.collection ?? globalSlug;
305
310
  if (collection == null) {
306
- throw new Error('deleteVersions requires a collection');
311
+ throw new Error('deleteVersions requires a collection or globalSlug');
307
312
  }
308
313
  const locale = addFallbackLocale(args.locale, this.payload);
309
314
  const { error, response: httpResponse } = await this.client.POST('/api/v1/document_versions:delete', {
@@ -312,7 +317,7 @@ async function deleteVersions(args) {
312
317
  contentSystemId: this.contentSystemId,
313
318
  locale,
314
319
  schema: this.documentSchema(),
315
- where: convertPayloadWhereToContentAPI(args.where)
320
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, collection, true)
316
321
  }
317
322
  });
318
323
  if (error) {
@@ -342,7 +347,7 @@ async function findDistinct(args) {
342
347
  page: args.page ?? 1,
343
348
  schema: this.documentSchema(),
344
349
  sort: addFallbackSort(args.sort, this.payload, args.collection),
345
- where: convertPayloadWhereToContentAPI(args.where ?? {})
350
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
346
351
  }
347
352
  });
348
353
  if (error) {
@@ -375,7 +380,7 @@ async function updateMany(args) {
375
380
  locale,
376
381
  schema: this.documentSchema(),
377
382
  sort: addFallbackSort(args.sort || '-updatedAt', this.payload, args.collection),
378
- where: convertPayloadWhereToContentAPI(args.where)
383
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
379
384
  }
380
385
  });
381
386
  if (error) {
@@ -415,7 +420,7 @@ async function updateOne(args) {
415
420
  environmentName: this.environmentName,
416
421
  locale,
417
422
  schema: this.documentSchema(),
418
- where: convertPayloadWhereToContentAPI(where)
423
+ where: convertPayloadWhereToContentAPI(where, this.payload, args.collection, false)
419
424
  }
420
425
  });
421
426
  if (error) {
@@ -447,7 +452,7 @@ async function deleteMany(args) {
447
452
  environmentName: this.environmentName,
448
453
  locale,
449
454
  schema: this.documentSchema(),
450
- where: convertPayloadWhereToContentAPI(args.where)
455
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
451
456
  }
452
457
  });
453
458
  if (error) {
@@ -463,7 +468,7 @@ async function deleteOne(args) {
463
468
  environmentName: this.environmentName,
464
469
  locale,
465
470
  schema: this.documentSchema(),
466
- where: convertPayloadWhereToContentAPI(args.where)
471
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
467
472
  }
468
473
  });
469
474
  if (error) {
@@ -521,6 +526,12 @@ async function create(args) {
521
526
  }
522
527
  });
523
528
  if (error) {
529
+ redirectInitialAutosaveCreate({
530
+ collectionSlug: args.collection,
531
+ errorCode: error.code,
532
+ payload: this.payload,
533
+ req: args.req
534
+ });
524
535
  throw createDocumentContentAPIError(this, 'create', error, httpResponse.status, args.collection);
525
536
  }
526
537
  if (!contentAPIResponse) {
@@ -570,7 +581,7 @@ async function count(args) {
570
581
  contentSystemId: this.contentSystemId,
571
582
  locale,
572
583
  schema: this.documentSchema(),
573
- where: convertPayloadWhereToContentAPI(args.where)
584
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
574
585
  }
575
586
  });
576
587
  if (error) {
@@ -591,7 +602,7 @@ async function countVersions(args) {
591
602
  contentSystemId: this.contentSystemId,
592
603
  locale,
593
604
  schema: this.documentSchema(),
594
- where: convertPayloadWhereToContentAPI(args.where)
605
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, true)
595
606
  }
596
607
  });
597
608
  if (error) {
@@ -604,12 +615,19 @@ async function countVersions(args) {
604
615
  totalDocs: response.result.count
605
616
  };
606
617
  }
618
+ // Content API document ids are text. A document id is only ever a string or a number
619
+ // (a custom numeric id), so coerce those to a string and treat anything else as absent.
620
+ function toIdString(value) {
621
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined;
622
+ }
607
623
  async function upsert(args) {
608
- let documentId = args.data.id;
624
+ // The Content API stores every document id as text; a custom numeric id arrives as a
625
+ // number, so coerce it (matching `create`) before it becomes `createOnMissing.id`.
626
+ let documentId = toIdString(args.data.id);
609
627
  if (!documentId && args.where && 'id' in args.where) {
610
628
  const idClause = args.where.id;
611
- if (idClause && 'equals' in idClause) {
612
- documentId = idClause.equals;
629
+ if (idClause) {
630
+ documentId = toIdString(idClause.equals);
613
631
  }
614
632
  }
615
633
  if (!documentId) {
@@ -621,21 +639,27 @@ async function upsert(args) {
621
639
  data: args.data,
622
640
  payload: this.payload
623
641
  });
642
+ // `createdAt` is create-time metadata, so seed it on `createOnMissing` (used only when no
643
+ // document matches) rather than on `doc`. The server preserves an existing document's
644
+ // `createdAt` on update precisely because `doc` omits it; `updatedAt` is still refreshed on
645
+ // every write via `dataToContentAPI`. Sending `createdAt` on `doc` would overwrite it on
646
+ // every update (e.g. repeated `updateGlobal` calls).
647
+ const createdAt = new Date().toISOString();
624
648
  const { data: contentAPIResponse, error, response: httpResponse } = await this.client.POST('/api/v1/documents:update', {
625
649
  body: {
626
650
  collection: args.collection,
627
651
  contentSystemId: this.contentSystemId,
628
652
  createOnMissing: {
629
- id: documentId
653
+ id: documentId,
654
+ createdAt
630
655
  },
631
656
  doc: dataToContentAPI(this.payload, args.collection, args.data, {
632
- applyDefaults: true,
633
- createdAt: true
657
+ applyDefaults: true
634
658
  }),
635
659
  environmentName: this.environmentName,
636
660
  locale,
637
661
  schema: this.documentSchema(),
638
- where: convertPayloadWhereToContentAPI(args.where)
662
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
639
663
  }
640
664
  });
641
665
  if (error) {
@@ -682,7 +706,7 @@ async function updateJobs(args) {
682
706
  } : where;
683
707
  const docData = dataToContentAPI(this.payload, collection, data);
684
708
  const sortClause = addFallbackSort(sort || '-updatedAt', this.payload, collection);
685
- const whereQuery = convertPayloadWhereToContentAPI(whereClause);
709
+ const whereQuery = convertPayloadWhereToContentAPI(whereClause, this.payload, collection, false);
686
710
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/documents:update', {
687
711
  body: {
688
712
  collection,
@@ -716,13 +740,13 @@ async function updateJobs(args) {
716
740
  payload: this.payload
717
741
  }));
718
742
  }
719
- function createGlobal(args) {
743
+ async function createGlobal(args) {
720
744
  // Globals are singletons identified by their slug. Use upsert so concurrent calls
721
745
  // (e.g. handleSchedules processing multiple queueables with the same null jobStats)
722
746
  // don't fail with "A record with this field already exists". The deep-merge semantics
723
747
  // of the update path are safe here: callers that want a fresh create (first run) get
724
748
  // an effective create, and callers that race a prior create get a merge instead.
725
- return this.upsert({
749
+ const doc = await this.upsert({
726
750
  collection: getGlobalSlug(args.slug),
727
751
  data: {
728
752
  ...args.data,
@@ -734,6 +758,7 @@ function createGlobal(args) {
734
758
  }
735
759
  }
736
760
  });
761
+ return injectGlobalType(doc, args.slug);
737
762
  }
738
763
  async function findGlobal(args) {
739
764
  const doc = await this.findOne({
@@ -741,17 +766,10 @@ async function findGlobal(args) {
741
766
  locale: args.locale,
742
767
  where: args.where ?? {}
743
768
  });
744
- if (doc != null) {
745
- ;
746
- doc.globalType = args.slug;
747
- }
748
- return doc;
769
+ return injectGlobalType(doc, args.slug);
749
770
  }
750
771
  async function updateGlobal(args) {
751
- // TODO: upsert's `createOnMissing` path generates a new `createdAt` on every call,
752
- // which overwrites the original timestamp on updates. The fix belongs in the Content API:
753
- // `createOnMissing` should preserve `createdAt` on updates and only set it on creates.
754
- return this.upsert({
772
+ const doc = await this.upsert({
755
773
  collection: getGlobalSlug(args.slug),
756
774
  data: args.data,
757
775
  where: {
@@ -760,6 +778,7 @@ async function updateGlobal(args) {
760
778
  }
761
779
  }
762
780
  });
781
+ return injectGlobalType(doc, args.slug);
763
782
  }
764
783
  // TODO: remove Omit when this PR is released: https://github.com/payloadcms/payload/pull/15183
765
784
  function findGlobalVersions(args) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Payload expects globals to carry a `globalType` (their slug) alongside the stored data.
3
+ * Globals are stored as `_global-<slug>` collections that have no such column, so the adapter
4
+ * attaches it to every global document it returns.
5
+ */
6
+ export declare function injectGlobalType<T>(doc: T, slug: string): T;
@@ -0,0 +1,11 @@
1
+ import { isRecord } from '../isRecord.js';
2
+ /**
3
+ * Payload expects globals to carry a `globalType` (their slug) alongside the stored data.
4
+ * Globals are stored as `_global-<slug>` collections that have no such column, so the adapter
5
+ * attaches it to every global document it returns.
6
+ */ export function injectGlobalType(doc, slug) {
7
+ return isRecord(doc) ? {
8
+ ...doc,
9
+ globalType: slug
10
+ } : doc;
11
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A plain, string-keyed object — the shape that can safely be spread and have
3
+ * properties read off it without tripping over arrays, `null`, or exotic objects.
4
+ *
5
+ * Objects carrying symbol-keyed own properties are rejected: symbol keys are how
6
+ * libraries tag a value as something more than data, and a spread copies only the
7
+ * string keys, so treating such a value as a record silently strips the tag.
8
+ */
9
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A plain, string-keyed object — the shape that can safely be spread and have
3
+ * properties read off it without tripping over arrays, `null`, or exotic objects.
4
+ *
5
+ * Objects carrying symbol-keyed own properties are rejected: symbol keys are how
6
+ * libraries tag a value as something more than data, and a spread copies only the
7
+ * string keys, so treating such a value as a record silently strips the tag.
8
+ */ export function isRecord(value) {
9
+ if (value == null) {
10
+ return false;
11
+ }
12
+ if (typeof value !== 'object') {
13
+ return false;
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return false;
17
+ }
18
+ return Object.getOwnPropertySymbols(value).length === 0;
19
+ }
@@ -102,6 +102,10 @@ import { convertPayloadWhereToContentAPI } from './where.js';
102
102
  const effectiveLimit = joinQuery.limit ?? foundJoin.defaultLimit;
103
103
  // Add fallback sort for the joined collection
104
104
  const sortWithFallback = addFallbackSort(joinQuery.sort, payload, foundJoin.collectionSlug, foundJoin.defaultSort);
105
+ // A join's `where` filters the *joined* collection, so id coercion resolves against
106
+ // that collection (the first target for a polymorphic join; `id` is coerced regardless).
107
+ const joinedSlug = Array.isArray(foundJoin.collectionSlug) ? foundJoin.collectionSlug[0] : foundJoin.collectionSlug;
108
+ const whereConverted = convertPayloadWhereToContentAPI(joinQuery.where, payload, joinedSlug, false);
105
109
  const contentAPIJoin = {
106
110
  count: joinQuery.count ?? false,
107
111
  path: joinPath,
@@ -115,7 +119,7 @@ import { convertPayloadWhereToContentAPI } from './where.js';
115
119
  sort: sortWithFallback
116
120
  },
117
121
  ...joinQuery.where && {
118
- where: convertPayloadWhereToContentAPI(joinQuery.where)
122
+ where: whereConverted
119
123
  }
120
124
  };
121
125
  result.push(contentAPIJoin);
@@ -0,0 +1,10 @@
1
+ import type { Field, Payload } from 'payload';
2
+ export interface ResolveContext {
3
+ readonly fieldCache: Map<string, boolean>;
4
+ readonly fields: Field[];
5
+ readonly isVersions: boolean;
6
+ readonly payload: Payload;
7
+ }
8
+ /** Walks a path's segments against a field list, reporting whether it lands on a
9
+ * relationship/upload field's id-bearing value. */
10
+ export declare function resolvesToRelationshipValue(segments: string[], fields: Field[], ctx: ResolveContext): boolean;
@@ -0,0 +1,95 @@
1
+ import { resolveBlocks } from '../resolveBlocks.js';
2
+ /** Walks a path's segments against a field list, reporting whether it lands on a
3
+ * relationship/upload field's id-bearing value. */ export function resolvesToRelationshipValue(segments, fields, ctx) {
4
+ const [head, ...rest] = segments;
5
+ if (head == null) {
6
+ return false;
7
+ }
8
+ const field = findNamedField(fields, head);
9
+ if (field == null) {
10
+ return false;
11
+ }
12
+ switch(field.type){
13
+ case 'array':
14
+ return 'fields' in field && resolvesToRelationshipValue(skipLocaleSegment(rest, field, ctx), field.fields, ctx);
15
+ case 'blocks':
16
+ {
17
+ if (!('blocks' in field)) {
18
+ return false;
19
+ }
20
+ // Any block whose fields the remaining path resolves through makes it id-bearing:
21
+ // one path can only mean one thing, whichever block supplied the field.
22
+ const childSegments = skipLocaleSegment(rest, field, ctx);
23
+ return resolveBlocks(field.blocks, ctx.payload.config.blocks).some((block)=>resolvesToRelationshipValue(childSegments, block.fields, ctx));
24
+ }
25
+ case 'group':
26
+ return 'fields' in field && resolvesToRelationshipValue(rest, field.fields, ctx);
27
+ case 'relationship':
28
+ case 'upload':
29
+ // A polymorphic field's id sits at `<path>.value`; its `.relationTo` is a slug.
30
+ if (Array.isArray(field.relationTo)) {
31
+ return rest.length === 1 && rest[0] === 'value';
32
+ }
33
+ // A single-target field's id sits at the field's own path.
34
+ if (rest.length === 0) {
35
+ return true;
36
+ }
37
+ // Payload accepts an explicit `<path>.id` for that same value —
38
+ // `getTableColumnFromPath` treats the bare path and `.id` as one column — which the
39
+ // Content API resolves as a traversal into the related collection's, equally text, id.
40
+ return rest.length === 1 && rest[0] === 'id';
41
+ default:
42
+ return false;
43
+ }
44
+ }
45
+ /** Finds the field a path segment names. Unnamed containers (row, collapsible, unnamed tab)
46
+ * are transparent to query paths, so their children are searched too; a named tab nests its
47
+ * fields under its own name, exactly like a group. */ function findNamedField(fields, name) {
48
+ for (const field of fields){
49
+ if ('name' in field && field.name) {
50
+ // A named container's children need its name consumed first, so only match the
51
+ // container itself here.
52
+ if (field.name === name) {
53
+ return field;
54
+ }
55
+ continue;
56
+ }
57
+ if ('fields' in field && Array.isArray(field.fields)) {
58
+ const match = findNamedField(field.fields, name);
59
+ if (match) {
60
+ return match;
61
+ }
62
+ }
63
+ if ('tabs' in field && Array.isArray(field.tabs)) {
64
+ for (const tab of field.tabs){
65
+ if (!tab.fields) {
66
+ continue;
67
+ }
68
+ if (!tab.name) {
69
+ const match = findNamedField(tab.fields, name);
70
+ if (match) {
71
+ return match;
72
+ }
73
+ } else if (tab.name === name) {
74
+ return {
75
+ name: tab.name,
76
+ type: 'group',
77
+ fields: tab.fields
78
+ };
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return undefined;
84
+ }
85
+ // A localized array/blocks field is stored as a locale map, so its children's paths carry a
86
+ // locale key (`items.en.rel`) — see `normalizeLocaleInWhere`, which runs before this.
87
+ // Locale keys are *only* added to `array` and `blocks`.
88
+ function skipLocaleSegment(segments, field, ctx) {
89
+ const localization = ctx.payload.config.localization;
90
+ if (!localization || !('localized' in field) || !field.localized) {
91
+ return segments;
92
+ }
93
+ const [head] = segments;
94
+ return head != null && localization.localeCodes.includes(head) ? segments.slice(1) : segments;
95
+ }
@@ -0,0 +1,9 @@
1
+ import type { Payload, Where } from 'payload';
2
+ /**
3
+ * Stringifies numeric operands on a Where clause's id-bearing paths (`id`, `parent` for
4
+ * versions, and relationship-value paths), so a custom-numeric-id filter matches the
5
+ * Content API's text storage. Returns the clause untouched when there is nothing to
6
+ * convert. `isVersions` treats `parent` (the parent document's id) as id-bearing and looks
7
+ * for the collection's own fields under `version.`.
8
+ */
9
+ export declare function stringifyIdsInWhere(where: undefined | Where, payload: Payload, collectionSlug: string, isVersions: boolean): undefined | Where;
@@ -0,0 +1,112 @@
1
+ import { resolvesToRelationshipValue } from './resolvesToRelationshipValue.js';
2
+ // The Content API stores every document id and every relationship value as text, but
3
+ // Payload keeps a collection's custom numeric id as a `number`. So a filter that carries
4
+ // a numeric id would never match the stored string form. This is the sole place the
5
+ // adapter translates those query-side ids to the wire's string form (the data-side
6
+ // counterpart is `convertRelationshipIds`); `convertPayloadWhereToContentAPI` itself is
7
+ // purely structural.
8
+ //
9
+ // A condition's path is id-bearing when it resolves to:
10
+ // - `id` — the document's own id (always).
11
+ // - `parent` — the parent document's id, on a *versions* query (`isVersions`).
12
+ // - a relationship/upload field's value: the field's own path for a single-target field
13
+ // (`rel: { equals: 999 }`), or `<path>.value` for a polymorphic one
14
+ // (`rel.value: { equals: 999 }`). A polymorphic field's `.relationTo` is a slug, not
15
+ // an id, so it is deliberately left alone.
16
+ //
17
+ // Every stored id is text, so a bare "stringify any numeric operand on this path" is
18
+ // sufficient — no per-target id-type resolution is needed (which also handles a
19
+ // polymorphic `in` array that mixes targets).
20
+ /**
21
+ * Stringifies numeric operands on a Where clause's id-bearing paths (`id`, `parent` for
22
+ * versions, and relationship-value paths), so a custom-numeric-id filter matches the
23
+ * Content API's text storage. Returns the clause untouched when there is nothing to
24
+ * convert. `isVersions` treats `parent` (the parent document's id) as id-bearing and looks
25
+ * for the collection's own fields under `version.`.
26
+ */ export function stringifyIdsInWhere(where, payload, collectionSlug, isVersions) {
27
+ if (!where) {
28
+ return where;
29
+ }
30
+ // An unknown slug names no fields, but `id` (and `parent` on a versions query) is
31
+ // id-bearing regardless of the config, so still walk the clause with an empty field list:
32
+ // discarding it here would turn the query into a match-all.
33
+ const config = payload.config.collections.find((c)=>c.slug === collectionSlug);
34
+ const fields = config?.fields ?? [];
35
+ const fieldCache = new Map();
36
+ return transformWhere(where, {
37
+ fieldCache,
38
+ fields,
39
+ isVersions,
40
+ payload
41
+ });
42
+ }
43
+ /** Recursively transforms a Where clause, stringifying numeric operands on any condition
44
+ * whose path is id-bearing. */ function transformWhere(where, context) {
45
+ const result = {};
46
+ for (const [key, value] of Object.entries(where)){
47
+ const keyLower = key.toLowerCase();
48
+ if (keyLower === 'and' || keyLower === 'or') {
49
+ const targetValue = where[key];
50
+ if (targetValue == null || !Array.isArray(targetValue)) {
51
+ continue;
52
+ }
53
+ result[key] = targetValue.map((clause)=>transformWhere(clause, context));
54
+ } else if (isIdBearingPath(key, context)) {
55
+ result[key] = stringifyNumericOperands(value);
56
+ } else {
57
+ result[key] = value;
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+ /** Whether a condition's path resolves to an id. */ function isIdBearingPath(path, context) {
63
+ const cachedResult = context.fieldCache.get(path);
64
+ if (cachedResult != null) {
65
+ return cachedResult;
66
+ }
67
+ // GraphQL spells a nested path with `__`, since dots are invalid in GraphQL names; the
68
+ // structural conversion normalizes those the same way.
69
+ const segments = path.replace(/__/g, '.').split('.');
70
+ // The document's own id — on a versions query, the version document's own id.
71
+ if (segments.length === 1 && segments[0] === 'id') {
72
+ return true;
73
+ }
74
+ if (context.isVersions) {
75
+ // `parent` is a version-only field that contains an ID.
76
+ if (segments.length === 1) {
77
+ return segments[0] === 'parent';
78
+ }
79
+ // The only other paths that can contain IDs are in the version envelope.
80
+ if (segments[0] !== 'version') {
81
+ return false;
82
+ }
83
+ }
84
+ // Normalize the path segments by removing the `version` envelope for paths
85
+ // in document versions.
86
+ const normalizedSegments = context.isVersions ? segments.slice(1) : segments;
87
+ return resolvesToRelationshipValue(normalizedSegments, context.fields, context);
88
+ }
89
+ function stringifyNumericOperands(condition) {
90
+ // Payload's shorthand — `{ id: 999 }` rather than `{ id: { equals: 999 } }` — carries the
91
+ // operand directly, with no operator keys to walk. The structural conversion reads it as
92
+ // an implicit `equals`, so coerce it in place: walking a primitive as an operator map
93
+ // would drop the condition entirely and turn the query into a match-all.
94
+ if (condition === null || typeof condition !== 'object' || Array.isArray(condition)) {
95
+ return stringifyNumbers(condition);
96
+ }
97
+ const result = {};
98
+ for (const [operator, operand] of Object.entries(condition)){
99
+ result[operator] = stringifyNumbers(operand);
100
+ }
101
+ return result;
102
+ }
103
+ /** A number becomes its string form; a number inside an array does too (e.g. `in`);
104
+ * everything else passes through untouched. */ function stringifyNumbers(value) {
105
+ if (typeof value === 'number') {
106
+ return String(value);
107
+ }
108
+ if (Array.isArray(value)) {
109
+ return value.map((entry)=>typeof entry === 'number' ? String(entry) : entry);
110
+ }
111
+ return value;
112
+ }
@@ -1,9 +1,5 @@
1
- import type { Where } from 'payload';
1
+ import type { Payload, Where } from 'payload';
2
2
  import type { components } from '../generated/content-api-types.js';
3
3
  type WhereClauseV1 = components['schemas']['WhereClauseV1'];
4
- type ConvertOptions = {
5
- /** Internal flag to track if we're inside a logical operator (and/or) */
6
- insideLogicalOperator?: boolean;
7
- };
8
- export declare function convertPayloadWhereToContentAPI(where: undefined | Where, options?: ConvertOptions): WhereClauseV1;
4
+ export declare function convertPayloadWhereToContentAPI(where: undefined | Where, payload: Payload, collectionSlug: string, isVersions: boolean): WhereClauseV1;
9
5
  export {};
@@ -1,4 +1,21 @@
1
- export function convertPayloadWhereToContentAPI(where, options = {}) {
1
+ import { stringifyIdsInWhere } from './meta/stringifyIdsInWhere.js';
2
+ // Payload's format: { fieldName: { operator: value }, and: [...], or: [...] }
3
+ // Content API expects: { and: [{ path, operator, value }], or: [{ path, operator, value }] }
4
+ //
5
+ // The format stays different. This allows the Content API to evolve its filter schema,
6
+ // for example to support composite types like Point, which are not lexicographically sortable.
7
+ // Semantics must remain identical to Payload.
8
+ export function convertPayloadWhereToContentAPI(where, payload, collectionSlug, isVersions) {
9
+ // Normalize id-bearing operands to the wire's string form (custom numeric ids), then
10
+ // convert Payload's where shape to the Content API's. The two steps are inseparable —
11
+ // every id filter must be coerced — so they live behind this single entry point.
12
+ return convertStructure(stringifyIdsInWhere(where, payload, collectionSlug, isVersions), {
13
+ insideLogicalOperator: false
14
+ });
15
+ }
16
+ // The structural Payload → Content API conversion. Assumes id operands have already been
17
+ // coerced to the wire's string form (see `convertPayloadWhereToContentAPI`).
18
+ function convertStructure(where, options) {
2
19
  // Empty where {} should be { and: [] } not undefined
3
20
  // Content API requires a where clause structure even for "no filter"
4
21
  if (!where || Object.keys(where).length === 0) {
@@ -10,7 +27,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
10
27
  for (const [key, value] of Object.entries(where)){
11
28
  const keyLower = key.toLowerCase();
12
29
  if (keyLower === 'and' || keyLower === 'or') {
13
- const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, {
30
+ const nestedConditions = value.map((item)=>convertStructure(item, {
14
31
  ...options,
15
32
  insideLogicalOperator: true
16
33
  })).filter((condition)=>{
@@ -42,7 +59,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
42
59
  conditions.push({
43
60
  operator: 'equals',
44
61
  path: fieldPath,
45
- value: fieldPath === 'id' && typeof value === 'number' ? String(value) : value
62
+ value
46
63
  });
47
64
  continue;
48
65
  }
@@ -67,12 +84,6 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
67
84
  if (finalValue === 'null') {
68
85
  finalValue = null;
69
86
  }
70
- // Content API stores all document IDs as strings.
71
- // Payload sends numeric values for collections with custom numeric ID fields,
72
- // so we must stringify to match.
73
- if (fieldPath === 'id' && typeof finalValue === 'number') {
74
- finalValue = String(finalValue);
75
- }
76
87
  conditions.push({
77
88
  operator: op,
78
89
  path: fieldPath,
@@ -0,0 +1 @@
1
+ export { LimitReachedView } from '../plugin/cloud-limits/LimitReachedView/index.js';
@@ -0,0 +1 @@
1
+ export { LimitReachedView } from '../plugin/cloud-limits/LimitReachedView/index.js';