@sequoialabs/payload-plugin-reversia 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/endpoints/resources-insert.js +27 -10
- package/dist/utils/fields.js +44 -19
- package/package.json +1 -1
|
@@ -18,7 +18,8 @@ async function withRetry(fn) {
|
|
|
18
18
|
}
|
|
19
19
|
catch (error) {
|
|
20
20
|
if (attempt < WRITE_CONFLICT_MAX_RETRIES && isWriteConflict(error)) {
|
|
21
|
-
const delay = WRITE_CONFLICT_BASE_DELAY_MS * 2 ** attempt +
|
|
21
|
+
const delay = WRITE_CONFLICT_BASE_DELAY_MS * 2 ** attempt +
|
|
22
|
+
Math.random() * WRITE_CONFLICT_BASE_DELAY_MS;
|
|
22
23
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
@@ -65,7 +66,19 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
|
|
|
65
66
|
const previous = previousDoc ?? null;
|
|
66
67
|
const source = cleanSource ?? null;
|
|
67
68
|
const dataKeys = new Set(Object.keys(data));
|
|
68
|
-
// Seed required fields
|
|
69
|
+
// Seed required fields not being translated so Payload's whole-document
|
|
70
|
+
// validation doesn't reject the update.
|
|
71
|
+
//
|
|
72
|
+
// Scalars: only seed when the target locale has no value at all (null /
|
|
73
|
+
// undefined / empty string). If the target already has something, leave it.
|
|
74
|
+
//
|
|
75
|
+
// Containers: always seed from source when not in `data`. The target might
|
|
76
|
+
// have a "structurally present but content-empty" value (e.g. Payload
|
|
77
|
+
// auto-creates 9 default block items from `defaultValue` but every block's
|
|
78
|
+
// required inner richText is null). A shallow null-check misses this; a
|
|
79
|
+
// deep check is impractical. Seeding writes source-locale content as a
|
|
80
|
+
// placeholder — once Reversia translates the field and sends it in `data`,
|
|
81
|
+
// the seed is overwritten with the real translation.
|
|
69
82
|
for (const field of allowedFields) {
|
|
70
83
|
if (!field.hasRequiredLeaf) {
|
|
71
84
|
continue;
|
|
@@ -73,18 +86,22 @@ function applyTranslations({ allowedFields, data, sourceDoc, previousDoc, }) {
|
|
|
73
86
|
if (dataKeys.has(field.name)) {
|
|
74
87
|
continue; // Reversia is sending it — will be handled below
|
|
75
88
|
}
|
|
76
|
-
const targetValue = previous ? previous[field.name] : undefined;
|
|
77
|
-
if (targetValue !== undefined && targetValue !== null) {
|
|
78
|
-
continue; // Target already has a value — don't overwrite
|
|
79
|
-
}
|
|
80
89
|
const sourceValue = source ? source[field.name] : undefined;
|
|
81
90
|
if (sourceValue === undefined || sourceValue === null) {
|
|
82
91
|
continue; // Source is also empty — nothing we can do
|
|
83
92
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if (field.isContainer) {
|
|
94
|
+
// Always seed containers from source — inner required subfields may be
|
|
95
|
+
// empty even when the container itself is non-null in the target.
|
|
96
|
+
updateData[field.name] = deflatePopulatedRelationships(structuredClone(sourceValue));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Scalars: only seed when truly empty in target.
|
|
100
|
+
const targetValue = previous ? previous[field.name] : undefined;
|
|
101
|
+
if (targetValue === undefined || targetValue === null || targetValue === '') {
|
|
102
|
+
updateData[field.name] = sourceValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
88
105
|
}
|
|
89
106
|
for (const [fieldName, translatedValue] of Object.entries(data)) {
|
|
90
107
|
const field = fieldByName.get(fieldName);
|
package/dist/utils/fields.js
CHANGED
|
@@ -855,9 +855,13 @@ export function deflatePopulatedRelationships(value) {
|
|
|
855
855
|
return out;
|
|
856
856
|
}
|
|
857
857
|
/**
|
|
858
|
-
* Deflate a single value: if it's a populated doc, return its id. If it's
|
|
859
|
-
*
|
|
860
|
-
*
|
|
858
|
+
* Deflate a single value: if it's a populated doc, return its id. If it's an
|
|
859
|
+
* object with a `relationTo` + populated `value` (polymorphic relationship OR
|
|
860
|
+
* Lexical upload/relationship node), deflate only the `value` property while
|
|
861
|
+
* preserving all other keys (`type`, `children`, `version`, `format`, …).
|
|
862
|
+
*
|
|
863
|
+
* Previous implementation returned `{ relationTo, value: id }` which stripped
|
|
864
|
+
* Lexical node metadata and corrupted richText trees.
|
|
861
865
|
*/
|
|
862
866
|
function deflateValue(val) {
|
|
863
867
|
if (val === null || val === undefined || typeof val !== 'object') {
|
|
@@ -866,30 +870,51 @@ function deflateValue(val) {
|
|
|
866
870
|
if (Array.isArray(val)) {
|
|
867
871
|
return val.map(deflateValue);
|
|
868
872
|
}
|
|
869
|
-
// Direct populated doc → raw id (e.g. upload field: { id, filename, ... })
|
|
870
|
-
if (isPopulatedDoc(val)) {
|
|
871
|
-
return val.id;
|
|
872
|
-
}
|
|
873
|
-
// Polymorphic relationship: { relationTo: 'x', value: <populated> }
|
|
874
873
|
const obj = val;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
874
|
+
// Direct populated doc → raw id (e.g. upload field stored as { id, filename, … })
|
|
875
|
+
// Only match when the object has NO other "structural" keys that indicate
|
|
876
|
+
// it's a Lexical node or rich object we shouldn't collapse.
|
|
877
|
+
if (isPopulatedDoc(obj) && !isStructuralNode(obj)) {
|
|
878
|
+
return obj.id;
|
|
879
|
+
}
|
|
880
|
+
// Object with `relationTo` + populated `value` — this matches both Payload
|
|
881
|
+
// polymorphic relationships AND Lexical upload/relationship nodes.
|
|
882
|
+
// Deflate only the `value` key; preserve everything else.
|
|
883
|
+
if (typeof obj.relationTo === 'string' && 'value' in obj) {
|
|
884
|
+
if (isPopulatedDoc(obj.value)) {
|
|
885
|
+
const out = {};
|
|
886
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
887
|
+
out[k] = k === 'value' ? obj.value.id : deflateValue(v);
|
|
888
|
+
}
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
891
|
+
if (Array.isArray(obj.value)) {
|
|
892
|
+
const out = {};
|
|
893
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
894
|
+
out[k] =
|
|
895
|
+
k === 'value'
|
|
896
|
+
? v.map((item) => isPopulatedDoc(item) ? item.id : item)
|
|
897
|
+
: deflateValue(v);
|
|
898
|
+
}
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
884
901
|
}
|
|
885
902
|
// Recurse into nested structure
|
|
886
903
|
return deflatePopulatedRelationships(val);
|
|
887
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* A "structural node" is an object that carries its own semantics beyond being
|
|
907
|
+
* a populated relationship doc — e.g. a Lexical node with `type`, `children`,
|
|
908
|
+
* `version`. Collapsing it to just its `id` would destroy the tree.
|
|
909
|
+
*/
|
|
910
|
+
function isStructuralNode(obj) {
|
|
911
|
+
return typeof obj.type === 'string' || 'children' in obj || 'version' in obj;
|
|
912
|
+
}
|
|
888
913
|
function isPopulatedDoc(val) {
|
|
889
914
|
if (!val || typeof val !== 'object' || Array.isArray(val)) {
|
|
890
915
|
return false;
|
|
891
916
|
}
|
|
892
917
|
const obj = val;
|
|
893
|
-
|
|
894
|
-
|
|
918
|
+
const hasId = typeof obj.id === 'string' || typeof obj.id === 'number';
|
|
919
|
+
return (hasId && ('createdAt' in obj || 'updatedAt' in obj || 'filename' in obj || 'mimeType' in obj));
|
|
895
920
|
}
|