@payloadcms/figma 0.1.0-alpha.4 → 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 (45) hide show
  1. package/dist/db-content-api/index.js +31 -17
  2. package/dist/db-content-api/utilities/joins.js +5 -1
  3. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.d.ts +10 -0
  4. package/dist/db-content-api/utilities/meta/resolvesToRelationshipValue.js +95 -0
  5. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.d.ts +9 -0
  6. package/dist/db-content-api/utilities/meta/stringifyIdsInWhere.js +112 -0
  7. package/dist/db-content-api/utilities/where.d.ts +2 -6
  8. package/dist/db-content-api/utilities/where.js +20 -11
  9. package/dist/exports/views.d.ts +1 -0
  10. package/dist/exports/views.js +1 -0
  11. package/dist/imageSizeOptions.d.ts +23 -0
  12. package/dist/imageSizeOptions.js +26 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/oauth/defaults.d.ts +1 -0
  15. package/dist/oauth/defaults.js +1 -0
  16. package/dist/oauth/endpoints/getLoginEndpoint.js +7 -2
  17. package/dist/oauth/index.js +24 -9
  18. package/dist/oauth/utilities/getAdminCollectionSlug.d.ts +1 -1
  19. package/dist/oauth/utilities/getAuthorizeURL.d.ts +3 -0
  20. package/dist/oauth/utilities/getAuthorizeURL.js +8 -2
  21. package/dist/plugin/build-config.js +22 -9
  22. package/dist/plugin/cloud-limits/CloudLimitModal/index.d.ts +2 -3
  23. package/dist/plugin/cloud-limits/CloudLimitModal/index.js +4 -16
  24. package/dist/plugin/cloud-limits/CloudLimitModal/index.scss +5 -0
  25. package/dist/plugin/cloud-limits/CloudLimitProvider/index.d.ts +0 -3
  26. package/dist/plugin/cloud-limits/CloudLimitProvider/index.js +65 -124
  27. package/dist/plugin/cloud-limits/LimitReachedView/index.css +14 -0
  28. package/dist/plugin/cloud-limits/LimitReachedView/index.d.ts +4 -0
  29. package/dist/plugin/cloud-limits/LimitReachedView/index.js +52 -0
  30. package/dist/plugin/cloud-limits/SlugRegistryContext/index.d.ts +7 -2
  31. package/dist/plugin/cloud-limits/SlugTracker/index.d.ts +0 -8
  32. package/dist/plugin/cloud-limits/SlugTracker/index.js +14 -10
  33. package/dist/plugin/cloud-limits/routes.d.ts +2 -0
  34. package/dist/plugin/cloud-limits/routes.js +4 -0
  35. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.d.ts +9 -0
  36. package/dist/plugin/cloud-limits/server/redirectInitialAutosaveCreate.js +28 -0
  37. package/dist/plugin/cloud-limits/shared.d.ts +1 -1
  38. package/dist/plugin/cloud-limits/shared.js +25 -0
  39. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.d.ts +2 -0
  40. package/dist/plugin/cloud-limits/utilities/cloudLimitInterceptor.js +134 -0
  41. package/dist/plugin/cloud-limits/utilities/toasts.d.ts +4 -0
  42. package/dist/plugin/cloud-limits/utilities/toasts.js +19 -0
  43. package/package.json +8 -1
  44. package/dist/db-content-api/utilities/data/stringifyNumericIds.d.ts +0 -1
  45. package/dist/db-content-api/utilities/data/stringifyNumericIds.js +0 -19
@@ -3,6 +3,7 @@ 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';
@@ -122,7 +123,7 @@ async function createCollection(collectionId) {
122
123
  }
