@payloadcms/figma 0.0.1-alpha.26 → 0.0.1-alpha.27

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 (41) hide show
  1. package/dist/api/control-plane.d.ts +18 -0
  2. package/dist/api/control-plane.d.ts.map +1 -1
  3. package/dist/api/control-plane.js +27 -0
  4. package/dist/api/control-plane.js.map +1 -1
  5. package/dist/api/figma-api.d.ts.map +1 -1
  6. package/dist/api/figma-api.js +5 -2
  7. package/dist/api/figma-api.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +41 -2
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/config/oauth.js +2 -1
  12. package/dist/config/oauth.js.map +1 -1
  13. package/dist/db-content-api/generated/content-api-types.d.ts +3129 -0
  14. package/dist/db-content-api/generated/content-api-types.d.ts.map +1 -0
  15. package/dist/db-content-api/generated/content-api-types.js +7 -0
  16. package/dist/db-content-api/generated/content-api-types.js.map +1 -0
  17. package/dist/db-content-api/index.d.ts +31 -0
  18. package/dist/db-content-api/index.d.ts.map +1 -0
  19. package/dist/db-content-api/index.js +825 -0
  20. package/dist/db-content-api/index.js.map +1 -0
  21. package/dist/db-content-api/types.d.ts +31 -0
  22. package/dist/db-content-api/types.d.ts.map +1 -0
  23. package/dist/db-content-api/types.js +7 -0
  24. package/dist/db-content-api/types.js.map +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/oauth/endpoints/getLoginEndpoint.js +1 -1
  30. package/dist/oauth/endpoints/getLoginEndpoint.js.map +1 -1
  31. package/dist/oauth/strategy/index.d.ts.map +1 -1
  32. package/dist/oauth/strategy/index.js +4 -0
  33. package/dist/oauth/strategy/index.js.map +1 -1
  34. package/dist/plugin/build-config.d.ts.map +1 -1
  35. package/dist/plugin/build-config.js +35 -17
  36. package/dist/plugin/build-config.js.map +1 -1
  37. package/package.json +3 -1
  38. package/dist/db-adapter.d.ts +0 -17
  39. package/dist/db-adapter.d.ts.map +0 -1
  40. package/dist/db-adapter.js +0 -760
  41. package/dist/db-adapter.js.map +0 -1
