@sequoialabs/payload-plugin-reversia 0.1.7 → 0.1.8
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.
|
@@ -124,6 +124,61 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
|
|
|
124
124
|
}
|
|
125
125
|
return { updateData, diff, acceptedFields };
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Payload's unique validator rejects an update when the new value already
|
|
129
|
+
* exists on another doc in the same locale. Reversia legitimately produces
|
|
130
|
+
* colliding translations (proper nouns, short phrases, numeric-suffix titles
|
|
131
|
+
* that don't change across locales), so without a pre-check the whole item
|
|
132
|
+
* fails with `ValidationError: <field>.<locale>: Value must be unique`.
|
|
133
|
+
*
|
|
134
|
+
* For every scalar field in `updateData` that the schema marks `unique: true`,
|
|
135
|
+
* query the target collection/locale for any OTHER doc with that value. When
|
|
136
|
+
* one exists, drop the field from the write so the rest of the update can go
|
|
137
|
+
* through. The source of truth stays on the first doc that claimed the value;
|
|
138
|
+
* the second doc's target-locale slot remains empty and can be edited
|
|
139
|
+
* manually later.
|
|
140
|
+
*/
|
|
141
|
+
async function dropUniqueCollisions(params) {
|
|
142
|
+
const { payload, collection, id, locale, fields, updateData, acceptedFields, diff } = params;
|
|
143
|
+
for (const field of fields) {
|
|
144
|
+
if (field.isContainer || !field.unique) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (!(field.name in updateData)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const value = updateData[field.name];
|
|
151
|
+
if (value === null || value === undefined || value === '') {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const existing = await payload.find({
|
|
155
|
+
collection: collection,
|
|
156
|
+
locale: locale,
|
|
157
|
+
depth: 0,
|
|
158
|
+
limit: 1,
|
|
159
|
+
pagination: false,
|
|
160
|
+
where: {
|
|
161
|
+
and: [{ [field.name]: { equals: value } }, { id: { not_equals: id } }],
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
if (existing.docs.length === 0) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
payload.logger.warn({
|
|
168
|
+
collection,
|
|
169
|
+
id,
|
|
170
|
+
targetLocale: locale,
|
|
171
|
+
field: field.name,
|
|
172
|
+
collidingDocId: existing.docs[0]?.id,
|
|
173
|
+
}, '[reversia] skipping field to avoid unique collision');
|
|
174
|
+
delete updateData[field.name];
|
|
175
|
+
const acceptedIdx = acceptedFields.indexOf(field.name);
|
|
176
|
+
if (acceptedIdx !== -1) {
|
|
177
|
+
acceptedFields.splice(acceptedIdx, 1);
|
|
178
|
+
}
|
|
179
|
+
delete diff[field.name];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
127
182
|
export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, globalsMap) {
|
|
128
183
|
return {
|
|
129
184
|
path: '/reversia/resources-insert',
|
|
@@ -238,6 +293,20 @@ export function createResourcesInsertEndpoint(pluginConfig, collectionsMap, glob
|
|
|
238
293
|
response.errors.push(`Item ${index}: no recognised fields in data (keys: ${Object.keys(item.data).join(', ')})`);
|
|
239
294
|
continue;
|
|
240
295
|
}
|
|
296
|
+
await dropUniqueCollisions({
|
|
297
|
+
payload: req.payload,
|
|
298
|
+
collection: slug,
|
|
299
|
+
id: itemId,
|
|
300
|
+
locale: item.targetLocale,
|
|
301
|
+
fields: allowedFields,
|
|
302
|
+
updateData,
|
|
303
|
+
acceptedFields,
|
|
304
|
+
diff,
|
|
305
|
+
});
|
|
306
|
+
if (acceptedFields.length === 0 || Object.keys(updateData).length === 0) {
|
|
307
|
+
response.errors.push(`Item ${index}: all translatable fields skipped due to unique-constraint collisions in ${item.targetLocale}`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
241
310
|
await withRetry(() => req.payload.update({
|
|
242
311
|
collection: slug,
|
|
243
312
|
id: itemId,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint, GlobalConfig } from 'payload';
|
|
2
2
|
import type { ReversiaPluginConfig } from '../types.js';
|
|
3
|
-
export declare function createResourcesEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>,
|
|
3
|
+
export declare function createResourcesEndpoint(pluginConfig: ReversiaPluginConfig, collectionsMap: Map<string, CollectionConfig>, globalsMap: Map<string, GlobalConfig>): Endpoint;
|
|
@@ -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
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -183,6 +183,15 @@ export interface LocalizedFieldInfo {
|
|
|
183
183
|
* the update.
|
|
184
184
|
*/
|
|
185
185
|
hasRequiredLeaf?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* True when a top-level localized scalar is `unique: true`. Used during
|
|
188
|
+
* insertion to pre-check for cross-document collisions in the target locale
|
|
189
|
+
* before writing — Reversia may translate two docs' source values to the
|
|
190
|
+
* same target string, and Payload's unique validator would reject the
|
|
191
|
+
* second write. Only meaningful for scalars; Payload doesn't support
|
|
192
|
+
* `unique` on containers.
|
|
193
|
+
*/
|
|
194
|
+
unique?: boolean;
|
|
186
195
|
}
|
|
187
196
|
export interface ResourceItem {
|
|
188
197
|
id: string;
|
package/dist/utils/fields.js
CHANGED
|
@@ -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
|