123
124
  async function findMany({ collection, joins, limit, locale: localeArg, page, pagination: _pagination, sort, where }) {
124
125
  const locale = addFallbackLocale(localeArg, this.payload);
125
- const normalizedWhere = normalizeLocaleInWhere(where, this.payload, collection, locale);
126
+ const localeNormalizedWhere = normalizeLocaleInWhere(where, this.payload, collection, locale);
126
127
  const joinClause = convertPayloadJoinsToContentAPI(this.payload, collection, joins);
127
128
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/documents:find', {
128
129
  body: {
@@ -134,7 +135,7 @@ async function findMany({ collection, joins, limit, locale: localeArg, page, pag
134
135
  page: page ?? 1,
135
136
  schema: this.documentSchema(),
136
137
  sort: addFallbackSort(sort, this.payload, collection),
137
- where: convertPayloadWhereToContentAPI(normalizedWhere ?? {})
138
+ where: convertPayloadWhereToContentAPI(localeNormalizedWhere, this.payload, collection, false)
138
139
  }
139
140
  });
140
141
  if (error) {
@@ -166,7 +167,7 @@ async function find(args) {
166
167
  // TODO: remove Omit when this PR is released: https://github.com/payloadcms/payload/pull/15183
167
168
  async function findVersions(args) {
168
169
  const sortWithFallback = addFallbackSort(args.sort || '-updatedAt', this.payload, args.collection);
169
- const whereClause = convertPayloadWhereToContentAPI(args.where ?? {});
170
+ const whereClause = convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, true);
170
171
  const locale = addFallbackLocale(args.locale, this.payload);
171
172
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/document_versions:find', {
172
173
  body: {
@@ -281,7 +282,7 @@ async function updateVersion(args) {
281
282
  updatedAt: versionMeta.updatedAt,
282
283
  version: versionContent
283
284
  },
284
- where: convertPayloadWhereToContentAPI(where)
285
+ where: convertPayloadWhereToContentAPI(where, this.payload, args.collection, true)
285
286
  }
286
287
  });
287
288
  if (error) {
@@ -316,7 +317,7 @@ async function deleteVersions(args) {
316
317
  contentSystemId: this.contentSystemId,
317
318
  locale,
318
319
  schema: this.documentSchema(),
319
- where: convertPayloadWhereToContentAPI(args.where)
320
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, collection, true)
320
321
  }
321
322
  });
322
323
  if (error) {
@@ -346,7 +347,7 @@ async function findDistinct(args) {
346
347
  page: args.page ?? 1,
347
348
  schema: this.documentSchema(),
348
349
  sort: addFallbackSort(args.sort, this.payload, args.collection),
349
- where: convertPayloadWhereToContentAPI(args.where ?? {})
350
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
350
351
  }
351
352
  });
352
353
  if (error) {
@@ -379,7 +380,7 @@ async function updateMany(args) {
379
380
  locale,
380
381
  schema: this.documentSchema(),
381
382
  sort: addFallbackSort(args.sort || '-updatedAt', this.payload, args.collection),
382
- where: convertPayloadWhereToContentAPI(args.where)
383
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
383
384
  }
384
385
  });
385
386
  if (error) {
@@ -419,7 +420,7 @@ async function updateOne(args) {
419
420
  environmentName: this.environmentName,
420
421
  locale,
421
422
  schema: this.documentSchema(),
422
- where: convertPayloadWhereToContentAPI(where)
423
+ where: convertPayloadWhereToContentAPI(where, this.payload, args.collection, false)
423
424
  }
424
425
  });
425
426
  if (error) {
@@ -451,7 +452,7 @@ async function deleteMany(args) {
451
452
  environmentName: this.environmentName,
452
453
  locale,
453
454
  schema: this.documentSchema(),
454
- where: convertPayloadWhereToContentAPI(args.where)
455
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
455
456
  }
456
457
  });
457
458
  if (error) {
@@ -467,7 +468,7 @@ async function deleteOne(args) {
467
468
  environmentName: this.environmentName,
468
469
  locale,
469
470
  schema: this.documentSchema(),
470
- where: convertPayloadWhereToContentAPI(args.where)
471
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
471
472
  }
472
473
  });
473
474
  if (error) {
@@ -525,6 +526,12 @@ async function create(args) {
525
526
  }
526
527
  });
527
528
  if (error) {
529
+ redirectInitialAutosaveCreate({
530
+ collectionSlug: args.collection,
531
+ errorCode: error.code,
532
+ payload: this.payload,
533
+ req: args.req
534
+ });
528
535
  throw createDocumentContentAPIError(this, 'create', error, httpResponse.status, args.collection);
529
536
  }
