@korajs/merge 0.6.0 → 1.0.0-beta.1
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/LICENSE +21 -0
- package/dist/index.cjs +197 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +188 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dr. Obed Ehoneah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
maxMerge: () => maxMerge,
|
|
43
43
|
mergeAtomicOps: () => mergeAtomicOps,
|
|
44
44
|
mergeField: () => mergeField,
|
|
45
|
+
mergeObject: () => mergeObject,
|
|
45
46
|
mergeRichtext: () => mergeRichtext,
|
|
46
47
|
minMerge: () => minMerge,
|
|
47
48
|
resolveConstraintViolation: () => resolveConstraintViolation,
|
|
@@ -109,21 +110,128 @@ function addWinsSet(localArray, remoteArray, baseArray) {
|
|
|
109
110
|
for (const item of baseArray) {
|
|
110
111
|
addIfNew(serialize(item), item);
|
|
111
112
|
}
|
|
113
|
+
const additions = /* @__PURE__ */ new Map();
|
|
112
114
|
for (const item of localArray) {
|
|
113
115
|
const s = serialize(item);
|
|
114
|
-
if (addedLocal.has(s)) {
|
|
115
|
-
|
|
116
|
+
if (addedLocal.has(s) && !additions.has(s)) {
|
|
117
|
+
additions.set(s, item);
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
for (const item of remoteArray) {
|
|
119
121
|
const s = serialize(item);
|
|
120
|
-
if (addedRemote.has(s)) {
|
|
121
|
-
|
|
122
|
+
if (addedRemote.has(s) && !additions.has(s)) {
|
|
123
|
+
additions.set(s, item);
|
|
122
124
|
}
|
|
123
125
|
}
|
|
126
|
+
for (const s of [...additions.keys()].sort()) {
|
|
127
|
+
addIfNew(s, additions.get(s));
|
|
128
|
+
}
|
|
124
129
|
return result;
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
// src/strategies/object-merge.ts
|
|
133
|
+
function mergeObject(localValue, remoteValue, baseValue, localTs, remoteTs) {
|
|
134
|
+
const local = asPlainObject(localValue);
|
|
135
|
+
const remote = asPlainObject(remoteValue);
|
|
136
|
+
const base = asPlainObject(baseValue);
|
|
137
|
+
if (local === null || remote === null) {
|
|
138
|
+
const winner = lastWriteWins(localValue, remoteValue, localTs, remoteTs).value;
|
|
139
|
+
return asPlainObject(winner) ?? {};
|
|
140
|
+
}
|
|
141
|
+
const baseObj = base ?? {};
|
|
142
|
+
const result = {};
|
|
143
|
+
const keys = /* @__PURE__ */ new Set([
|
|
144
|
+
...Object.keys(local),
|
|
145
|
+
...Object.keys(remote),
|
|
146
|
+
...Object.keys(baseObj)
|
|
147
|
+
]);
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
const inLocal = key in local;
|
|
150
|
+
const inRemote = key in remote;
|
|
151
|
+
const inBase = key in baseObj;
|
|
152
|
+
const localVal = local[key];
|
|
153
|
+
const remoteVal = remote[key];
|
|
154
|
+
const baseVal = baseObj[key];
|
|
155
|
+
const localWrote = inLocal && (!inBase || !valuesEqual(localVal, baseVal));
|
|
156
|
+
const remoteWrote = inRemote && (!inBase || !valuesEqual(remoteVal, baseVal));
|
|
157
|
+
const localRemoved = !inLocal && inBase;
|
|
158
|
+
const remoteRemoved = !inRemote && inBase;
|
|
159
|
+
if (localWrote && remoteWrote) {
|
|
160
|
+
result[key] = mergeWrittenValues(localVal, remoteVal, baseVal, localTs, remoteTs);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (localWrote && !remoteRemoved) {
|
|
164
|
+
result[key] = localVal;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (remoteWrote && !localRemoved) {
|
|
168
|
+
result[key] = remoteVal;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (localWrote && remoteRemoved) {
|
|
172
|
+
result[key] = localVal;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (remoteWrote && localRemoved) {
|
|
176
|
+
result[key] = remoteVal;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (localRemoved || remoteRemoved) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (inBase) {
|
|
183
|
+
result[key] = baseVal;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function mergeWrittenValues(localVal, remoteVal, baseVal, localTs, remoteTs) {
|
|
189
|
+
if (asPlainObject(localVal) !== null && asPlainObject(remoteVal) !== null) {
|
|
190
|
+
return mergeObject(localVal, remoteVal, baseVal, localTs, remoteTs);
|
|
191
|
+
}
|
|
192
|
+
if (Array.isArray(localVal) && Array.isArray(remoteVal)) {
|
|
193
|
+
const baseArr = Array.isArray(baseVal) ? baseVal : [];
|
|
194
|
+
return addWinsSet(localVal, remoteVal, baseArr);
|
|
195
|
+
}
|
|
196
|
+
return lastWriteWins(localVal, remoteVal, localTs, remoteTs).value;
|
|
197
|
+
}
|
|
198
|
+
function asPlainObject(value) {
|
|
199
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const proto = Object.getPrototypeOf(value);
|
|
203
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function valuesEqual(a, b) {
|
|
209
|
+
if (a === b) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return stableStringify(a) === stableStringify(b);
|
|
213
|
+
}
|
|
214
|
+
function stableStringify(value) {
|
|
215
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
216
|
+
}
|
|
217
|
+
function sortKeysDeep(value) {
|
|
218
|
+
if (Array.isArray(value)) {
|
|
219
|
+
return value.map(sortKeysDeep);
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === "object" && value !== null) {
|
|
222
|
+
const proto = Object.getPrototypeOf(value);
|
|
223
|
+
if (proto === null || proto === Object.prototype) {
|
|
224
|
+
const record = value;
|
|
225
|
+
const sorted = {};
|
|
226
|
+
for (const key of Object.keys(record).sort()) {
|
|
227
|
+
sorted[key] = sortKeysDeep(record[key]);
|
|
228
|
+
}
|
|
229
|
+
return sorted;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
127
235
|
// src/strategies/atomic-merge.ts
|
|
128
236
|
function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
|
|
129
237
|
if (localAtomicOp.type !== remoteAtomicOp.type) {
|
|
@@ -187,6 +295,7 @@ function mergeAtomicOps(localAtomicOp, remoteAtomicOp, baseValue) {
|
|
|
187
295
|
}
|
|
188
296
|
|
|
189
297
|
// src/strategies/yjs-richtext.ts
|
|
298
|
+
var import_core2 = require("@korajs/core");
|
|
190
299
|
var Y = __toESM(require("yjs"), 1);
|
|
191
300
|
var TEXT_KEY = "content";
|
|
192
301
|
function mergeRichtext(localValue, remoteValue, baseValue) {
|
|
@@ -219,11 +328,17 @@ function toYjsUpdate(value) {
|
|
|
219
328
|
if (value instanceof ArrayBuffer) {
|
|
220
329
|
return new Uint8Array(value);
|
|
221
330
|
}
|
|
222
|
-
|
|
331
|
+
if ((0, import_core2.isKoraBytesValue)(value) || (0, import_core2.isLegacyNumericByteObject)(value)) {
|
|
332
|
+
const decoded = (0, import_core2.decodeBytesFromOpData)(value);
|
|
333
|
+
return decoded instanceof Uint8Array ? decoded : stringToRichtextUpdate(decoded);
|
|
334
|
+
}
|
|
335
|
+
throw new Error(
|
|
336
|
+
"Richtext value must be a string, Uint8Array, ArrayBuffer, tagged { $koraBytes } object, null, or undefined."
|
|
337
|
+
);
|
|
223
338
|
}
|
|
224
339
|
|
|
225
340
|
// src/strategies/schema-strategies.ts
|
|
226
|
-
var
|
|
341
|
+
var import_core3 = require("@korajs/core");
|
|
227
342
|
function counterMerge(localValue, remoteValue, baseValue) {
|
|
228
343
|
const base = typeof baseValue === "number" ? baseValue : 0;
|
|
229
344
|
const local = typeof localValue === "number" ? localValue : base;
|
|
@@ -306,6 +421,38 @@ function applySchemaStrategy(strategy, localValue, remoteValue, baseValue, local
|
|
|
306
421
|
|
|
307
422
|
// src/engine/field-merger.ts
|
|
308
423
|
function mergeField(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
|
|
424
|
+
const result = mergeFieldInner(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver);
|
|
425
|
+
if (fieldDescriptor.kind === "secret") {
|
|
426
|
+
return {
|
|
427
|
+
value: result.value,
|
|
428
|
+
trace: {
|
|
429
|
+
...result.trace,
|
|
430
|
+
operationA: redactOperationField(result.trace.operationA, fieldName),
|
|
431
|
+
operationB: redactOperationField(result.trace.operationB, fieldName),
|
|
432
|
+
inputA: SECRET_REDACTED,
|
|
433
|
+
inputB: SECRET_REDACTED,
|
|
434
|
+
base: result.trace.base == null ? result.trace.base : SECRET_REDACTED,
|
|
435
|
+
output: SECRET_REDACTED
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
var SECRET_REDACTED = "[secret]";
|
|
442
|
+
function redactOperationField(operation, field) {
|
|
443
|
+
const redactRecord = (record) => {
|
|
444
|
+
if (record === null || !(field in record)) {
|
|
445
|
+
return record;
|
|
446
|
+
}
|
|
447
|
+
return { ...record, [field]: SECRET_REDACTED };
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
...operation,
|
|
451
|
+
data: redactRecord(operation.data),
|
|
452
|
+
previousData: redactRecord(operation.previousData)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function mergeFieldInner(fieldName, localOp, remoteOp, baseState, fieldDescriptor, resolver) {
|
|
309
456
|
const startTime = Date.now();
|
|
310
457
|
const localData = localOp.data ?? {};
|
|
311
458
|
const remoteData = remoteOp.data ?? {};
|
|
@@ -437,7 +584,14 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
437
584
|
case "number":
|
|
438
585
|
case "boolean":
|
|
439
586
|
case "enum":
|
|
440
|
-
case "timestamp":
|
|
587
|
+
case "timestamp":
|
|
588
|
+
// A blob field's value is a small content-addressed reference; concurrent
|
|
589
|
+
// replacements of the whole reference resolve by last-write-wins (the bytes
|
|
590
|
+
// themselves are immutable and deduplicated by hash in the blob store).
|
|
591
|
+
case "blob":
|
|
592
|
+
// A secret field's stored value (ciphertext or hash) resolves by
|
|
593
|
+
// last-write-wins; the merge never sees or emits plaintext (redacted below).
|
|
594
|
+
case "secret": {
|
|
441
595
|
const lwwResult = lastWriteWins(
|
|
442
596
|
localValue,
|
|
443
597
|
remoteValue,
|
|
@@ -494,6 +648,28 @@ function autoMerge(fieldName, localOp, remoteOp, localValue, remoteValue, baseVa
|
|
|
494
648
|
startTime
|
|
495
649
|
);
|
|
496
650
|
}
|
|
651
|
+
case "object":
|
|
652
|
+
case "json": {
|
|
653
|
+
const merged = mergeObject(
|
|
654
|
+
localValue,
|
|
655
|
+
remoteValue,
|
|
656
|
+
baseValue,
|
|
657
|
+
localOp.timestamp,
|
|
658
|
+
remoteOp.timestamp
|
|
659
|
+
);
|
|
660
|
+
return createResult(
|
|
661
|
+
merged,
|
|
662
|
+
fieldName,
|
|
663
|
+
localOp,
|
|
664
|
+
remoteOp,
|
|
665
|
+
localValue,
|
|
666
|
+
remoteValue,
|
|
667
|
+
baseValue,
|
|
668
|
+
"object-lww-map",
|
|
669
|
+
1,
|
|
670
|
+
startTime
|
|
671
|
+
);
|
|
672
|
+
}
|
|
497
673
|
}
|
|
498
674
|
}
|
|
499
675
|
function createResult(value, field, operationA, operationB, inputA, inputB, base, strategy, tier, startTime) {
|
|
@@ -614,13 +790,13 @@ function matchesWhere(record, where) {
|
|
|
614
790
|
}
|
|
615
791
|
|
|
616
792
|
// src/constraints/resolvers.ts
|
|
617
|
-
var
|
|
793
|
+
var import_core4 = require("@korajs/core");
|
|
618
794
|
function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp, baseState) {
|
|
619
795
|
const startTime = Date.now();
|
|
620
796
|
const { constraint } = violation;
|
|
621
797
|
switch (constraint.onConflict) {
|
|
622
798
|
case "last-write-wins": {
|
|
623
|
-
const comparison =
|
|
799
|
+
const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
|
|
624
800
|
const winner = comparison >= 0 ? localOp : remoteOp;
|
|
625
801
|
const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
|
|
626
802
|
return createResolution(
|
|
@@ -634,7 +810,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
|
|
|
634
810
|
);
|
|
635
811
|
}
|
|
636
812
|
case "first-write-wins": {
|
|
637
|
-
const comparison =
|
|
813
|
+
const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
|
|
638
814
|
const winner = comparison <= 0 ? localOp : remoteOp;
|
|
639
815
|
const resolvedRecord = applyWinnerFields(mergedRecord, winner, violation.fields);
|
|
640
816
|
return createResolution(
|
|
@@ -650,7 +826,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
|
|
|
650
826
|
case "priority-field": {
|
|
651
827
|
const priorityField = constraint.priorityField;
|
|
652
828
|
if (priorityField === void 0) {
|
|
653
|
-
const comparison =
|
|
829
|
+
const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
|
|
654
830
|
const winner2 = comparison >= 0 ? localOp : remoteOp;
|
|
655
831
|
const resolvedRecord2 = applyWinnerFields(mergedRecord, winner2, violation.fields);
|
|
656
832
|
return createResolution(
|
|
@@ -694,7 +870,7 @@ function resolveConstraintViolation(violation, mergedRecord, localOp, remoteOp,
|
|
|
694
870
|
}
|
|
695
871
|
case "custom": {
|
|
696
872
|
if (constraint.resolve === void 0) {
|
|
697
|
-
const comparison =
|
|
873
|
+
const comparison = import_core4.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
|
|
698
874
|
const winner = comparison >= 0 ? localOp : remoteOp;
|
|
699
875
|
const resolvedRecord2 = applyWinnerFields(mergedRecord, winner, violation.fields);
|
|
700
876
|
return createResolution(
|
|
@@ -786,7 +962,7 @@ function extractFields(record, fields) {
|
|
|
786
962
|
}
|
|
787
963
|
|
|
788
964
|
// src/constraints/referential-integrity.ts
|
|
789
|
-
var
|
|
965
|
+
var import_core5 = require("@korajs/core");
|
|
790
966
|
function buildMergeRelationLookup(schema) {
|
|
791
967
|
const lookup = /* @__PURE__ */ new Map();
|
|
792
968
|
const relationNames = Object.keys(schema.relations).sort();
|
|
@@ -1057,17 +1233,17 @@ function createDeleteVsInsertTrace(deleteOp, insertOp, relation, strategy, actio
|
|
|
1057
1233
|
}
|
|
1058
1234
|
|
|
1059
1235
|
// src/engine/merge-engine.ts
|
|
1060
|
-
var
|
|
1236
|
+
var import_core7 = require("@korajs/core");
|
|
1061
1237
|
|
|
1062
1238
|
// src/constraints/state-machine-constraint.ts
|
|
1063
|
-
var
|
|
1239
|
+
var import_core6 = require("@korajs/core");
|
|
1064
1240
|
function isValid(sm, from, to) {
|
|
1065
1241
|
const constraint = {
|
|
1066
1242
|
field: sm.field,
|
|
1067
1243
|
collection: "",
|
|
1068
1244
|
transitions: sm.transitions
|
|
1069
1245
|
};
|
|
1070
|
-
return (0,
|
|
1246
|
+
return (0, import_core6.validateTransition)(constraint, from, to).valid;
|
|
1071
1247
|
}
|
|
1072
1248
|
function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, stateMachine) {
|
|
1073
1249
|
const startTime = Date.now();
|
|
@@ -1128,7 +1304,7 @@ function resolveStateMachineMerge(fieldName, localOp, remoteOp, baseState, state
|
|
|
1128
1304
|
const localValid = isValid(stateMachine, baseStateStr, localStr);
|
|
1129
1305
|
const remoteValid = isValid(stateMachine, baseStateStr, remoteStr);
|
|
1130
1306
|
if (localValid && remoteValid) {
|
|
1131
|
-
const comparison =
|
|
1307
|
+
const comparison = import_core6.HybridLogicalClock.compare(localOp.timestamp, remoteOp.timestamp);
|
|
1132
1308
|
const winner = comparison >= 0 ? localStr : remoteStr;
|
|
1133
1309
|
return makeResult(
|
|
1134
1310
|
winner,
|
|
@@ -1280,7 +1456,7 @@ var MergeEngine = class {
|
|
|
1280
1456
|
*/
|
|
1281
1457
|
mergeFields(input) {
|
|
1282
1458
|
const { local, remote, baseState, collectionDef } = input;
|
|
1283
|
-
const allFields = collectAffectedFields(local, remote
|
|
1459
|
+
const allFields = collectAffectedFields(local, remote);
|
|
1284
1460
|
const mergedData = {};
|
|
1285
1461
|
const traces = [];
|
|
1286
1462
|
for (const fieldName of allFields) {
|
|
@@ -1322,7 +1498,7 @@ var MergeEngine = class {
|
|
|
1322
1498
|
*/
|
|
1323
1499
|
mergeWithDelete(input) {
|
|
1324
1500
|
const { local, remote } = input;
|
|
1325
|
-
const comparison =
|
|
1501
|
+
const comparison = import_core7.HybridLogicalClock.compare(local.timestamp, remote.timestamp);
|
|
1326
1502
|
if (comparison >= 0) {
|
|
1327
1503
|
if (local.type === "delete") {
|
|
1328
1504
|
return { mergedData: {}, traces: [], appliedOperation: "local", sideEffects: [] };
|
|
@@ -1345,11 +1521,8 @@ var MergeEngine = class {
|
|
|
1345
1521
|
};
|
|
1346
1522
|
}
|
|
1347
1523
|
};
|
|
1348
|
-
function collectAffectedFields(local, remote
|
|
1524
|
+
function collectAffectedFields(local, remote) {
|
|
1349
1525
|
const fields = /* @__PURE__ */ new Set();
|
|
1350
|
-
for (const fieldName of Object.keys(collectionDef.fields)) {
|
|
1351
|
-
fields.add(fieldName);
|
|
1352
|
-
}
|
|
1353
1526
|
if (local.data !== null) {
|
|
1354
1527
|
for (const fieldName of Object.keys(local.data)) {
|
|
1355
1528
|
fields.add(fieldName);
|
|
@@ -1360,9 +1533,6 @@ function collectAffectedFields(local, remote, baseState, collectionDef) {
|
|
|
1360
1533
|
fields.add(fieldName);
|
|
1361
1534
|
}
|
|
1362
1535
|
}
|
|
1363
|
-
for (const fieldName of Object.keys(baseState)) {
|
|
1364
|
-
fields.add(fieldName);
|
|
1365
|
-
}
|
|
1366
1536
|
return fields;
|
|
1367
1537
|
}
|
|
1368
1538
|
function determineAppliedOperation(traces) {
|
|
@@ -1404,6 +1574,7 @@ function determineAppliedOperation(traces) {
|
|
|
1404
1574
|
maxMerge,
|
|
1405
1575
|
mergeAtomicOps,
|
|
1406
1576
|
mergeField,
|
|
1577
|
+
mergeObject,
|
|
1407
1578
|
mergeRichtext,
|
|
1408
1579
|
minMerge,
|
|
1409
1580
|
resolveConstraintViolation,
|