@sequoialabs/payload-plugin-reversia 0.1.7 → 0.2.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.
- package/README.md +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/use-trigger-crawl.d.ts +30 -0
- package/dist/client/use-trigger-crawl.js +43 -0
- package/dist/endpoints/confirm-resources-sync.d.ts +1 -1
- package/dist/endpoints/confirm-resources-sync.js +2 -2
- package/dist/endpoints/resource.d.ts +1 -1
- package/dist/endpoints/resource.js +3 -3
- package/dist/endpoints/resources-definition.d.ts +1 -1
- package/dist/endpoints/resources-definition.js +3 -3
- package/dist/endpoints/resources-insert.d.ts +1 -1
- package/dist/endpoints/resources-insert.js +93 -3
- package/dist/endpoints/resources-sync.d.ts +1 -1
- package/dist/endpoints/resources-sync.js +3 -3
- package/dist/endpoints/resources.d.ts +2 -2
- package/dist/endpoints/resources.js +46 -5
- package/dist/endpoints/settings.d.ts +1 -1
- package/dist/endpoints/settings.js +1 -1
- package/dist/endpoints/trigger-crawl-dashboard.d.ts +3 -0
- package/dist/endpoints/trigger-crawl-dashboard.js +34 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +14 -11
- package/dist/trigger-crawl.d.ts +24 -0
- package/dist/trigger-crawl.js +47 -0
- package/dist/types.d.ts +16 -1
- package/dist/utils/cursor.d.ts +1 -1
- package/dist/utils/fields.d.ts +2 -2
- package/dist/utils/fields.js +6 -4
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Then mark whichever fields are translatable. **You don't need to annotate contai
|
|
|
71
71
|
- **[Field annotations](./docs/field-annotations.md)** — the `custom.reversia` metadata reference.
|
|
72
72
|
- **[Rich text & JSON fields](./docs/rich-text.md)** — `translatableKeys`, path patterns, and the `extract`/`apply` escape hatch.
|
|
73
73
|
- **[API reference](./docs/api-reference.md)** — HTTP endpoints consumed by the Reversia SaaS.
|
|
74
|
+
- **[Triggering crawls](./docs/trigger-crawl.md)** — `triggerCrawl()` for server code and `useTriggerCrawl()` for admin UI.
|
|
74
75
|
- **[Contributing](./CONTRIBUTING.md)** — local dev, linting, testing.
|
|
75
76
|
|
|
76
77
|
## Requirements
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type TriggerCrawlArgs, type TriggerCrawlOutcome, type TriggerCrawlStatus, type UseTriggerCrawlOptions, type UseTriggerCrawlResult, useTriggerCrawl, } from './use-trigger-crawl';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useTriggerCrawl, } from './use-trigger-crawl';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CollectionSlug, GlobalSlug } from 'payload';
|
|
2
|
+
export type TriggerCrawlStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
3
|
+
export interface UseTriggerCrawlOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Override the server endpoint path. Defaults to
|
|
6
|
+
* `/api/reversia/dashboard/trigger-crawl`.
|
|
7
|
+
*/
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TriggerCrawlArgs {
|
|
11
|
+
/**
|
|
12
|
+
* Collection and/or global slugs to crawl. Slugs are prefixed with
|
|
13
|
+
* `payloadcms:` server-side before reaching Reversia. Omit/empty to crawl
|
|
14
|
+
* every enabled resource for the project.
|
|
15
|
+
*/
|
|
16
|
+
types?: Array<CollectionSlug | GlobalSlug>;
|
|
17
|
+
/** Bypass cached crawl content. Defaults to `false`. */
|
|
18
|
+
noCache?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface TriggerCrawlOutcome {
|
|
21
|
+
success: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface UseTriggerCrawlResult {
|
|
25
|
+
trigger: (args?: TriggerCrawlArgs) => Promise<TriggerCrawlOutcome>;
|
|
26
|
+
status: TriggerCrawlStatus;
|
|
27
|
+
error: string | null;
|
|
28
|
+
reset: () => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function useTriggerCrawl(options?: UseTriggerCrawlOptions): UseTriggerCrawlResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
const DEFAULT_ENDPOINT = '/api/reversia/dashboard/trigger-crawl';
|
|
4
|
+
export function useTriggerCrawl(options = {}) {
|
|
5
|
+
const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
|
|
6
|
+
const [status, setStatus] = useState('idle');
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const trigger = useCallback(async (args = {}) => {
|
|
9
|
+
setStatus('pending');
|
|
10
|
+
setError(null);
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(endpoint, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
credentials: 'same-origin',
|
|
16
|
+
body: JSON.stringify({
|
|
17
|
+
...(args.types && args.types.length > 0 ? { types: args.types } : {}),
|
|
18
|
+
...(args.noCache ? { noCache: true } : {}),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const body = (await response.json().catch(() => ({})));
|
|
22
|
+
if (!response.ok || body.success === false) {
|
|
23
|
+
const message = body.error ?? `Request failed (${response.status})`;
|
|
24
|
+
setStatus('error');
|
|
25
|
+
setError(message);
|
|
26
|
+
return { success: false, error: message };
|
|
27
|
+
}
|
|
28
|
+
setStatus('success');
|
|
29
|
+
return { success: true };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
setStatus('error');
|
|
34
|
+
setError(message);
|
|
35
|
+
return { success: false, error: message };
|
|
36
|
+
}
|
|
37
|
+
}, [endpoint]);
|
|
38
|
+
const reset = useCallback(() => {
|
|
39
|
+
setStatus('idle');
|
|
40
|
+
setError(null);
|
|
41
|
+
}, []);
|
|
42
|
+
return { trigger, status, error, reset };
|
|
43
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { decodeCursor } from '../utils/cursor
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { decodeCursor } from '../utils/cursor';
|
|
3
3
|
export function createConfirmResourcesSyncEndpoint(pluginConfig) {
|
|
4
4
|
return {
|
|
5
5
|
path: '/reversia/confirm-resources-sync',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint, GlobalConfig } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from '../types
|
|
2
|
+
import type { ReversiaPluginConfig } from '../types';
|
|
3
3
|
export declare function createResourceEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>, globalsMap: Map<string, GlobalConfig>): Endpoint;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { findLocalizedFields, serializeField } from '../utils/fields
|
|
3
|
-
import { resolveDefaultLocale } from '../utils/payload-helpers
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { findLocalizedFields, serializeField } from '../utils/fields';
|
|
3
|
+
import { resolveDefaultLocale } from '../utils/payload-helpers';
|
|
4
4
|
function extract(doc, fields) {
|
|
5
5
|
const content = {};
|
|
6
6
|
const contentTypes = {};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint, GlobalConfig } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from '../types
|
|
2
|
+
import type { ReversiaPluginConfig } from '../types';
|
|
3
3
|
export declare function createResourcesDefinitionEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>, globalsMap: Map<string, GlobalConfig>): Endpoint;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { buildTranslatableConfiguration, findLocalizedFields } from '../utils/fields
|
|
3
|
-
import { resolveStaticLabel } from '../utils/labels
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { buildTranslatableConfiguration, findLocalizedFields } from '../utils/fields';
|
|
3
|
+
import { resolveStaticLabel } from '../utils/labels';
|
|
4
4
|
export function createResourcesDefinitionEndpoint(pluginConfig, collectionsMap, globalsMap) {
|
|
5
5
|
return {
|
|
6
6
|
path: '/reversia/resources-definition',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint, GlobalConfig } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from '../types
|
|
2
|
+
import type { ReversiaPluginConfig } from '../types';
|
|
3
3
|
export declare function createResourcesInsertEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>, globalsMap: Map<string, GlobalConfig>): Endpoint;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { deflatePopulatedRelationships, deserializeFieldValue, findLocalizedFields, } from '../utils/fields
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { deflatePopulatedRelationships, deserializeFieldValue, findLocalizedFields, } from '../utils/fields';
|
|
3
3
|
const WRITE_CONFLICT_MAX_RETRIES = 3;
|
|
4
4
|
const WRITE_CONFLICT_BASE_DELAY_MS = 50;
|
|
5
5
|
function isWriteConflict(error) {
|
|
@@ -109,7 +109,24 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
|
|
|
109
109
|
continue;
|
|
110
110
|
}
|
|
111
111
|
const sourceValue = source ? source[fieldName] : undefined;
|
|
112
|
-
|
|
112
|
+
let finalValue;
|
|
113
|
+
try {
|
|
114
|
+
finalValue = deserializeFieldValue(field, sourceValue, translatedValue);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Re-throw with the offending field name attached so the caller's
|
|
118
|
+
// logger can pinpoint which container's source-locale shape blew up
|
|
119
|
+
// (e.g. a richText with a missing `children` array, an array item
|
|
120
|
+
// without an `id`). Without this, the surface error is something like
|
|
121
|
+
// "Cannot set properties of undefined (setting '0')" with no clue
|
|
122
|
+
// which field caused it across a multi-field document.
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
const wrapped = new Error(`field "${fieldName}" failed to deserialize: ${message}`);
|
|
125
|
+
if (error instanceof Error && error.stack) {
|
|
126
|
+
wrapped.stack = error.stack;
|
|
127
|
+
}
|
|
128
|
+
throw wrapped;
|
|
129
|
+
}
|
|
113
130
|
updateData[fieldName] = finalValue;
|
|
114
131
|
acceptedFields.push(fieldName);
|
|
115
132
|
const prevValue = previous ? previous[fieldName] : undefined;
|
|
@@ -124,6 +141,65 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
|
|
|
124
141
|
}
|
|
125
142
|
return { updateData, diff, acceptedFields };
|
|
126
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Payload's unique validator rejects an update when the new value already
|
|
146
|
+
* exists on another doc in the same locale. Reversia legitimately produces
|
|
147
|
+
* colliding translations (proper nouns, short phrases, numeric-suffix titles
|
|
148
|
+
* that don't change across locales), so without a pre-check the whole item
|
|
149
|
+
* fails with `ValidationError: <field>.<locale>: Value must be unique`.
|
|
150
|
+
*
|
|
151
|
+
* For every scalar field in `updateData` that the schema marks `unique: true`,
|
|
152
|
+
* query the target collection/locale for any OTHER doc with that value. When
|
|
153
|
+
* one exists, drop the field from the write so the rest of the update can go
|
|
154
|
+
* through. The source of truth stays on the first doc that claimed the value;
|
|
155
|
+
* the second doc's target-locale slot remains empty and can be edited
|
|
156
|
+
* manually later.
|
|
157
|
+
*/
|
|
158
|
+
async function dropUniqueCollisions(params) {
|
|
159
|
+
const { payload, collection, id, locale, fields, updateData, acceptedFields, diff } = params;
|
|
160
|
+
const dropped = [];
|
|
161
|
+
for (const field of fields) {
|
|
162
|
+
if (field.isContainer || !field.unique) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!(field.name in updateData)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const value = updateData[field.name];
|
|
169
|
+
if (value === null || value === undefined || value === '') {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const existing = await payload.find({
|
|
173
|
+
collection: collection,
|
|
174
|
+
locale: locale,
|
|
175
|
+
depth: 0,
|
|
176
|
+
limit: 1,
|
|
177
|
+
pagination: false,
|
|
178
|
+
where: {
|
|
179
|
+
and: [{ [field.name]: { equals: value } }, { id: { not_equals: id } }],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
if (existing.docs.length === 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
payload.logger.warn({
|
|
186
|
+
collection,
|
|
187
|
+
id,
|
|
188
|
+
targetLocale: locale,
|
|
189
|
+
field: field.name,
|
|
190
|
+
value,
|
|
191
|
+
collidingDocId: existing.docs[0]?.id,
|
|
192
|
+
}, '[reversia] skipping field to avoid unique collision');
|
|
193
|
+
delete updateData[field.name];
|
|
194
|
+
const acceptedIdx = acceptedFields.indexOf(field.name);
|
|
195
|
+
if (acceptedIdx !== -1) {
|
|
196
|
+
acceptedFields.splice(acceptedIdx, 1);
|
|
197
|
+
}
|
|
198
|
+
delete diff[field.name];
|
|
199
|
+
dropped.push(field.name);
|
|
200
|
+
}
|
|
201
|
+
return dropped;
|
|
202
|
+
}
|
|
127
203
|
export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, globalsMap) {
|
|
128
204
|
return {
|
|
129
205
|
path: '/reversia/resources-insert',
|
|
@@ -238,6 +314,20 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
|
|
|
238
314
|
response.errors.push(`Item ${index}: no recognised fields in data (keys: ${Object.keys(item.data).join(', ')})`);
|
|
239
315
|
continue;
|
|
240
316
|
}
|
|
317
|
+
const droppedForUnique = await dropUniqueCollisions({
|
|
318
|
+
payload: req.payload,
|
|
319
|
+
collection: slug,
|
|
320
|
+
id: itemId,
|
|
321
|
+
locale: item.targetLocale,
|
|
322
|
+
fields: allowedFields,
|
|
323
|
+
updateData,
|
|
324
|
+
acceptedFields,
|
|
325
|
+
diff,
|
|
326
|
+
});
|
|
327
|
+
if (acceptedFields.length === 0 || Object.keys(updateData).length === 0) {
|
|
328
|
+
response.errors.push(`Item ${index} (${item.type} ${itemId} → ${item.targetLocale}): all translatable fields skipped due to unique-constraint collisions [${droppedForUnique.join(', ')}]`);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
241
331
|
await withRetry(() => req.payload.update({
|
|
242
332
|
collection: slug,
|
|
243
333
|
id: itemId,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from '../types
|
|
2
|
+
import type { ReversiaPluginConfig } from '../types';
|
|
3
3
|
export declare function createResourcesSyncEndpoint(pluginConfig: ReversiaPluginConfig, _collectionsMap: Map<string, CollectionConfig>): Endpoint;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { decodeCursor, encodeCursor } from '../utils/cursor
|
|
3
|
-
import { parseLimit } from '../utils/payload-helpers
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { decodeCursor, encodeCursor } from '../utils/cursor';
|
|
3
|
+
import { parseLimit } from '../utils/payload-helpers';
|
|
4
4
|
export function createResourcesSyncEndpoint(pluginConfig, _collectionsMap) {
|
|
5
5
|
return {
|
|
6
6
|
path: '/reversia/resources-sync',
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint, GlobalConfig } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from '../types
|
|
3
|
-
export declare function createResourcesEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>,
|
|
2
|
+
import type { ReversiaPluginConfig } from '../types';
|
|
3
|
+
export declare function createResourcesEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>, globalsMap: Map<string, GlobalConfig>): Endpoint;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { unauthorizedResponse, validateApiKey } from '../utils/auth
|
|
2
|
-
import { decodeCursor, encodeCursor } from '../utils/cursor
|
|
3
|
-
import { findLocalizedFields, serializeField } from '../utils/fields
|
|
4
|
-
import { parseLimit, resolveDefaultLocale } from '../utils/payload-helpers
|
|
1
|
+
import { unauthorizedResponse, validateApiKey } from '../utils/auth';
|
|
2
|
+
import { decodeCursor, encodeCursor } from '../utils/cursor';
|
|
3
|
+
import { findLocalizedFields, serializeField } from '../utils/fields';
|
|
4
|
+
import { parseLimit, resolveDefaultLocale } from '../utils/payload-helpers';
|
|
5
5
|
function extractContent(doc, fields) {
|
|
6
6
|
const content = {};
|
|
7
7
|
const contentTypes = {};
|
|
@@ -25,7 +25,7 @@ function getLabelValue(doc, fields) {
|
|
|
25
25
|
const value = doc?.[labelField.name];
|
|
26
26
|
return typeof value === 'string' ? value : undefined;
|
|
27
27
|
}
|
|
28
|
-
export function createResourcesEndpoint(pluginConfig, collectionsMap,
|
|
28
|
+
export function createResourcesEndpoint(pluginConfig, collectionsMap, globalsMap) {
|
|
29
29
|
return {
|
|
30
30
|
path: '/reversia/resources',
|
|
31
31
|
method: 'get',
|
|
@@ -98,6 +98,47 @@ export function createResourcesEndpoint(pluginConfig, collectionsMap, _globalsMa
|
|
|
98
98
|
response.content.push({ type: resourceType, data: items });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
// Globals are singletons (one doc per slug, id = slug), emitted after
|
|
102
|
+
// collections so pagination via `cursor` resumes at the right slot.
|
|
103
|
+
// Each global counts as 1 toward `limit`. Cursor semantics mirror the
|
|
104
|
+
// collection loop: when we find the type the cursor points to, that
|
|
105
|
+
// slot has already been returned — advance past it and resume from
|
|
106
|
+
// the next iteration.
|
|
107
|
+
for (const [slug, global] of globalsMap) {
|
|
108
|
+
const resourceType = `payloadcms:global:${slug}`;
|
|
109
|
+
if (requestedTypes && !requestedTypes.includes(resourceType)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!startFromCursor) {
|
|
113
|
+
if (cursor && cursor.type === resourceType) {
|
|
114
|
+
startFromCursor = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (totalFetched >= limit) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
const localizedFields = findLocalizedFields(global.fields);
|
|
123
|
+
if (localizedFields.length === 0) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const doc = await req.payload.findGlobal({ slug, locale: defaultLocale });
|
|
127
|
+
const { content, contentTypes } = extractContent(doc, localizedFields);
|
|
128
|
+
if (Object.keys(content).length === 0) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const item = {
|
|
132
|
+
id: slug,
|
|
133
|
+
label: getLabelValue(doc, localizedFields),
|
|
134
|
+
content,
|
|
135
|
+
contentTypes: Object.keys(contentTypes).length > 0 ? contentTypes : undefined,
|
|
136
|
+
};
|
|
137
|
+
response.content.push({ type: resourceType, data: [item] });
|
|
138
|
+
lastType = resourceType;
|
|
139
|
+
lastId = slug;
|
|
140
|
+
totalFetched++;
|
|
141
|
+
}
|
|
101
142
|
if (lastType && lastId && totalFetched >= limit) {
|
|
102
143
|
response.cursor = encodeCursor(lastType, lastId);
|
|
103
144
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { triggerCrawl } from '../trigger-crawl';
|
|
2
|
+
export function createTriggerCrawlDashboardEndpoint(pluginConfig) {
|
|
3
|
+
return {
|
|
4
|
+
path: '/reversia/dashboard/trigger-crawl',
|
|
5
|
+
method: 'post',
|
|
6
|
+
handler: async (req) => {
|
|
7
|
+
if (!req.user) {
|
|
8
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
9
|
+
}
|
|
10
|
+
const body = req.json ? await req.json() : undefined;
|
|
11
|
+
const types = Array.isArray(body?.types)
|
|
12
|
+
? body.types.filter((t) => typeof t === 'string')
|
|
13
|
+
: undefined;
|
|
14
|
+
const noCache = Boolean(body?.noCache);
|
|
15
|
+
try {
|
|
16
|
+
const result = await triggerCrawl({
|
|
17
|
+
apiKey: pluginConfig.apiKey,
|
|
18
|
+
baseUrl: pluginConfig.baseUrl,
|
|
19
|
+
...(types && types.length > 0 ? { types } : {}),
|
|
20
|
+
...(noCache ? { noCache: true } : {}),
|
|
21
|
+
});
|
|
22
|
+
return Response.json(result);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
req.payload.logger.error({
|
|
27
|
+
msg: '[reversia] dashboard trigger-crawl failed',
|
|
28
|
+
err: message,
|
|
29
|
+
});
|
|
30
|
+
return Response.json({ error: message }, { status: 502 });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Config } from 'payload';
|
|
2
|
-
import type { ReversiaPluginConfig } from './types
|
|
3
|
-
export type {
|
|
4
|
-
export {
|
|
2
|
+
import type { ReversiaPluginConfig } from './types';
|
|
3
|
+
export type { TriggerCrawlOptions, TriggerCrawlResult } from './trigger-crawl';
|
|
4
|
+
export { triggerCrawl } from './trigger-crawl';
|
|
5
|
+
export type { ConfirmResourcesSyncResponse, InsertionRequest, InsertionResponse, ResourceDefinition, ResourceItem, ResourceResponse, ReversiaErrorResponse, ReversiaFieldCustom, ReversiaPluginConfig, SettingsResponse, StreamResponse, TranslatableFieldConfig, } from './types';
|
|
6
|
+
export { ReversiaFieldBehavior, ReversiaFieldType } from './types';
|
|
5
7
|
export declare const reversiaPlugin: (pluginConfig: ReversiaPluginConfig) => (config: Config) => Config;
|
|
6
8
|
export default reversiaPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { reversiaSyncPendingCollection } from './collections/sync-pending
|
|
2
|
-
import { createConfirmResourcesSyncEndpoint } from './endpoints/confirm-resources-sync
|
|
3
|
-
import { createResourceEndpoint } from './endpoints/resource
|
|
4
|
-
import { createResourcesEndpoint } from './endpoints/resources
|
|
5
|
-
import { createResourcesDefinitionEndpoint } from './endpoints/resources-definition
|
|
6
|
-
import { createResourcesInsertEndpoint } from './endpoints/resources-insert
|
|
7
|
-
import { createResourcesSyncEndpoint } from './endpoints/resources-sync
|
|
8
|
-
import { createSettingsEndpoint } from './endpoints/settings
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
1
|
+
import { reversiaSyncPendingCollection } from './collections/sync-pending';
|
|
2
|
+
import { createConfirmResourcesSyncEndpoint } from './endpoints/confirm-resources-sync';
|
|
3
|
+
import { createResourceEndpoint } from './endpoints/resource';
|
|
4
|
+
import { createResourcesEndpoint } from './endpoints/resources';
|
|
5
|
+
import { createResourcesDefinitionEndpoint } from './endpoints/resources-definition';
|
|
6
|
+
import { createResourcesInsertEndpoint } from './endpoints/resources-insert';
|
|
7
|
+
import { createResourcesSyncEndpoint } from './endpoints/resources-sync';
|
|
8
|
+
import { createSettingsEndpoint } from './endpoints/settings';
|
|
9
|
+
import { createTriggerCrawlDashboardEndpoint } from './endpoints/trigger-crawl-dashboard';
|
|
10
|
+
import { createAfterChangeHook } from './hooks/after-change';
|
|
11
|
+
import { findLocalizedFields } from './utils/fields';
|
|
12
|
+
export { triggerCrawl } from './trigger-crawl';
|
|
13
|
+
export { ReversiaFieldBehavior, ReversiaFieldType } from './types';
|
|
12
14
|
const APPLIED_MARKER = Symbol.for('payload-plugin-reversia.applied');
|
|
13
15
|
export const reversiaPlugin = (pluginConfig) => (config) => {
|
|
14
16
|
if (pluginConfig.disabled) {
|
|
@@ -76,6 +78,7 @@ export const reversiaPlugin = (pluginConfig) => (config) => {
|
|
|
76
78
|
createResourcesInsertEndpoint(pluginConfig, collectionsMap, globalsMap),
|
|
77
79
|
createConfirmResourcesSyncEndpoint(pluginConfig),
|
|
78
80
|
createSettingsEndpoint(pluginConfig),
|
|
81
|
+
createTriggerCrawlDashboardEndpoint(pluginConfig),
|
|
79
82
|
];
|
|
80
83
|
return config;
|
|
81
84
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CollectionSlug, GlobalSlug } from 'payload';
|
|
2
|
+
export interface TriggerCrawlOptions {
|
|
3
|
+
/** Reversia project API key (the same value passed to `reversiaPlugin({ apiKey })`). */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/**
|
|
6
|
+
* Collection and/or global slugs to crawl. Each slug is prefixed with
|
|
7
|
+
* `payloadcms:` before being sent to Reversia. Omit/empty → crawl all
|
|
8
|
+
* enabled resources for the project.
|
|
9
|
+
*/
|
|
10
|
+
types?: Array<CollectionSlug | GlobalSlug>;
|
|
11
|
+
/** Bypass cached crawl content. Defaults to `false`. */
|
|
12
|
+
noCache?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Override the Reversia API base URL. Defaults to `process.env.REVERSIA_API_URL`
|
|
15
|
+
* then a built-in production/staging URL.
|
|
16
|
+
*/
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
/** Optional custom fetch (for tests / proxies). Defaults to the global `fetch`. */
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
export interface TriggerCrawlResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function triggerCrawl(options: TriggerCrawlOptions): Promise<TriggerCrawlResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://staging.api.reversia.tech';
|
|
2
|
+
const TYPE_NAMESPACE = 'payloadcms:';
|
|
3
|
+
const TRIGGER_CRAWL_PATH = '/projects/trigger-crawl';
|
|
4
|
+
export async function triggerCrawl(options) {
|
|
5
|
+
if (typeof options.apiKey !== 'string' || options.apiKey.length === 0) {
|
|
6
|
+
throw new Error('[reversia] apiKey is required. Pass `TriggerCrawlOptions.apiKey` as a non-empty string.');
|
|
7
|
+
}
|
|
8
|
+
const baseUrl = (options.baseUrl ?? process.env.REVERSIA_API_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
9
|
+
const normalizedTypes = (options.types ?? [])
|
|
10
|
+
.filter((t) => typeof t === 'string' && t.length > 0)
|
|
11
|
+
.map((t) => `${TYPE_NAMESPACE}${t}`);
|
|
12
|
+
const body = {};
|
|
13
|
+
if (normalizedTypes.length > 0) {
|
|
14
|
+
body.types = normalizedTypes;
|
|
15
|
+
}
|
|
16
|
+
if (options.noCache) {
|
|
17
|
+
body.noCache = true;
|
|
18
|
+
}
|
|
19
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
20
|
+
const response = await fetchImpl(`${baseUrl}${TRIGGER_CRAWL_PATH}`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
Accept: 'application/json',
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
let excerpt = '';
|
|
31
|
+
try {
|
|
32
|
+
excerpt = (await response.text()).slice(0, 200);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore body read errors — keep the status info
|
|
36
|
+
}
|
|
37
|
+
throw new Error(`[reversia] trigger-crawl failed: ${response.status} ${response.statusText}${excerpt ? ` — ${excerpt}` : ''}`);
|
|
38
|
+
}
|
|
39
|
+
let parsed = null;
|
|
40
|
+
try {
|
|
41
|
+
parsed = (await response.json());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
parsed = null;
|
|
45
|
+
}
|
|
46
|
+
return { success: parsed?.success !== false };
|
|
47
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CollectionSlug } from 'payload';
|
|
2
|
-
import type { LeafSegment } from './utils/path-resolver
|
|
2
|
+
import type { LeafSegment } from './utils/path-resolver';
|
|
3
3
|
export interface ReversiaPluginConfig {
|
|
4
4
|
/**
|
|
5
5
|
* API key used by Reversia SaaS to authenticate requests.
|
|
@@ -20,6 +20,12 @@ export interface ReversiaPluginConfig {
|
|
|
20
20
|
* Whether the plugin is disabled. Defaults to false.
|
|
21
21
|
*/
|
|
22
22
|
disabled?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Override the Reversia API base URL used by `triggerCrawl` and the
|
|
25
|
+
* dashboard button. Defaults to `process.env.REVERSIA_API_URL` then the
|
|
26
|
+
* built-in production URL.
|
|
27
|
+
*/
|
|
28
|
+
baseUrl?: string;
|
|
23
29
|
}
|
|
24
30
|
export declare enum ReversiaFieldType {
|
|
25
31
|
TEXT = "TEXT",
|
|
@@ -183,6 +189,15 @@ export interface LocalizedFieldInfo {
|
|
|
183
189
|
* the update.
|
|
184
190
|
*/
|
|
185
191
|
hasRequiredLeaf?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* True when a top-level localized scalar is `unique: true`. Used during
|
|
194
|
+
* insertion to pre-check for cross-document collisions in the target locale
|
|
195
|
+
* before writing — Reversia may translate two docs' source values to the
|
|
196
|
+
* same target string, and Payload's unique validator would reject the
|
|
197
|
+
* second write. Only meaningful for scalars; Payload doesn't support
|
|
198
|
+
* `unique` on containers.
|
|
199
|
+
*/
|
|
200
|
+
unique?: boolean;
|
|
186
201
|
}
|
|
187
202
|
export interface ResourceItem {
|
|
188
203
|
id: string;
|
package/dist/utils/cursor.d.ts
CHANGED
package/dist/utils/fields.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Field } from 'payload';
|
|
2
|
-
import type { LocalizedFieldInfo, TranslatableFieldConfig } from '../types
|
|
3
|
-
import { ReversiaFieldType } from '../types
|
|
2
|
+
import type { LocalizedFieldInfo, TranslatableFieldConfig } from '../types';
|
|
3
|
+
import { ReversiaFieldType } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Collects one `LocalizedFieldInfo` per top-level field that is itself
|
|
6
6
|
* localized OR that contains at least one localized descendant.
|
package/dist/utils/fields.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ReversiaFieldType } from '../types
|
|
2
|
-
import { applyByKeys, compileKeyMatcher, DEFAULT_RICHTEXT_KEYS, extractByKeys, } from './json-extract
|
|
3
|
-
import { resolveStaticLabel } from './labels
|
|
4
|
-
import { applyTranslationsToContainer, joinPointers, resolveLeafLocations, } from './path-resolver
|
|
1
|
+
import { ReversiaFieldType } from '../types';
|
|
2
|
+
import { applyByKeys, compileKeyMatcher, DEFAULT_RICHTEXT_KEYS, extractByKeys, } from './json-extract';
|
|
3
|
+
import { resolveStaticLabel } from './labels';
|
|
4
|
+
import { applyTranslationsToContainer, joinPointers, resolveLeafLocations, } from './path-resolver';
|
|
5
5
|
function getFieldLabel(field) {
|
|
6
6
|
if (!('name' in field)) {
|
|
7
7
|
return '';
|
|
@@ -134,6 +134,7 @@ function describeTopLevelField(field) {
|
|
|
134
134
|
const maxLength = getNumericProp(field, 'maxLength');
|
|
135
135
|
const minLength = getNumericProp(field, 'minLength');
|
|
136
136
|
const required = getBoolProp(field, 'required');
|
|
137
|
+
const unique = getBoolProp(field, 'unique');
|
|
137
138
|
return {
|
|
138
139
|
name,
|
|
139
140
|
label,
|
|
@@ -151,6 +152,7 @@ function describeTopLevelField(field) {
|
|
|
151
152
|
...(maxLength !== undefined && { maxLength }),
|
|
152
153
|
...(minLength !== undefined && { minLength }),
|
|
153
154
|
...(required && { hasRequiredLeaf: true }),
|
|
155
|
+
...(unique && { unique: true }),
|
|
154
156
|
};
|
|
155
157
|
}
|
|
156
158
|
// Top-level richText / json (localized or not localized but containing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sequoialabs/payload-plugin-reversia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "Jean Walrave",
|
|
6
6
|
"email": "contact@reversia.tech",
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
".": {
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./client": {
|
|
32
|
+
"types": "./dist/client/index.d.ts",
|
|
33
|
+
"import": "./dist/client/index.js"
|
|
30
34
|
}
|
|
31
35
|
},
|
|
32
36
|
"main": "dist/index.js",
|
|
@@ -44,14 +48,17 @@
|
|
|
44
48
|
"prepublishOnly": "bun run test && bun run build"
|
|
45
49
|
},
|
|
46
50
|
"peerDependencies": {
|
|
47
|
-
"payload": "^3.0.0"
|
|
51
|
+
"payload": "^3.0.0",
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@biomejs/biome": "2.4.12",
|
|
51
56
|
"@payloadcms/db-sqlite": "^3.0.0",
|
|
52
57
|
"@payloadcms/richtext-lexical": "^3.79.1",
|
|
53
58
|
"@types/bun": "^1.3.10",
|
|
59
|
+
"@types/react": "^19.0.0",
|
|
54
60
|
"payload": "^3.0.0",
|
|
61
|
+
"react": "^19.0.0",
|
|
55
62
|
"typescript": "^5.0.0"
|
|
56
63
|
}
|
|
57
|
-
}
|
|
64
|
+
}
|