530
537
  if (!contentAPIResponse) {
@@ -574,7 +581,7 @@ async function count(args) {
574
581
  contentSystemId: this.contentSystemId,
575
582
  locale,
576
583
  schema: this.documentSchema(),
577
- where: convertPayloadWhereToContentAPI(args.where)
584
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
578
585
  }
579
586
  });
580
587
  if (error) {
@@ -595,7 +602,7 @@ async function countVersions(args) {
595
602
  contentSystemId: this.contentSystemId,
596
603
  locale,
597
604
  schema: this.documentSchema(),
598
- where: convertPayloadWhereToContentAPI(args.where)
605
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, true)
599
606
  }
600
607
  });
601
608
  if (error) {
@@ -608,12 +615,19 @@ async function countVersions(args) {
608
615
  totalDocs: response.result.count
609
616
  };
610
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
+ }
611
623
  async function upsert(args) {
612
- 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);
613
627
  if (!documentId && args.where && 'id' in args.where) {
614
628
  const idClause = args.where.id;
615
- if (idClause && 'equals' in idClause) {
616
- documentId = idClause.equals;
629
+ if (idClause) {
630
+ documentId = toIdString(idClause.equals);
617
631
  }
618
632
  }
619
633
  if (!documentId) {
@@ -645,7 +659,7 @@ async function upsert(args) {
645
659
  environmentName: this.environmentName,
646
660
  locale,
647
661
  schema: this.documentSchema(),
648
- where: convertPayloadWhereToContentAPI(args.where)
662
+ where: convertPayloadWhereToContentAPI(args.where, this.payload, args.collection, false)
649
663
  }
650
664
  });