@@ -0,0 +1,825 @@
1
+ import { createDatabaseAdapter, traverseFields } from 'payload';
2
+ import { v4 as uuid } from 'uuid';
3
+ import { getValidProjectToken } from '../auth/project-token.js';
4
+ // Field name mapping between Payload and Content API
5
+ // TODO: Update these when Content API renames to internal_id/external_id (or similar)
6
+ const PAYLOAD_ID_FIELD = 'id';
7
+ const CONTENT_API_KEY_FIELD = 'key';
8
+ // Module-level cache for devJwt tokens (encapsulated, not exposed on adapter)
9
+ const devJwtCache = new Map();
10
+ // Private helper to fetch JWT from dev endpoint (not exposed on adapter type)
11
+ async function fetchDevJwt(url, contentSystemId) {
12
+ const usesSuperUser = !contentSystemId || contentSystemId === 'test-system' || contentSystemId === '*';
13
+ const contentSystemIdForJWT = usesSuperUser ? '*' : contentSystemId;
14
+ const response = await fetch(`${url}/dev/jwt`, {
15
+ body: JSON.stringify({
16
+ content_system_id: contentSystemIdForJWT
17
+ }),
18
+ headers: {
19
+ 'Content-Type': 'application/json'
20
+ },
21
+ method: 'POST'
22
+ });
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to get dev JWT: ${response.status} ${response.statusText}`);
25
+ }
26
+ const { token } = await response.json();
27
+ return token;
28
+ }
29
+ async function init() {
30
+ // Log which auth mode is being used
31
+ if (this.auth.mode === 'apiKey') {
32
+ this.payload.logger.info('Using API Key authentication');
33
+ } else if (this.auth.mode === 'tokenStore') {
34
+ this.payload.logger.info('Using TokenStore authentication (local dev)');
35
+ } else {
36
+ this.payload.logger.info('Using Dev JWT authentication (testing)');
37
+ }
38
+ // Drop database if PAYLOAD_DROP_DATABASE is set (for tests)
39
+ if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
40
+ this.payload.logger.info(`---- DROPPING CONTENT API SYSTEM (${this.contentSystemId}) ----`);
41
+ try {
42
+ await this.makeRequest('/dev/clear-db', {
43
+ body: {
44
+ contentSystemId: this.contentSystemId
45
+ },
46
+ method: 'POST'
47
+ });
48
+ this.payload.logger.info('---- DROPPED CONTENT API SYSTEM ----');
49
+ } catch (error) {
50
+ this.payload.logger.warn(`Failed to drop content system: ${error.message}`);
51
+ }
52
+ }
53
+ // Create collections if they don't exist
54
+ for (const collection of this.payload.config.collections){
55
+ await createCollectionIfNotExists.call(this, collection.slug);
56
+ }
57
+ // Create global collections
58
+ for (const global of this.payload.config.globals || []){
59
+ await createCollectionIfNotExists.call(this, `_global-${global.slug}`);
60
+ }
61
+ return;
62
+ }
63
+ // Helper to create a collection if it doesn't exist
64
+ async function createCollectionIfNotExists(collectionKey) {
65
+ try {
66
+ await this.makeRequest('/api/v0/collections', {
67
+ body: {
68
+ name: collectionKey,
69
+ contentSystemId: this.contentSystemId,
70
+ key: collectionKey
71
+ },
72
+ method: 'POST'
73
+ });
74
+ this.payload.logger.info(`Created collection: ${collectionKey}`);
75
+ } catch (error) {
76
+ // If collection already exists, that's fine - ignore the error
77
+ const errorMessage = error.message;
78
+ if (!errorMessage.includes('already exists') && !errorMessage.includes('409')) {
79
+ this.payload.logger.warn(`Failed to create collection ${collectionKey}: ${errorMessage}`);
80
+ }
81
+ }
82
+ }
83
+ // Helper function to make HTTP requests to the Content API
84
+ async function makeRequest(path, options = {}) {
85
+ const { body, method = 'POST', retryCount = 0 } = options;
86
+ // Build auth header based on auth mode
87
+ let authHeader;
88
+ if (this.auth.mode === 'apiKey') {
89
+ authHeader = {
90
+ 'X-Api-Key': this.auth.apiKey
91
+ };
92
+ } else if (this.auth.mode === 'tokenStore') {
93
+ const token = await getValidProjectToken(this.auth.tokenStore, this.contentSystemId);
94
+ if (!token) {
95
+ throw new Error('Authentication required. Run `npx @payloadcms/figma login` to authenticate.');
96
+ }
97
+ authHeader = {
98
+ Authorization: `Bearer ${token}`
99
+ };
100
+ } else {
101
+ // devJwt - inline cache handling
102
+ let token = devJwtCache.get(this.contentSystemId);
103
+ if (!token) {
104
+ token = await fetchDevJwt(this.url, this.contentSystemId);
105
+ devJwtCache.set(this.contentSystemId, token);
106
+ this.payload.logger.info(`Dev JWT acquired for content system: ${this.contentSystemId}`);
107
+ }
108
+ authHeader = {
109
+ Authorization: `Bearer ${token}`
110
+ };
111
+ }
112
+ const requestUrl = `${this.url}${path}`;
113
+ const response = await fetch(requestUrl, {
114
+ body: body ? JSON.stringify(body) : undefined,
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ ...authHeader
118
+ },
119
+ method
120
+ });
121
+ // Read response as text first (better for debugging)
122
+ const text = await response.text();
123
+ // Handle 401 - retry logic differs per mode
124
+ if (response.status === 401 && retryCount === 0 && this.auth.mode === 'devJwt') {
125
+ this.payload.logger.info('Dev JWT expired, refreshing...');
126
+ devJwtCache.delete(this.contentSystemId);
127
+ return this.makeRequest(path, {
128
+ body,
129
+ method,
130
+ retryCount: retryCount + 1
131
+ });
132
+ }
133
+ // tokenStore handles refresh internally via getValidProjectToken
134
+ // apiKey doesn't expire - 401 means invalid key
135
+ // Check HTTP status
136
+ if (!response.ok) {
137
+ this.payload.logger.error({
138
+ msg: `HTTP ${response.status} from ${path}`,
139
+ response: text
140
+ });
141
+ throw new Error(`Content API HTTP ${response.status}: ${text.substring(0, 200)}`);
142
+ }
143
+ // Parse JSON response
144
+ try {
145
+ const parsed = JSON.parse(text);
146
+ // Check if the response is an error object (HTTP 200 but with error in body)
147
+ if (parsed && typeof parsed === 'object' && 'error' in parsed) {
148
+ throw new Error(`Content API error: ${parsed.message || parsed.error}`);
149
+ }
150
+ return parsed;
151
+ } catch (error) {
152
+ // If it's already our custom error, re-throw it
153
+ if (error instanceof Error && error.message.startsWith('Content API error:')) {
154
+ throw error;
155
+ }
156
+ this.payload.logger.error({
157
+ err: error instanceof Error ? error : new Error(String(error)),
158
+ msg: `Failed to parse JSON response from ${path}`,
159
+ response: text
160
+ });
161
+ throw new Error(`Invalid JSON response from content API: ${text.substring(0, 100)}...`);
162
+ }
163
+ }
164
+ // ⚠️ TEMPORARY WORKAROUND - Remove once Content API is fixed
165
+ //
166
+ // TODO: Content API should accept Payload's native Where format instead of requiring conversion.
167
+ // This converter exists because after a GitHub merge, Content API changed to expect a different format.
168
+ //
169
+ // Payload's format: { fieldName: { operator: value }, and: [...], or: [...] }
170
+ // Content API expects: { and: [{ path, operator, value }], or: [{ path, operator, value }] }
171
+ //
172
+ // Once Content API accepts Payload's format:
173
+ // 1. Remove this function entirely
174
+ // 2. Remove all calls to convertPayloadWhereToContentAPI()
175
+ // 3. Pass `where` directly to Content API endpoints
176
+ // 4. Update CONTENT_API_ISSUES.md to mark Workaround #1 as resolved
177
+ function convertPayloadWhereToContentAPI(where, insideLogicalOperator = false) {
178
+ // ⚠️ WORKAROUND: Empty where {} should be { and: [] } not undefined
179
+ // Content API requires a where clause structure even for "no filter"
180
+ if (!where || Object.keys(where).length === 0) {
181
+ return {
182
+ and: []
183
+ };
184
+ }
185
+ const conditions = [];
186
+ for (const [key, value] of Object.entries(where)){
187
+ if (key === 'and') {
188
+ // Recursively convert nested 'and' conditions
189
+ const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, true));
190
+ return {
191
+ and: nestedConditions
192
+ };
193
+ } else if (key === 'or') {
194
+ // Recursively convert nested 'or' conditions
195
+ const nestedConditions = value.map((item)=>convertPayloadWhereToContentAPI(item, true));
196
+ return {
197
+ or: nestedConditions
198
+ };
199
+ } else {
200
+ // Convert field conditions: { fieldName: { operator: value } }
201
+ // to: { path: fieldName, operator, value }
202
+ const operators = value;
203
+ for (const [op, operatorValue] of Object.entries(operators)){
204
+ let finalValue = operatorValue;
205
+ // Add wildcards for contains/like operators (Payload doesn't add them)
206
+ if ((op === 'contains' || op === 'like') && typeof operatorValue === 'string') {
207
+ finalValue = `%${operatorValue}%`;
208
+ }
209
+ conditions.push({
210
+ // Map Payload's 'id' field to Content API's 'key' field
211
+ operator: op,
212
+ path: key === PAYLOAD_ID_FIELD ? CONTENT_API_KEY_FIELD : key,
213
+ value: finalValue
214
+ });
215
+ }
216
+ }
217
+ }
218
+ // If inside a logical operator (and/or), return conditions directly
219
+ // Otherwise, wrap in 'and' to match WhereClause type
220
+ if (insideLogicalOperator && conditions.length === 1) {
221
+ return conditions[0];
222
+ }
223
+ return {
224
+ and: conditions
225
+ };
226
+ }
227
+ // Add fallback sort to ensure consistent ordering when sorting by non-unique fields
228
+ // Matches MongoDB adapter behavior
229
+ function addFallbackSort(sort, collectionConfig) {
230
+ if (!sort || !collectionConfig) {
231
+ return sort;
232
+ }
233
+ const sortArray = Array.isArray(sort) ? sort : [
234
+ sort
235
+ ];
236
+ // Determine fallback sort field
237
+ let fallbackSort = '-id';
238
+ if (collectionConfig.timestamps !== false) {
239
+ fallbackSort = '-createdAt';
240
+ }
241
+ // Check if fallback sort is already included
242
+ const hasFallback = sortArray.some((item)=>item === fallbackSort || item === fallbackSort.replace('-', ''));
243
+ if (hasFallback) {
244
+ return sort;
245
+ }
246
+ // Check if all sort fields are unique (then no fallback needed)
247
+ // For simplicity, we'll always add fallback - checking uniqueness requires field traversal
248
+ // which would be expensive. This matches the conservative approach.
249
+ // Add fallback sort - always return array to preserve multiple sort fields
250
+ return [
251
+ ...sortArray,
252
+ fallbackSort
253
+ ];
254
+ }
255
+ // ⚠️ TEMPORARY WORKAROUND - Remove once Content API is fixed
256
+ //
257
+ // TODO: Content API should accept Payload's native Sort format instead of requiring conversion.
258
+ // This converter exists because Content API expects a different format.
259
+ //
260
+ // Payload's format: "-createdAt" or ["createdAt", "-updatedAt"]
261
+ // Content API expects: [{ path: "createdAt", direction: "dsc" }]
262
+ //
263
+ // Once Content API accepts Payload's format:
264
+ // 1. Remove this function entirely
265
+ // 2. Remove all calls to convertPayloadSortToContentAPI()
266
+ // 3. Pass `sort` directly to Content API endpoints
267
+ function convertPayloadSortToContentAPI(sort) {
268
+ if (!sort) {
269
+ return undefined;
270
+ }
271
+ const sortArray = Array.isArray(sort) ? sort : [
272
+ sort
273
+ ];
274
+ return sortArray.map((field)=>{
275
+ let path = field;
276
+ let direction = 'asc';
277
+ // Check if field starts with '-' for descending
278
+ if (field.startsWith('-')) {
279
+ path = field.substring(1); // Remove the '-'
280
+ direction = 'desc';
281
+ }
282
+ // ⚠️ WORKAROUND: Strip "version." prefix from field names
283
+ // Payload sends "-version.createdAt" for version sort fields
284
+ // but Content API expects just "createdAt"
285
+ // TODO: Content API should handle version field paths correctly
286
+ if (path.startsWith('version.')) {
287
+ path = path.substring(8); // Remove "version." prefix
288
+ }
289
+ return {
290
+ direction,
291
+ path
292
+ };
293
+ });
294
+ }
295
+ /**
296
+ * Transform data before sending to Content API (WRITE operations)
297
+ *
298
+ * Conversions applied:
299
+ * - RichText fields: Objects -> JSON strings
300
+ * - Date fields: Date objects -> Unix timestamps (numbers)
301
+ */ function dataToContentAPI(collectionSlug, data) {
302
+ if (!data || typeof data !== 'object') {
303
+ return data;
304
+ }
305
+ // Deep clone to avoid mutating original data
306
+ const transformed = JSON.parse(JSON.stringify(data));
307
+ // Get collection config
308
+ const isGlobal = collectionSlug.startsWith('_global-');
309
+ const actualSlug = isGlobal ? collectionSlug.substring(8) : collectionSlug;
310
+ const collectionConfig = isGlobal ? this.payload.config.globals?.find((g)=>g.slug === actualSlug) : this.payload.config.collections.find((c)=>c.slug === actualSlug);
311
+ if (!collectionConfig?.fields) {
312
+ return transformed;
313
+ }
314
+ // Use Payload's traverseFields to iterate over all fields
315
+ const callback = ({ field, ref })=>{
316
+ if (!('name' in field) || !field.name) {
317
+ return;
318
+ }
319
+ if (!ref || typeof ref !== 'object') {
320
+ return;
321
+ }
322
+ const current = ref;
323
+ const value = current[field.name];
324
+ if (value !== null && value !== undefined) {
325
+ // RichText: object -> JSON string
326
+ if (field.type === 'richText' && typeof value !== 'string') {
327
+ current[field.name] = JSON.stringify(value);
328
+ } else if (field.type === 'date') {
329
+ const dateValue = value instanceof Date ? value : new Date(value);
330
+ if (!isNaN(dateValue.getTime())) {
331
+ current[field.name] = dateValue.getTime();
332
+ }
333
+ }
334
+ }
335
+ };
336
+ traverseFields({
337
+ callback,
338
+ fields: collectionConfig.fields,
339
+ ref: transformed
340
+ });
341
+ return transformed;
342
+ }
343
+ /**
344
+ * Transform data received from Content API (READ operations)
345
+ *
346
+ * Conversions applied:
347
+ * - RichText fields: JSON strings -> Objects
348
+ * - Date fields: Unix timestamps (numbers) -> Date objects
349
+ */ function dataFromContentAPI(collectionSlug, data) {
350
+ if (!data || typeof data !== 'object') {
351
+ return data;
352
+ }
353
+ // Deep clone to avoid mutating original data
354
+ const transformed = JSON.parse(JSON.stringify(data));
355
+ // Get collection config
356
+ const isGlobal = collectionSlug.startsWith('_global-');
357
+ const actualSlug = isGlobal ? collectionSlug.substring(8) : collectionSlug;
358
+ const collectionConfig = isGlobal ? this.payload.config.globals?.find((g)=>g.slug === actualSlug) : this.payload.config.collections.find((c)=>c.slug === actualSlug);
359
+ if (!collectionConfig?.fields) {
360
+ return transformed;
361
+ }
362
+ // Use Payload's traverseFields to iterate over all fields
363
+ const callback = ({ field, parentPath, ref })=>{
364
+ if (!('name' in field) || !field.name) {
365
+ return;
366
+ }
367
+ if (!ref || typeof ref !== 'object') {
368
+ return;
369
+ }
370
+ const current = ref;
371
+ const value = current[field.name];
372
+ if (value !== null && value !== undefined) {
373
+ // RichText: JSON string -> object
374
+ if (field.type === 'richText' && typeof value === 'string') {
375
+ try {
376
+ current[field.name] = JSON.parse(value);
377
+ } catch (error) {
378
+ const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
379
+ this.payload.logger.warn({
380
+ err: error instanceof Error ? error : new Error(String(error)),
381
+ msg: `Failed to parse richtext field '${fieldPath}' in collection '${collectionSlug}'`
382
+ });
383
+ }
384
+ } else if (field.type === 'date') {
385
+ current[field.name] = new Date(value);
386
+ }
387
+ }
388
+ };
389
+ traverseFields({
390
+ callback,
391
+ fields: collectionConfig.fields,
392
+ ref: transformed
393
+ });
394
+ return transformed;
395
+ }
396
+ // Helper to unwrap Content API document format to Payload format
397
+ // Content API returns: { id, data: { ...fields }, createdAt, updatedAt }
398
+ // Payload expects: { id, ...fields, createdAt, updatedAt }
399
+ function unwrapDocument(doc, collectionSlug) {
400
+ if (!doc) {
401
+ return doc;
402
+ }
403
+ // TODO
404
+ // Document has 'key', DocumentVersion has 'documentId'
405
+ const docId = CONTENT_API_KEY_FIELD in doc ? doc[CONTENT_API_KEY_FIELD] : doc.documentId;
406
+ const baseDoc = {
407
+ ...doc.data,
408
+ createdAt: doc.createdAt,
409
+ [PAYLOAD_ID_FIELD]: docId,
410
+ updatedAt: doc.updatedAt
411
+ };
412
+ // Transform data from Content API format to Payload format
413
+ if (collectionSlug) {
414
+ return dataFromContentAPI.call(this, collectionSlug, baseDoc);
415
+ }
416
+ return baseDoc;
417
+ }
418
+ const findMany = async function findMany({ collectionSlug, limit, page, pagination, skip, sort, where }) {
419
+ // Payload semantics: limit: 0 means "no limit" (get all documents)
420
+ // Pass this through to Content API which follows the same convention
421
+ const effectiveLimit = limit;
422
+ const offset = skip ?? (limit === 0 ? 0 : (page - 1) * limit);
423
+ // Add fallback sort to ensure consistent ordering (matching MongoDB behavior)
424
+ const collectionConfig = this.payload.config.collections.find((c)=>c.slug === collectionSlug);
425
+ const sortWithFallback = addFallbackSort(sort, collectionConfig);
426
+ const response = await this.makeRequest('/api/v0/documents:find', {
427
+ body: {
428
+ collectionKey: collectionSlug,
429
+ contentSystemId: this.contentSystemId,
430
+ limit: effectiveLimit,
431
+ offset,
432
+ sort: convertPayloadSortToContentAPI(sortWithFallback),
433
+ where: convertPayloadWhereToContentAPI(where)
434
+ },
435
+ method: 'POST'
436
+ });
437
+ return {
438
+ docs: response.result.data.map((doc)=>unwrapDocument.call(this, doc, collectionSlug)),
439
+ hasNextPage: pagination !== false && limit > 0 && response.result.pagination.total > offset + response.result.data.length,
440
+ hasPrevPage: pagination !== false && offset > 0,
441
+ limit,
442
+ nextPage: pagination !== false && response.result.pagination.total > offset + response.result.data.length ? page + 1 : null,
443
+ page,
444
+ pagingCounter: offset + 1,
445
+ prevPage: pagination !== false && offset > 0 ? page - 1 : null,
446
+ totalDocs: response.result.pagination.total,
447
+ totalPages: limit > 0 ? Math.ceil(response.result.pagination.total / limit) : 1
448
+ };
449
+ };
450
+ const find = async function find({ collection: collectionSlug, limit = 0, page = 1, pagination, skip, sort, where }) {
451
+ // Apply defaultSort from collection config if no sort is provided
452
+ const collectionConfig = this.payload.config.collections.find((c)=>c.slug === collectionSlug);
453
+ const effectiveSort = sort || collectionConfig?.defaultSort;
454
+ return findMany.call(this, {
455
+ collectionSlug,
456
+ limit,
457
+ page,
458
+ pagination,
459
+ skip,
460
+ sort: effectiveSort,
461
+ where: where ?? {}
462
+ });
463
+ };
464
+ const findVersions = async function findVersions({ collection: collectionSlug, limit = 0, page = 1, pagination, skip, sort, where }) {
465
+ const offset = skip ?? (page - 1) * limit;
466
+ const response = await this.makeRequest('/api/v0/document_versions:find', {
467
+ body: {
468
+ collectionKey: collectionSlug,
469
+ contentSystemId: this.contentSystemId,
470
+ limit,
471
+ offset,
472
+ sort: convertPayloadSortToContentAPI(sort),
473
+ where: convertPayloadWhereToContentAPI(where ?? {})
474
+ },
475
+ method: 'POST'
476
+ });
477
+ return {
478
+ docs: response.result.data.map((doc)=>unwrapDocument.call(this, doc, collectionSlug)),
479
+ hasNextPage: pagination !== false && limit > 0 && response.result.pagination.total > offset + response.result.data.length,
480
+ hasPrevPage: pagination !== false && offset > 0,
481
+ limit,
482
+ nextPage: pagination !== false && response.result.pagination.total > offset + response.result.data.length ? page + 1 : null,
483
+ page,
484
+ pagingCounter: offset + 1,
485
+ prevPage: pagination !== false && offset > 0 ? page - 1 : null,
486
+ totalDocs: response.result.pagination.total,
487
+ totalPages: limit > 0 ? Math.ceil(response.result.pagination.total / limit) : 1
488
+ };
489
+ };
490
+ const queryDrafts = async function queryDrafts({ collection: collectionSlug, limit, page, pagination, sort, where = {} }) {
491
+ // TODO: review this
492
+ // Content API doesn't have a separate "draft" concept
493
+ // It only has versions with a "latest" flag
494
+ // PayloadCMS drafts would typically be versions where latest=false
495
+ // But this mapping may need adjustment based on your use case
496
+ const result = await this.findVersions({
497
+ collection: collectionSlug,
498
+ limit,
499
+ page,
500
+ pagination,
501
+ sort,
502
+ where: {
503
+ ...where,
504
+ // Query non-latest versions as "drafts"
505
+ latest: {
506
+ equals: false
507
+ }
508
+ }
509
+ });
510
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
511
+ return result;
512
+ };
513
+ const createVersion = async function createVersion({ autosave, collectionSlug, parent, versionData }) {
514
+ const response = await this.makeRequest('/api/v0/document_versions:create', {
515
+ body: {
516
+ collectionKey: collectionSlug,
517
+ contentSystemId: this.contentSystemId,
518
+ data: dataToContentAPI.call(this, collectionSlug, versionData),
519
+ documentKey: parent,
520
+ latest: !autosave
521
+ },
522
+ method: 'POST'
523
+ });
524
+ // Handle union type response: { id: string } | { data: DocumentVersion }
525
+ if ('data' in response.result) {
526
+ return unwrapDocument.call(this, response.result.data, collectionSlug);
527
+ }
528
+ throw new Error('Unexpected response format from createVersion');
529
+ };
530
+ const updateVersion = async function updateVersion({ id, collection: collectionSlug, versionData, where }) {
531
+ const response = await this.makeRequest('/api/v0/document_versions:update', {
532
+ body: {
533
+ collectionKey: collectionSlug,
534
+ contentSystemId: this.contentSystemId,
535
+ createOnMissing: false,
536
+ data: dataToContentAPI.call(this, collectionSlug, versionData),
537
+ where: convertPayloadWhereToContentAPI(where ?? {
538
+ id: {
539
+ equals: id
540
+ }
541
+ })
542
+ },
543
+ method: 'POST'
544
+ });
545
+ // Handle union type response: { count: number } | { data: DocumentVersion[] }
546
+ if (!response?.result || !('data' in response.result)) {
547
+ throw new Error('No document data in updateVersion response');
548
+ }
549
+ return unwrapDocument.call(this, response.result.data[0], collectionSlug);
550
+ };
551
+ const deleteVersions = async function deleteVersions({ collection: collectionSlug, where }) {
552
+ await this.makeRequest('/api/v0/document_versions:delete', {
553
+ body: {
554
+ collectionKey: collectionSlug,
555
+ contentSystemId: this.contentSystemId,
556
+ returning: false,
557
+ where: convertPayloadWhereToContentAPI(where)
558
+ },
559
+ method: 'POST'
560
+ });
561
+ };
562
+ const findOne = async function findOne({ collection, where }) {
563
+ const { docs: [first] } = await this.find({
564
+ collection,
565
+ limit: 1,
566
+ pagination: false,
567
+ where
568
+ });
569
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
570
+ return first ?? null;
571
+ };
572
+ const updateMany = async function updateMany({ collection: collectionSlug, data, where }) {
573
+ const response = await this.makeRequest('/api/v0/documents:update', {
574
+ body: {
575
+ collectionKey: collectionSlug,
576
+ contentSystemId: this.contentSystemId,
577
+ createOnMissing: false,
578
+ data: dataToContentAPI.call(this, collectionSlug, data),
579
+ returning: {},
580
+ where: convertPayloadWhereToContentAPI(where)
581
+ },
582
+ method: 'POST'
583
+ });
584
+ // Handle union type: { count: number } | { data: Document[] }
585
+ if (response.result && 'data' in response.result) {
586
+ return response.result.data.map((doc)=>unwrapDocument.call(this, doc, collectionSlug));
587
+ }
588
+ return null;
589
+ };
590
+ const updateOne = async function updateOne({ id, collection: collectionSlug, data, where }) {
591
+ const response = await this.makeRequest('/api/v0/documents:update', {
592
+ body: {
593
+ collectionKey: collectionSlug,
594
+ contentSystemId: this.contentSystemId,
595
+ createOnMissing: false,
596
+ data: dataToContentAPI.call(this, collectionSlug, data),
597
+ where: convertPayloadWhereToContentAPI(where ?? {
598
+ id: {
599
+ equals: id
600
+ }
601
+ })
602
+ },
603
+ method: 'POST'
604
+ });
605
+ // Handle union type: { count: number } | { data: Document[] }
606
+ if (response.result && 'data' in response.result) {
607
+ const doc = response.result.data[0];
608
+ if (!doc) {
609
+ throw new Error('No document data in updateOne response');
610
+ }
611
+ return unwrapDocument.call(this, doc, collectionSlug);
612
+ }
613
+ throw new Error('Unexpected response format from updateOne');
614
+ };
615
+ const deleteMany = async function deleteMany({ collection: collectionSlug, where }) {
616
+ await this.makeRequest('/api/v0/documents:delete', {
617
+ body: {
618
+ collectionKey: collectionSlug,
619
+ contentSystemId: this.contentSystemId,
620
+ returning: false,
621
+ where: convertPayloadWhereToContentAPI(where)
622
+ },
623
+ method: 'POST'
624
+ });
625
+ };
626
+ const deleteOne = async function deleteOne({ collection: collectionSlug, where }) {
627
+ await this.makeRequest('/api/v0/documents:delete', {
628
+ body: {
629
+ collectionKey: collectionSlug,
630
+ contentSystemId: this.contentSystemId,
631
+ returning: false,
632
+ where: convertPayloadWhereToContentAPI(where)
633
+ },
634
+ method: 'POST'
635
+ });
636
+ };
637
+ const create = async function create({ collection: collectionSlug, data }) {
638
+ // Generate a document key if not provided
639
+ // Content API has two ID fields:
640
+ // - `id` (UUID, auto-generated by DB, internal use only)
641
+ // - `key` (text, public document identifier, maps to Payload's `id`)
642
+ // If Payload doesn't provide an ID, we generate a UUID
643
+ // (same as other SQL-based Payload adapters like db-postgres and db-drizzle)
644
+ const key = data[PAYLOAD_ID_FIELD] || uuid();
645
+ const response = await this.makeRequest('/api/v0/documents:create', {
646
+ body: {
647
+ collectionKey: collectionSlug,
648
+ contentSystemId: this.contentSystemId,
649
+ data: dataToContentAPI.call(this, collectionSlug, data),
650
+ key
651
+ },
652
+ method: 'POST'
653
+ });
654
+ // Handle union type response: { id: string } | { data: Document }
655
+ if ('data' in response.result) {
656
+ return unwrapDocument.call(this, response.result.data, collectionSlug);
657
+ }
658
+ throw new Error('Unexpected response format from create');
659
+ };
660
+ const count = async function count({ collection: collectionSlug, where }) {
661
+ const response = await this.makeRequest('/api/v0/documents:count', {
662
+ body: {
663
+ collectionKey: collectionSlug,
664
+ contentSystemId: this.contentSystemId,
665
+ where: convertPayloadWhereToContentAPI(where)
666
+ },
667
+ method: 'POST'
668
+ });
669
+ return {
670
+ totalDocs: response.result.count
671
+ };
672
+ };
673
+ const countVersions = async function countVersions({ collection: collectionSlug, where }) {
674
+ const response = await this.makeRequest('/api/v0/document_versions:count', {
675
+ body: {
676
+ collectionKey: collectionSlug,
677
+ contentSystemId: this.contentSystemId,
678
+ where: convertPayloadWhereToContentAPI(where)
679
+ },
680
+ method: 'POST'
681
+ });
682
+ return {
683
+ totalDocs: response.result.count
684
+ };
685
+ };
686
+ const upsert = async function upsert({ collection: collectionSlug, data, where }) {
687
+ const response = await this.makeRequest('/api/v0/documents:update', {
688
+ body: {
689
+ collectionKey: collectionSlug,
690
+ contentSystemId: this.contentSystemId,
691
+ createOnMissing: true,
692
+ data: dataToContentAPI.call(this, collectionSlug, data),
693
+ where: convertPayloadWhereToContentAPI(where)
694
+ },
695
+ method: 'POST'
696
+ });
697
+ // Handle union type: { count: number } | { data: Document[] }
698
+ if (response.result && 'data' in response.result) {
699
+ const doc = response.result.data[0];
700
+ if (!doc) {
701
+ throw new Error('No document data in upsert response');
702
+ }
703
+ return unwrapDocument.call(this, doc, collectionSlug);
704
+ }
705
+ throw new Error('Unexpected response format from upsert');
706
+ };
707
+ // TODO: global should be a prefix or a resource in the REST API / Table in the DB?
708
+ const getGlobalSlug = (slug)=>`_global-${slug}`;
709
+ const createGlobal = function({ slug, data }) {
710
+ return this.create({
711
+ collection: getGlobalSlug(slug),
712
+ data: {
713
+ ...data,
714
+ globalType: slug
715
+ }
716
+ });
717
+ };
718
+ const findGlobal = function({ slug, where = {} }) {
719
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
720
+ return this.findOne({
721
+ collection: getGlobalSlug(slug),
722
+ where
723
+ });
724
+ };
725
+ const updateGlobal = function({ slug, data }) {
726
+ return this.updateOne({
727
+ collection: getGlobalSlug(slug),
728
+ data,
729
+ where: {}
730
+ });
731
+ };
732
+ const findGlobalVersions = function({ global: slug, limit, page, pagination, skip, sort, where }) {
733
+ return this.findVersions({
734
+ collection: getGlobalSlug(slug),
735
+ limit,
736
+ page,
737
+ pagination,
738
+ skip,
739
+ sort,
740
+ where
741
+ });
742
+ };
743
+ const createGlobalVersion = function(// eslint-disable-next-line @typescript-eslint/no-explicit-any
744
+ { autosave, createdAt, globalSlug, updatedAt, versionData, ...rest }) {
745
+ return this.createVersion({
746
+ autosave,
747
+ collectionSlug: getGlobalSlug(globalSlug),
748
+ createdAt,
749
+ parent: rest.parent,
750
+ updatedAt,
751
+ versionData
752
+ });
753
+ };
754
+ const updateGlobalVersion = function({ id, global: slug, versionData, where, ...rest }) {
755
+ // UpdateVersion accepts either id OR where, not both
756
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
757
+ const args = {
758
+ collection: getGlobalSlug(slug),
759
+ versionData,
760
+ ...rest
761
+ };
762
+ if (id !== undefined) {
763
+ args.id = id;
764
+ } else if (where !== undefined) {
765
+ args.where = where;
766
+ }
767
+ return this.updateVersion(args);
768
+ };
769
+ const countGlobalVersions = function({ global: slug, where }) {
770
+ return this.countVersions({
771
+ collection: getGlobalSlug(slug),
772
+ where
773
+ });
774
+ };
775
+ export const contentAPIAdapter = (opts)=>{
776
+ return {
777
+ name: 'content_api',
778
+ defaultIDType: 'text',
779
+ init: ({ payload })=>{
780
+ return createDatabaseAdapter({
781
+ name: 'content_api',
782
+ auth: opts.auth,
783
+ beginTransaction: ()=>{
784
+ return Promise.resolve('no-op-transaction');
785
+ },
786
+ commitTransaction: async ()=>{},
787
+ contentSystemId: opts.contentSystemId,
788
+ count,
789
+ countGlobalVersions,
790
+ countVersions,
791
+ create,
792
+ createGlobal,
793
+ createGlobalVersion,
794
+ createVersion,
795
+ defaultIDType: 'text',
796
+ deleteMany,
797
+ deleteOne,
798
+ deleteVersions,
799
+ find,
800
+ findDistinct: ()=>{
801
+ return Promise.reject(new Error('findDistinct is not yet implemented for Content API adapter'));
802
+ },
803
+ findGlobal,
804
+ findGlobalVersions,
805
+ findOne,
806
+ findVersions,
807
+ init,
808
+ makeRequest,
809
+ packageName: '@payloadcms/db-content-api',
810
+ payload,
811
+ queryDrafts,
812
+ rollbackTransaction: async ()=>{},
813
+ updateGlobal,
814
+ updateGlobalVersion,
815
+ updateMany,
816
+ updateOne,
817
+ updateVersion,
818
+ upsert,
819
+ url: opts.url
820
+ });
821
+ }
822
+ };
823
+ };
824
+
825
+ //# sourceMappingURL=index.js.map