@sequoialabs/payload-plugin-reversia 0.2.1 → 0.3.0

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.
@@ -1,6 +1,6 @@
1
1
  import { unauthorizedResponse, validateApiKey } from '../utils/auth';
2
2
  import { findLocalizedFields, serializeField } from '../utils/fields';
3
- import { resolveDefaultLocale } from '../utils/payload-helpers';
3
+ import { resolveDefaultLocale, shouldUseDrafts } from '../utils/payload-helpers';
4
4
  function extract(doc, fields) {
5
5
  const content = {};
6
6
  const contentTypes = {};
@@ -45,7 +45,11 @@ export function createResourceEndpoint(pluginConfig, collectionsMap, globalsMap)
45
45
  return Response.json({ error: `Global ${globalSlug} not found` }, { status: 404 });
46
46
  }
47
47
  const localizedFields = findLocalizedFields(globalConfig.fields);
48
- const doc = await req.payload.findGlobal({ slug: globalSlug, locale: defaultLocale });
48
+ const doc = await req.payload.findGlobal({
49
+ slug: globalSlug,
50
+ locale: defaultLocale,
51
+ draft: shouldUseDrafts(pluginConfig, 'global', globalConfig),
52
+ });
49
53
  const { content, contentTypes } = extract(doc, localizedFields);
50
54
  return Response.json({
51
55
  id: globalSlug,
@@ -69,6 +73,7 @@ export function createResourceEndpoint(pluginConfig, collectionsMap, globalsMap)
69
73
  collection: slug,
70
74
  id: resourceId,
71
75
  locale: defaultLocale,
76
+ draft: shouldUseDrafts(pluginConfig, 'collection', collection),
72
77
  });
73
78
  const { content, contentTypes } = extract(doc, localizedFields);
74
79
  return Response.json({
@@ -1,5 +1,6 @@
1
1
  import { unauthorizedResponse, validateApiKey } from '../utils/auth';
2
2
  import { deflatePopulatedRelationships, deserializeFieldValue, findLocalizedFields, } from '../utils/fields';
3
+ import { shouldUseDrafts } from '../utils/payload-helpers';
3
4
  const WRITE_CONFLICT_MAX_RETRIES = 3;
4
5
  const WRITE_CONFLICT_BASE_DELAY_MS = 50;
5
6
  function isWriteConflict(error) {
@@ -156,7 +157,7 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
156
157
  * manually later.
157
158
  */
158
159
  async function dropUniqueCollisions(params) {
159
- const { payload, collection, id, locale, fields, updateData, acceptedFields, diff } = params;
160
+ const { payload, collection, id, locale, draft, fields, updateData, acceptedFields, diff } = params;
160
161
  const dropped = [];
161
162
  for (const field of fields) {
162
163
  if (field.isContainer || !field.unique) {
@@ -175,6 +176,7 @@ async function dropUniqueCollisions(params) {
175
176
  depth: 0,
176
177
  limit: 1,
177
178
  pagination: false,
179
+ draft,
178
180
  where: {
179
181
  and: [{ [field.name]: { equals: value } }, { id: { not_equals: id } }],
180
182
  },
@@ -241,16 +243,26 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
241
243
  continue;
242
244
  }
243
245
  const allowedFields = findLocalizedFields(globalConfig.fields);
246
+ // When draft-aware, read the source and previous values from the
247
+ // latest draft and save the translation as a draft too, so nothing
248
+ // reaches the published global until an editor publishes it.
249
+ const draft = shouldUseDrafts(pluginConfig, 'global', globalConfig);
244
250
  // depth: 0 is critical — without it, Payload populates relationship
245
251
  // and upload fields as full objects instead of raw IDs. Our clone
246
252
  // would then write those objects back, which Payload's validator
247
253
  // rejects with "invalid relationships: [object Object]".
248
254
  const [sourceDoc, previousDoc] = await Promise.all([
249
- req.payload.findGlobal({ slug: globalSlug, locale: item.sourceLocale, depth: 0 }),
255
+ req.payload.findGlobal({
256
+ slug: globalSlug,
257
+ locale: item.sourceLocale,
258
+ depth: 0,
259
+ draft,
260
+ }),
250
261
  req.payload.findGlobal({
251
262
  slug: globalSlug,
252
263
  locale: item.targetLocale,
253
264
  depth: 0,
265
+ draft,
254
266
  fallbackLocale: false,
255
267
  }),
256
268
  ]);
@@ -268,6 +280,7 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
268
280
  slug: globalSlug,
269
281
  locale: item.targetLocale,
270
282
  data: updateData,
283
+ draft,
271
284
  context: { reversiaInsertion: true },
272
285
  }));
273
286
  response[index] = { index, type: item.type, id: globalSlug, diff };
@@ -289,18 +302,22 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
289
302
  }
290
303
  const itemId = item.id;
291
304
  const allowedFields = findLocalizedFields(collection.fields);
305
+ // See the global branch above: draft-aware reads + draft write.
306
+ const draft = shouldUseDrafts(pluginConfig, 'collection', collection);
292
307
  const [sourceDoc, previousDoc] = await Promise.all([
293
308
  req.payload.findByID({
294
309
  collection: slug,
295
310
  id: item.id,
296
311
  locale: item.sourceLocale,
297
312
  depth: 0,
313
+ draft,
298
314
  }),
299
315
  req.payload.findByID({
300
316
  collection: slug,
301
317
  id: item.id,
302
318
  locale: item.targetLocale,
303
319
  depth: 0,
320
+ draft,
304
321
  fallbackLocale: false,
305
322
  }),
306
323
  ]);
@@ -319,6 +336,7 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
319
336
  collection: slug,
320
337
  id: itemId,
321
338
  locale: item.targetLocale,
339
+ draft,
322
340
  fields: allowedFields,
323
341
  updateData,
324
342
  acceptedFields,
@@ -333,6 +351,7 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
333
351
  id: itemId,
334
352
  locale: item.targetLocale,
335
353
  data: updateData,
354
+ draft,
336
355
  context: { reversiaInsertion: true },
337
356
  }));