651
665
  if (error) {
@@ -692,7 +706,7 @@ async function updateJobs(args) {
692
706
  } : where;
693
707
  const docData = dataToContentAPI(this.payload, collection, data);
694
708
  const sortClause = addFallbackSort(sort || '-updatedAt', this.payload, collection);
695
- const whereQuery = convertPayloadWhereToContentAPI(whereClause);
709
+ const whereQuery = convertPayloadWhereToContentAPI(whereClause, this.payload, collection, false);
696
710
  const { data: response, error, response: httpResponse } = await this.client.POST('/api/v1/documents:update', {
697
711
  body: {
698
712
  collection,
@@ -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,5 +1,21 @@
1
- import { stringifyNumericIds } from './data/stringifyNumericIds.js';
2
- 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) {
3
19
  // Empty where {} should be { and: [] } not undefined
4
20
  // Content API requires a where clause structure even for "no filter"
5
21
  if (!where || Object.keys(where).length === 0) {
@@ -11,7 +27,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
11
27
  for (const [key, value] of Object.entries(where)){
12
28
  const keyLower = key.toLowerCase();
13
29
  if (keyLower === 'and' || keyLower === 'or') {
14
- const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, {
30
+ const nestedConditions = value.map((item)=>convertStructure(item, {
15
31
  ...options,
16
32
  insideLogicalOperator: true
17
33
  })).filter((condition)=>{
@@ -43,7 +59,7 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
43
59
  conditions.push({
44
60
  operator: 'equals',
45
61
  path: fieldPath,
46
- value: fieldPath === 'id' ? stringifyNumericIds(value) : value
62
+ value
47
63
  });
48
64
  continue;
49
65
  }
@@ -68,13 +84,6 @@ export function convertPayloadWhereToContentAPI(where, options = {}) {
68
84
  if (finalValue === 'null') {
69
85
  finalValue = null;
70
86
  }
71
- // Content API stores all document IDs as strings.
72
- // Payload sends numeric values for collections with custom numeric ID fields,
73
- // so we must stringify to match. This includes the `in`/`not_in` arrays that
74
- // Payload's relationship-population dataloader always uses ({ id: { in: [...] } }).
75
- if (fieldPath === 'id') {
76
- finalValue = stringifyNumericIds(finalValue);
77
- }
78
87
  conditions.push({
79
88
  operator: op,
80
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';
@@ -0,0 +1,23 @@
1
+ import type { RequestInitCfPropertiesImage } from '@cloudflare/workers-types';
2
+ type CloudflareWorkerImageOptions = RequestInitCfPropertiesImage;
3
+ export declare const supportedCloudflareImageOptionKeys: readonly ["anim", "background", "blur", "border", "brightness", "compression", "contrast", "dpr", "fit", "flip", "format", "gamma", "gravity", "height", "metadata", "quality", "rotate", "saturation", "segment", "sharpen", "trim", "upscale", "width", "zoom"];
4
+ type SupportedCloudflareImageOptionKey = (typeof supportedCloudflareImageOptionKeys)[number];
5
+ type SupportedCloudflareImageOptions = Pick<CloudflareWorkerImageOptions, SupportedCloudflareImageOptionKey>;
6
+ type CloudflareImageSizeFormat = Exclude<NonNullable<SupportedCloudflareImageOptions['format']>, 'json' | 'svg'>;
7
+ type CloudflareImageSizeOptionsExceptFormat = Omit<SupportedCloudflareImageOptions, 'format'>;
8
+ /**
9
+ * Options accepted for a reusable Payload image-size preset.
10
+ *
11
+ * Presets can control the generated image, but cannot use remote overlays or
12
+ * forward authentication to an image origin. Formats that do not produce a
13
+ * supported stored image are also excluded.
14
+ */
15
+ export type CloudflareImageSizeOptions = {
16
+ format?: CloudflareImageSizeFormat;
17
+ } & CloudflareImageSizeOptionsExceptFormat;
18
+ declare module 'payload' {
19
+ interface RegisteredImageSizeOptions {
20
+ figma: CloudflareImageSizeOptions;
21
+ }
22
+ }
23
+ export {};
@@ -0,0 +1,26 @@
1
+ export const supportedCloudflareImageOptionKeys = [
2
+ 'anim',
3
+ 'background',
4
+ 'blur',
5
+ 'border',
6
+ 'brightness',
7
+ 'compression',
8
+ 'contrast',
9
+ 'dpr',
10
+ 'fit',
11
+ 'flip',
12
+ 'format',
13
+ 'gamma',
14
+ 'gravity',
15
+ 'height',
16
+ 'metadata',
17
+ 'quality',
18
+ 'rotate',
19
+ 'saturation',
20
+ 'segment',
21
+ 'sharpen',
22
+ 'trim',
23
+ 'upscale',
24
+ 'width',
25
+ 'zoom'
26
+ ];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { contentAPIAdapter } from './db-content-api/index.js';
2
+ export type { CloudflareImageSizeOptions } from './imageSizeOptions.js';
2
3
  export { figmaContentSystem } from './plugin/adapter.js';
3
4
  export type { FigmaContentSystemOptions } from './plugin/adapter.js';
4
5
  export { buildFigmaConfig } from './plugin/build-config.js';
@@ -1,6 +1,7 @@
1
1
  import type { CollectionConfig, EmailField, TextField } from 'payload';
2
2
  import type { VerifyFunction } from './types.js';
3
3
  export declare const DEFAULT_USER_INFO_COOKIE_NAME = "figma-user-info";
4
+ export declare const FIGMA_OAUTH_CALLBACK_PATH = "/users/sso/login";
4
5
  export declare const defaultScope: string[];
5
6
  export declare const defaultUsernameField: TextField;
6
7
  export declare const defaultVerify: ({ collection, strategyName, userInfoCookieName, usernameField, }: {
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
3
3
  import { hasUserTokenPropsChanged } from './utilities/hasUserTokenPropsChanged.js';
4
4
  import { isDuplicateKeyError } from './utilities/isDuplicateKeyError.js';
5
5
  export const DEFAULT_USER_INFO_COOKIE_NAME = 'figma-user-info';
6
+ export const FIGMA_OAUTH_CALLBACK_PATH = '/users/sso/login';
6
7
  function getFigmaUserInfo(headers, cookieName) {
7
8
  try {
8
9
  const cookies = parseCookies(headers);
@@ -6,7 +6,8 @@ import { createDebugLogger } from '../utilities/createDebugLogger.js';
6
6
  import { establishSession } from '../utilities/establishSession.js';
7
7
  import { exchangeCodeForAccessToken } from '../utilities/exchangeCodeForAccessToken.js';
8
8
  import { extractOrigin } from '../utilities/extractOrigin.js';
9
- import { buildCsrfCookieClearHeader, OAUTH_STATE_CSRF_COOKIE_NAME } from '../utilities/getAuthorizeURL.js';
9
+ import { getAdminCollectionSlug } from '../utilities/getAdminCollectionSlug.js';
10
+ import { buildCsrfCookieClearHeader, getOAuthCallbackPath, OAUTH_STATE_CSRF_COOKIE_NAME } from '../utilities/getAuthorizeURL.js';
10
11
  import { isAbsoluteURL } from '../utilities/isAbsoluteURL.js';
11
12
  export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug, pluginOptions, strategy })=>({
12
13
  handler: async (req)=>{
@@ -151,7 +152,11 @@ export const getLoginEndpoint = ({ collection, collectionOptions, endpointSlug,
151
152
  }
152
153
  const redirectUri = formatAdminURL({
153
154
  apiRoute: config.routes?.api || '/api',
154
- path: `/${collection.slug}/${endpointSlug}/login`,
155
+ path: getOAuthCallbackPath({
156
+ adminCollectionSlug: getAdminCollectionSlug(config),
157
+ collection,
158
+ endpointSlug
159
+ }),
155
160
  serverURL: redirectServerURL
156
161
  });
157
162
  debugLogger.info({
@@ -1,5 +1,5 @@
1
1
  import { fieldAffectsData } from 'payload/shared';
2
- import { defaultVerify } from './defaults.js';
2
+ import { defaultVerify, FIGMA_OAUTH_CALLBACK_PATH } from './defaults.js';
3
3
  import { getLoginEndpoint } from './endpoints/getLoginEndpoint.js';
4
4
  import { getLogoutEndpoint } from './endpoints/getLogoutEndpoint.js';
5
5
  import { getMetaEndpoint } from './endpoints/getMetaEndpoint.js';
@@ -50,6 +50,7 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
50
50
  path: '@payloadcms/figma/client#DefaultLoginButton'
51
51
  };
52
52
  }
53
+ let callbackEndpoint;
53
54
  return {
54
55
  ...config,
55
56
  admin: {
@@ -148,6 +149,20 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
148
149
  usernameField
149
150
  })
150
151
  });
152
+ const loginEndpoint = getLoginEndpoint({
153
+ collection: existingCollection,
154
+ collectionOptions,
155
+ endpointSlug,
156
+ pluginOptions,
157
+ strategy
158
+ });
159
+ const collectionCallbackPath = `/${existingCollection.slug}/${endpointSlug}/login`;
160
+ if (existingCollection.slug === adminCollectionSlug && FIGMA_OAUTH_CALLBACK_PATH !== collectionCallbackPath) {
161
+ callbackEndpoint = {
162
+ ...loginEndpoint,
163
+ path: FIGMA_OAUTH_CALLBACK_PATH
164
+ };
165
+ }
151
166
  const authStrategy = {
152
167
  name: `${existingCollection.slug}-${strategyName}`,
153
168
  // Bind 'this' to ensure the authenticate function has the correct context
@@ -180,13 +195,7 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
180
195
  },
181
196
  endpoints: [
182
197
  ...existingCollection.endpoints || [],
183
- getLoginEndpoint({
184
- collection: existingCollection,
185
- collectionOptions,
186
- endpointSlug,
187
- pluginOptions,
188
- strategy
189
- }),
198
+ loginEndpoint,
190
199
  getMetaEndpoint({
191
200
  collection: existingCollection,
192
201
  collectionOptions,
@@ -237,6 +246,12 @@ export const oAuth2Plugin = (pluginOptions)=>(config)=>{
237
246
  ]
238
247
  }
239
248
  };
240
- })
249
+ }),
250
+ endpoints: [
251
+ ...config.endpoints ?? [],
252
+ ...callbackEndpoint ? [
253
+ callbackEndpoint
254
+ ] : []
255
+ ]
241
256
  };
242
257
  };