338
357
  response[index] = { index, type: item.type, id: itemId, diff };
@@ -1,7 +1,7 @@
1
1
  import { unauthorizedResponse, validateApiKey } from '../utils/auth';
2
2
  import { decodeCursor, encodeCursor } from '../utils/cursor';
3
3
  import { findLocalizedFields, serializeField } from '../utils/fields';
4
- import { parseLimit, resolveDefaultLocale } from '../utils/payload-helpers';
4
+ import { parseLimit, resolveDefaultLocale, shouldUseDrafts } from '../utils/payload-helpers';
5
5
  function extractContent(doc, fields) {
6
6
  const content = {};
7
7
  const contentTypes = {};
@@ -68,12 +68,17 @@ export function createResourcesEndpoint(pluginConfig, collectionsMap, globalsMap
68
68
  if (cursor && cursor.type === resourceType && cursor.id) {
69
69
  where.id = { greater_than: cursor.id };
70
70
  }
71
+ // With `draft: true` Payload queries the versions table and returns
72
+ // the latest version per document (draft or published). It rewrites
73
+ // `id` in `where`/`sort` to the version's `parent`, so the cursor keeps
74
+ // pointing at document ids.
71
75
  const docs = await req.payload.find({
72
76
  collection: slug,
73
77
  locale: defaultLocale,
74
78
  limit: limit - totalFetched,
75
79
  sort: 'id',
76
80
  where,
81
+ draft: shouldUseDrafts(pluginConfig, 'collection', collection),
77
82
  });
78
83
  if (docs.docs.length === 0) {
79
84
  continue;
@@ -123,7 +128,11 @@ export function createResourcesEndpoint(pluginConfig, collectionsMap, globalsMap
123
128
  if (localizedFields.length === 0) {
124
129
  continue;
125
130
  }
126
- const doc = await req.payload.findGlobal({ slug, locale: defaultLocale });
131
+ const doc = await req.payload.findGlobal({
132
+ slug,
133
+ locale: defaultLocale,
134
+ draft: shouldUseDrafts(pluginConfig, 'global', global),
135
+ });
127
136
  const { content, contentTypes } = extractContent(doc, localizedFields);
128
137
  if (Object.keys(content).length === 0) {
129
138
  continue;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Config } from 'payload';
2
2
  import type { ReversiaPluginConfig } from './types';
3
3
  export type { TriggerCrawlOptions, TriggerCrawlResult } from './trigger-crawl';
4
4
  export { triggerCrawl } from './trigger-crawl';
5
- export type { ConfirmResourcesSyncResponse, InsertionRequest, InsertionResponse, ResourceDefinition, ResourceItem, ResourceResponse, ReversiaErrorResponse, ReversiaFieldCustom, ReversiaPluginConfig, SettingsResponse, StreamResponse, TranslatableFieldConfig, } from './types';
5
+ export type { ConfirmResourcesSyncResponse, EnabledResource, EnabledResourceOptions, InsertionRequest, InsertionResponse, ResourceDefinition, ResourceItem, ResourceResponse, ReversiaErrorResponse, ReversiaFieldCustom, ReversiaPluginConfig, SettingsResponse, StreamResponse, TranslatableFieldConfig, } from './types';
6
6
  export { ReversiaFieldBehavior, ReversiaFieldType } from './types';
7
7
  export declare const reversiaPlugin: (pluginConfig: ReversiaPluginConfig) => (config: Config) => Config;
8
8
  export default reversiaPlugin;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { createSettingsEndpoint } from './endpoints/settings';
9
9
  import { createTriggerCrawlDashboardEndpoint } from './endpoints/trigger-crawl-dashboard';
10
10
  import { createAfterChangeHook } from './hooks/after-change';
11
11
  import { findLocalizedFields } from './utils/fields';
12
+ import { resourceSlug } from './utils/payload-helpers';
12
13
  export { triggerCrawl } from './trigger-crawl';
13
14
  export { ReversiaFieldBehavior, ReversiaFieldType } from './types';
14
15
  const APPLIED_MARKER = Symbol.for('payload-plugin-reversia.applied');
@@ -25,10 +26,10 @@ export const reversiaPlugin = (pluginConfig) => (config) => {
25
26
  }
26
27
  marked[APPLIED_MARKER] = true;
27
28
  const enabledCollectionSlugs = pluginConfig.enabledCollections
28
- ? new Set(pluginConfig.enabledCollections.map((s) => String(s)))
29
+ ? new Set(pluginConfig.enabledCollections.map(resourceSlug))
29
30
  : null;
30
31
  const enabledGlobalSlugs = pluginConfig.enabledGlobals
31
- ? new Set(pluginConfig.enabledGlobals)
32
+ ? new Set(pluginConfig.enabledGlobals.map(resourceSlug))
32
33
  : null;
33
34
  const collectionsMap = new Map();
34
35
  const globalsMap = new Map();
package/dist/types.d.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  import type { CollectionSlug } from 'payload';
2
2
  import type { LeafSegment } from './utils/path-resolver';
3
+ /**
4
+ * An entry of `enabledCollections` / `enabledGlobals`: either the bare slug,
5
+ * or an object carrying per-resource options.
6
+ */
7
+ export interface EnabledResourceOptions<Slug extends string = string> {
8
+ slug: Slug;
9
+ /**
10
+ * Per-resource override of the root `useDrafts` flag. Only meaningful when
11
+ * the collection or global enables `versions.drafts`.
12
+ */
13
+ useDrafts?: boolean;
14
+ }
15
+ export type EnabledResource<Slug extends string = string> = EnabledResourceOptions<Slug> | Slug;
3
16
  export interface ReversiaPluginConfig {
4
17
  /**
5
18
  * API key used by Reversia SaaS to authenticate requests.
@@ -9,13 +22,19 @@ export interface ReversiaPluginConfig {
9
22
  /**
10
23
  * Optional: restrict which collections are exposed to Reversia.
11
24
  * If omitted, all collections with localized fields are exposed.
25
+ *
26
+ * Each entry is a slug, or `{ slug, useDrafts }` to override the root
27
+ * `useDrafts` flag for that collection.
12
28
  */
13
- enabledCollections?: CollectionSlug[];
29
+ enabledCollections?: EnabledResource<CollectionSlug>[];
14
30
  /**
15
31
  * Optional: restrict which globals are exposed to Reversia.
16
32
  * If omitted, all globals with localized fields are exposed.
33
+ *
34
+ * Each entry is a slug, or `{ slug, useDrafts }` to override the root
35
+ * `useDrafts` flag for that global.
17
36
  */
18
- enabledGlobals?: string[];
37
+ enabledGlobals?: EnabledResource[];
19
38
  /**
20
39
  * Whether the plugin is disabled. Defaults to false.
21
40
  */
@@ -26,6 +45,27 @@ export interface ReversiaPluginConfig {
26
45
  * built-in production URL.
27
46
  */
28
47
  baseUrl?: string;
48
+ /**
49
+ * Make the plugin draft-aware for collections and globals that enable
50
+ * `versions.drafts`. Defaults to false. Can be overridden per resource via
51
+ * `{ slug, useDrafts }` entries in `enabledCollections` / `enabledGlobals`.
52
+ *
53
+ * When enabled, for those entities:
54
+ * - `GET /reversia/resources` and `GET /reversia/resource` return the latest
55
+ * draft (falling back to the published document when no newer draft
56
+ * exists), so Reversia translates what editors are currently working on.
57
+ * - `PUT /reversia/resources-insert` reads the source locale from the draft
58
+ * and saves the translation as a new draft version. The published document
59
+ * is never touched — publishing stays an editorial action in Payload.
60
+ *
61
+ * Entities without drafts are unaffected either way.
62
+ *
63
+ * When disabled (the default), every read and write targets the published
64
+ * document. Note that Payload bases a non-draft update on the latest
65
+ * version, so on a draft-enabled entity an insertion also publishes any
66
+ * pending source-locale draft — enable this flag if that is not desired.
67
+ */
68
+ useDrafts?: boolean;
29
69
  }
30
70
  export declare enum ReversiaFieldType {
31
71
  TEXT = "TEXT",
@@ -1,4 +1,5 @@
1
- import type { PayloadRequest } from 'payload';
1
+ import type { CollectionConfig, GlobalConfig, PayloadRequest } from 'payload';
2
+ import type { EnabledResource, ReversiaPluginConfig } from '../types';
2
3
  /**
3
4
  * Parses a caller-supplied `limit` query parameter and clamps it to a safe
4
5
  * range. NaN, zero, and negatives fall back to the default.
@@ -9,6 +10,29 @@ export declare function parseLimit(raw: string | null, fallback?: number): numbe
9
10
  * when localization is not configured.
10
11
  */
11
12
  export declare function resolveDefaultLocale(req: PayloadRequest): string;
13
+ /**
14
+ * True when a collection or global enables `versions.drafts`. Accepts both
15
+ * the raw config shape (`versions: { drafts: true }`) and Payload's sanitized
16
+ * one (`versions: { drafts: { autosave: false, … } }`).
17
+ */
18
+ export declare function hasDraftsEnabled(entity: CollectionConfig | GlobalConfig): boolean;
19
+ /**
20
+ * Slug of an `enabledCollections` / `enabledGlobals` entry, whichever form
21
+ * it was written in.
22
+ */
23
+ export declare function resourceSlug(entry: EnabledResource): string;
24
+ /**
25
+ * Resolves the configured `useDrafts` value for one resource: the
26
+ * per-resource override from its `enabledCollections` / `enabledGlobals`
27
+ * entry when present, otherwise the root `useDrafts` flag (default false).
28
+ */
29
+ export declare function resolveUseDrafts(pluginConfig: ReversiaPluginConfig, kind: 'collection' | 'global', slug: string): boolean;
30
+ /**
31
+ * Whether reads and writes for `entity` should target its draft rather than
32
+ * the published document: requires both `useDrafts` resolving to true for
33
+ * this resource and `versions.drafts` on the entity.
34
+ */
35
+ export declare function shouldUseDrafts(pluginConfig: ReversiaPluginConfig, kind: 'collection' | 'global', entity: CollectionConfig | GlobalConfig): boolean;
12
36
  /**
13
37
  * Plain dot-path lookup on an object, returning `undefined` for any missing
14
38
  * segment or non-object traversal. Does not walk into arrays.
@@ -28,6 +28,43 @@ export function resolveDefaultLocale(req) {
28
28
  }
29
29
  return 'en';
30
30
  }
31
+ /**
32
+ * True when a collection or global enables `versions.drafts`. Accepts both
33
+ * the raw config shape (`versions: { drafts: true }`) and Payload's sanitized
34
+ * one (`versions: { drafts: { autosave: false, … } }`).
35
+ */
36
+ export function hasDraftsEnabled(entity) {
37
+ const versions = entity.versions;
38
+ return Boolean(versions && typeof versions === 'object' && versions.drafts);
39
+ }
40
+ /**
41
+ * Slug of an `enabledCollections` / `enabledGlobals` entry, whichever form
42
+ * it was written in.
43
+ */
44
+ export function resourceSlug(entry) {
45
+ return typeof entry === 'string' ? entry : entry.slug;
46
+ }
47
+ /**
48
+ * Resolves the configured `useDrafts` value for one resource: the
49
+ * per-resource override from its `enabledCollections` / `enabledGlobals`
50
+ * entry when present, otherwise the root `useDrafts` flag (default false).
51
+ */
52
+ export function resolveUseDrafts(pluginConfig, kind, slug) {
53
+ const entries = kind === 'collection' ? pluginConfig.enabledCollections : pluginConfig.enabledGlobals;
54
+ const entry = entries?.find((e) => resourceSlug(e) === slug);
55
+ if (entry && typeof entry !== 'string' && typeof entry.useDrafts === 'boolean') {
56
+ return entry.useDrafts;
57
+ }
58
+ return pluginConfig.useDrafts === true;
59
+ }
60
+ /**
61
+ * Whether reads and writes for `entity` should target its draft rather than
62
+ * the published document: requires both `useDrafts` resolving to true for
63
+ * this resource and `versions.drafts` on the entity.
64
+ */
65
+ export function shouldUseDrafts(pluginConfig, kind, entity) {
66
+ return resolveUseDrafts(pluginConfig, kind, entity.slug) && hasDraftsEnabled(entity);
67
+ }
31
68
  /**
32
69
  * Plain dot-path lookup on an object, returning `undefined` for any missing
33
70
  * segment or non-object traversal. Does not walk into arrays.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequoialabs/payload-plugin-reversia",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "author": {
5
5
  "name": "Jean Walrave",
6
6
  "email": "contact@reversia.tech",
@@ -61,4 +61,4 @@
61
61
  "react": "^19.0.0",
62
62
  "typescript": "^5.0.0"
63
63
  }
64
- }
64
+ }