@verdant-web/common 2.8.0 → 2.9.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/dist/esm/authz.js.map +1 -1
- package/dist/esm/baseline.d.ts +2 -1
- package/dist/esm/diffing.d.ts +3 -2
- package/dist/esm/diffing.js +79 -19
- package/dist/esm/diffing.js.map +1 -1
- package/dist/esm/diffing.test.js +967 -183
- package/dist/esm/diffing.test.js.map +1 -1
- package/dist/esm/oids.d.ts +5 -0
- package/dist/esm/oids.js +23 -1
- package/dist/esm/oids.js.map +1 -1
- package/dist/esm/operation.d.ts +5 -4
- package/dist/esm/operation.js +4 -4
- package/dist/esm/operation.js.map +1 -1
- package/dist/esm/patch.d.ts +17 -13
- package/dist/esm/patch.js +42 -6
- package/dist/esm/patch.js.map +1 -1
- package/dist/esm/schema/fields.d.ts +1 -0
- package/dist/esm/schema/fields.js +5 -0
- package/dist/esm/schema/fields.js.map +1 -1
- package/dist/esm/schema/validation.d.ts +8 -1
- package/dist/esm/schema/validation.js +133 -63
- package/dist/esm/schema/validation.js.map +1 -1
- package/dist/esm/schema/validation.test.d.ts +1 -0
- package/dist/esm/schema/validation.test.js +60 -0
- package/dist/esm/schema/validation.test.js.map +1 -0
- package/dist/esm/timestamp.d.ts +8 -0
- package/dist/esm/timestamp.js +15 -0
- package/dist/esm/timestamp.js.map +1 -1
- package/dist/esm/undo.d.ts +2 -1
- package/dist/esm/undo.js +52 -158
- package/dist/esm/undo.js.map +1 -1
- package/dist/esm/undo.test.js +68 -7
- package/dist/esm/undo.test.js.map +1 -1
- package/package.json +1 -1
- package/src/authz.ts +2 -0
- package/src/baseline.ts +3 -1
- package/src/diffing.test.ts +1089 -225
- package/src/diffing.ts +99 -22
- package/src/oids.ts +23 -1
- package/src/operation.ts +9 -8
- package/src/patch.ts +92 -17
- package/src/schema/fields.ts +8 -0
- package/src/schema/validation.test.ts +66 -0
- package/src/schema/validation.ts +156 -73
- package/src/timestamp.ts +14 -0
- package/src/undo.test.ts +87 -8
- package/src/undo.ts +83 -157
package/src/diffing.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { AuthorizationKey } from './authz.js';
|
|
1
2
|
import { VerdantError } from './error.js';
|
|
2
3
|
import {
|
|
3
4
|
areOidsRelated,
|
|
4
5
|
assignOid,
|
|
6
|
+
createRef,
|
|
5
7
|
getOid,
|
|
6
8
|
maybeGetOid,
|
|
7
9
|
ObjectIdentifier,
|
|
@@ -27,7 +29,7 @@ export type DiffContext = {
|
|
|
27
29
|
/**
|
|
28
30
|
* Authorization to apply to all created operations
|
|
29
31
|
*/
|
|
30
|
-
authz?:
|
|
32
|
+
authz?: AuthorizationKey;
|
|
31
33
|
patchCreator: PatchCreator;
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -70,18 +72,10 @@ function enforceAssignedOid(
|
|
|
70
72
|
assignOid(newObject, existingObjectOid);
|
|
71
73
|
}
|
|
72
74
|
// NOTE: new OID assignments are done in patchCreator.
|
|
73
|
-
// else {
|
|
74
|
-
// assignOid(
|
|
75
|
-
// newObject,
|
|
76
|
-
// createSubOid(parentOid, ctx.patchCreator.createSubId),
|
|
77
|
-
// );
|
|
78
|
-
// }
|
|
79
75
|
} else if (!areOidsRelated(parentOid, oid)) {
|
|
80
76
|
// when there's any doubt, clone the whole object. false -> do not copy OIDs
|
|
81
77
|
const clone = cloneDeep(newObject, false);
|
|
82
78
|
// NOTE: new OID assignments are done in patchCreator.
|
|
83
|
-
// const cloneOid = createSubOid(parentOid, ctx.patchCreator.createSubId);
|
|
84
|
-
// assignOid(clone, cloneOid);
|
|
85
79
|
return clone;
|
|
86
80
|
}
|
|
87
81
|
return newObject;
|
|
@@ -102,7 +96,7 @@ export function diffToPatches(
|
|
|
102
96
|
merge?: boolean;
|
|
103
97
|
/** @deprecated - use 'merge' */
|
|
104
98
|
defaultUndefined?: boolean;
|
|
105
|
-
authz?:
|
|
99
|
+
authz?: AuthorizationKey;
|
|
106
100
|
},
|
|
107
101
|
) {
|
|
108
102
|
const ctx: DiffContext = {
|
|
@@ -141,7 +135,17 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
|
|
|
141
135
|
// from object must be registered with an OID.
|
|
142
136
|
const oid = getOid(from);
|
|
143
137
|
// this copy will be mutated to align with inserts, making things easier to compare.
|
|
144
|
-
const fromCopy = [
|
|
138
|
+
const fromCopy = [];
|
|
139
|
+
// while iterating from, also collect some metadata.
|
|
140
|
+
const oidsInFrom = new Set();
|
|
141
|
+
for (let i = 0; i < from.length; i++) {
|
|
142
|
+
const value = from[i];
|
|
143
|
+
fromCopy[i] = value;
|
|
144
|
+
const oid = maybeGetOid(value);
|
|
145
|
+
if (oid) {
|
|
146
|
+
oidsInFrom.add(oid);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
145
149
|
|
|
146
150
|
// first, normalize incoming data according to OID rules. this is done early
|
|
147
151
|
// so future equality checks are according to configuration like mergeUnknownObjects.
|
|
@@ -157,12 +161,17 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
|
|
|
157
161
|
// track whether the new list has gaps in it. a gap
|
|
158
162
|
// means we can no longer use list-push for new items.
|
|
159
163
|
let noGaps = true;
|
|
164
|
+
// track any items we move by reference, so we remember to
|
|
165
|
+
// not delete them later.
|
|
166
|
+
const movedItems = new Set<ObjectIdentifier>();
|
|
167
|
+
const overwrittenItems = new Set<ObjectIdentifier>();
|
|
160
168
|
for (let i = 0; i < to.length; i++) {
|
|
161
169
|
const value = to[i];
|
|
162
170
|
const oldValue = fromCopy[i];
|
|
163
171
|
if (value === undefined) {
|
|
164
172
|
noGaps = false;
|
|
165
173
|
}
|
|
174
|
+
const itemOid = maybeGetOid(value);
|
|
166
175
|
|
|
167
176
|
// we decide if this item is being added to the end of the list if:
|
|
168
177
|
// - there were no empty spaces before it in the new list
|
|
@@ -173,14 +182,49 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
|
|
|
173
182
|
// accidentally 'push' into a gap, which would put the item in the wrong place.
|
|
174
183
|
const isEndOfList = noGaps && i >= fromCopy.length;
|
|
175
184
|
|
|
176
|
-
|
|
185
|
+
// this branch exclusively deals with object references.
|
|
186
|
+
if (itemOid && oidsInFrom.has(itemOid)) {
|
|
187
|
+
// this item was in the old list, so no matter what we want to
|
|
188
|
+
// preserve its identity. the rest of the branches here all
|
|
189
|
+
// assume the item is new and initialize it.
|
|
190
|
+
if (areTheSameIdentity(value, oldValue)) {
|
|
191
|
+
// the item hasn't moved. we just diff it in place.
|
|
192
|
+
diff(oldValue, value, ctx);
|
|
193
|
+
} else {
|
|
194
|
+
// the item has moved. we are currently visiting its new index,
|
|
195
|
+
// so we can move it by reference now
|
|
196
|
+
ctx.patches.push(
|
|
197
|
+
...ctx.patchCreator.createListMoveByRef(
|
|
198
|
+
oid,
|
|
199
|
+
createRef(itemOid),
|
|
200
|
+
i,
|
|
201
|
+
ctx.authz,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
// the problem is, a move by ref is not the same as what
|
|
205
|
+
// we really observed here, since it's more like an insertion
|
|
206
|
+
// and not an overwrite. to complicate this, the replaced item
|
|
207
|
+
// at this index might actually have moved, not been replaced.
|
|
208
|
+
// we mark the item that was at this index previously, and if
|
|
209
|
+
// we don't see it later, we'll delete it.
|
|
210
|
+
const fromOid = maybeGetOid(oldValue);
|
|
211
|
+
if (fromOid) {
|
|
212
|
+
overwrittenItems.add(fromOid);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// remember this item so we don't delete it later.
|
|
216
|
+
movedItems.add(itemOid);
|
|
217
|
+
}
|
|
218
|
+
} else if (isEndOfList) {
|
|
177
219
|
// this will initialize all sub-objects in the item, too
|
|
178
220
|
ctx.patches.push(
|
|
179
221
|
...ctx.patchCreator.createListPush(oid, value, ctx.authz),
|
|
180
222
|
);
|
|
181
223
|
} else if (areTheSameIdentity(value, oldValue)) {
|
|
182
|
-
//
|
|
183
|
-
//
|
|
224
|
+
// note : since the branch above covers object references which
|
|
225
|
+
// were present in the original list, logically this branch only
|
|
226
|
+
// covers primitives. either way, call diff, it's just more
|
|
227
|
+
// proper to do so.
|
|
184
228
|
diff(oldValue, value, ctx);
|
|
185
229
|
} else {
|
|
186
230
|
// the identity of this item has changed. we can now evaluate
|
|
@@ -220,22 +264,55 @@ export function diffLists(from: any[], to: any[], ctx: DiffContext) {
|
|
|
220
264
|
}
|
|
221
265
|
}
|
|
222
266
|
|
|
223
|
-
// remove any remaining items at the end of the array
|
|
267
|
+
// remove any remaining items at the end of the array.
|
|
224
268
|
const deletedItemsAtEnd = fromCopy.length - to.length;
|
|
225
269
|
if (deletedItemsAtEnd > 0) {
|
|
270
|
+
for (let i = fromCopy.length - 1; i >= to.length; i--) {
|
|
271
|
+
const value = fromCopy[i];
|
|
272
|
+
const itemOid = maybeGetOid(value);
|
|
273
|
+
if (itemOid) {
|
|
274
|
+
// avoid deleting moved objects that were at the end of the list before.
|
|
275
|
+
// their reference was moved elsewhere, but their contents must be retained.
|
|
276
|
+
if (!movedItems.has(itemOid)) {
|
|
277
|
+
// if sub-items were objects, we need to delete them all
|
|
278
|
+
// this should recursively delete children of these items
|
|
279
|
+
// also!
|
|
280
|
+
deleteWithSubObjects(value, ctx);
|
|
281
|
+
ctx.patches.push(
|
|
282
|
+
...ctx.patchCreator.createListRemove(
|
|
283
|
+
oid,
|
|
284
|
+
createRef(itemOid),
|
|
285
|
+
'last',
|
|
286
|
+
ctx.authz,
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
ctx.patches.push(
|
|
292
|
+
...ctx.patchCreator.createListRemove(oid, value, 'last', ctx.authz),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const overwrittenOid of overwrittenItems) {
|
|
299
|
+
if (movedItems.has(overwrittenOid)) {
|
|
300
|
+
// this item was moved, not overwritten. we don't want to delete it.
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// this item was overwritten, not moved. we need to delete it.
|
|
226
304
|
// if sub-items were objects, we need to delete them all
|
|
227
305
|
// this should recursively delete children of these items
|
|
228
306
|
// also!
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
deleteWithSubObjects(
|
|
307
|
+
const item = from.find((x) => maybeGetOid(x) === overwrittenOid);
|
|
308
|
+
if (item) {
|
|
309
|
+
deleteWithSubObjects(item, ctx);
|
|
232
310
|
}
|
|
233
|
-
// push the list-delete for the deleted items
|
|
234
311
|
ctx.patches.push(
|
|
235
|
-
...ctx.patchCreator.
|
|
312
|
+
...ctx.patchCreator.createListRemove(
|
|
236
313
|
oid,
|
|
237
|
-
|
|
238
|
-
|
|
314
|
+
createRef(overwrittenOid),
|
|
315
|
+
'last',
|
|
239
316
|
ctx.authz,
|
|
240
317
|
),
|
|
241
318
|
);
|
package/src/oids.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { v4 } from 'uuid';
|
|
|
2
2
|
import { isFileRef } from './files.js';
|
|
3
3
|
import { isObjectRef, ObjectRef } from './operation.js';
|
|
4
4
|
import { isRef } from './refs.js';
|
|
5
|
-
import {
|
|
5
|
+
import { assert, isObject } from './utils.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* OIDs
|
|
@@ -421,3 +421,25 @@ function migrateForeignOid(parentOid: ObjectIdentifier, child: any) {
|
|
|
421
421
|
);
|
|
422
422
|
}
|
|
423
423
|
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Returns a list of all OIDs assigned to this object
|
|
427
|
+
* and all sub-objects.
|
|
428
|
+
*/
|
|
429
|
+
export function getAllOids(root: any) {
|
|
430
|
+
const oids = new Set<ObjectIdentifier>();
|
|
431
|
+
const stack = [root];
|
|
432
|
+
while (stack.length) {
|
|
433
|
+
const obj = stack.pop();
|
|
434
|
+
const oid = maybeGetOid(obj);
|
|
435
|
+
if (oid) {
|
|
436
|
+
oids.add(oid);
|
|
437
|
+
}
|
|
438
|
+
if (Array.isArray(obj)) {
|
|
439
|
+
stack.push(...obj);
|
|
440
|
+
} else if (isObject(obj)) {
|
|
441
|
+
stack.push(...Object.values(obj));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return Array.from(oids);
|
|
445
|
+
}
|
package/src/operation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AuthorizationKey } from './authz.js';
|
|
1
2
|
import { DocumentBaseline } from './baseline.js';
|
|
2
3
|
import { FileRef, isFileRef } from './files.js';
|
|
3
4
|
import {
|
|
@@ -151,7 +152,7 @@ export type Operation = {
|
|
|
151
152
|
oid: ObjectIdentifier;
|
|
152
153
|
timestamp: string;
|
|
153
154
|
data: OperationPatch;
|
|
154
|
-
authz?:
|
|
155
|
+
authz?: AuthorizationKey;
|
|
155
156
|
};
|
|
156
157
|
|
|
157
158
|
/**
|
|
@@ -164,7 +165,7 @@ export function initialToPatches(
|
|
|
164
165
|
getNow: () => string,
|
|
165
166
|
createSubId?: () => string,
|
|
166
167
|
patches: Operation[] = [],
|
|
167
|
-
options?: { authz?:
|
|
168
|
+
options?: { authz?: AuthorizationKey },
|
|
168
169
|
) {
|
|
169
170
|
assignOid(initial, rootOid);
|
|
170
171
|
assignOidsToAllSubObjects(initial, createSubId);
|
|
@@ -189,7 +190,7 @@ export function shallowInitialToPatches(
|
|
|
189
190
|
rootOid: ObjectIdentifier,
|
|
190
191
|
getNow: () => string,
|
|
191
192
|
patches: Operation[] = [],
|
|
192
|
-
options?: { authz?:
|
|
193
|
+
options?: { authz?: AuthorizationKey },
|
|
193
194
|
) {
|
|
194
195
|
const op: Operation = {
|
|
195
196
|
oid: rootOid,
|
|
@@ -205,7 +206,7 @@ export function shallowInitialToPatches(
|
|
|
205
206
|
|
|
206
207
|
// saves a bit of network traffic by not attaching authz
|
|
207
208
|
// key at all if not present
|
|
208
|
-
export function addAuthzToOp(op: Operation, authz?:
|
|
209
|
+
export function addAuthzToOp(op: Operation, authz?: AuthorizationKey) {
|
|
209
210
|
if (authz) {
|
|
210
211
|
op.authz = authz;
|
|
211
212
|
}
|
|
@@ -342,18 +343,18 @@ export function applyPatch<T extends NormalizedObject>(
|
|
|
342
343
|
do {
|
|
343
344
|
const valueToRemove = patch.value;
|
|
344
345
|
if (patch.only === 'last') {
|
|
345
|
-
if (
|
|
346
|
+
if (isRef(valueToRemove)) {
|
|
346
347
|
index = findLastIndex(
|
|
347
348
|
base,
|
|
348
|
-
(item: any) => item
|
|
349
|
+
(item: any) => isRef(item) && compareRefs(item, valueToRemove),
|
|
349
350
|
);
|
|
350
351
|
} else {
|
|
351
352
|
index = base.lastIndexOf(valueToRemove);
|
|
352
353
|
}
|
|
353
354
|
} else {
|
|
354
|
-
if (
|
|
355
|
+
if (isRef(valueToRemove)) {
|
|
355
356
|
index = base.findIndex(
|
|
356
|
-
(item: any) => item
|
|
357
|
+
(item: any) => isRef(item) && compareRefs(item, valueToRemove),
|
|
357
358
|
);
|
|
358
359
|
} else {
|
|
359
360
|
index = base.indexOf(valueToRemove);
|
package/src/patch.ts
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
ObjectRef,
|
|
12
12
|
Operation,
|
|
13
13
|
PropertyName,
|
|
14
|
+
PropertyValue,
|
|
15
|
+
shallowInitialToPatches,
|
|
14
16
|
} from './operation.js';
|
|
15
17
|
import { isRef } from './refs.js';
|
|
16
|
-
import { isObject } from './utils.js';
|
|
18
|
+
import { assert, isObject } from './utils.js';
|
|
17
19
|
|
|
18
20
|
export class PatchCreator {
|
|
19
21
|
constructor(
|
|
@@ -28,7 +30,13 @@ export class PatchCreator {
|
|
|
28
30
|
createDiff = (
|
|
29
31
|
from: any,
|
|
30
32
|
to: any,
|
|
31
|
-
options: {
|
|
33
|
+
options: {
|
|
34
|
+
mergeUnknownObjects?: boolean;
|
|
35
|
+
/** @deprecated use the equivalent 'merge' */
|
|
36
|
+
defaultUndefined?: boolean;
|
|
37
|
+
merge?: boolean;
|
|
38
|
+
authz?: AuthorizationKey;
|
|
39
|
+
} = {},
|
|
32
40
|
) => {
|
|
33
41
|
return diffToPatches(from, to, this.getNow, this.createSubId, [], options);
|
|
34
42
|
};
|
|
@@ -37,7 +45,17 @@ export class PatchCreator {
|
|
|
37
45
|
obj: any,
|
|
38
46
|
oid: ObjectIdentifier,
|
|
39
47
|
authz?: AuthorizationKey,
|
|
48
|
+
shallow?: boolean,
|
|
40
49
|
) => {
|
|
50
|
+
if (shallow) {
|
|
51
|
+
return shallowInitialToPatches(
|
|
52
|
+
obj,
|
|
53
|
+
oid,
|
|
54
|
+
this.getNow,
|
|
55
|
+
undefined,
|
|
56
|
+
authz ? { authz } : undefined,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
41
59
|
return initialToPatches(
|
|
42
60
|
obj,
|
|
43
61
|
oid,
|
|
@@ -52,7 +70,7 @@ export class PatchCreator {
|
|
|
52
70
|
oid: ObjectIdentifier,
|
|
53
71
|
key: PropertyName,
|
|
54
72
|
value: any,
|
|
55
|
-
authz?:
|
|
73
|
+
authz?: AuthorizationKey,
|
|
56
74
|
): Operation[] => {
|
|
57
75
|
// incoming value must be normalized. if it's not a primitive, it and all sub-objects
|
|
58
76
|
// must be created
|
|
@@ -102,7 +120,7 @@ export class PatchCreator {
|
|
|
102
120
|
createRemove = (
|
|
103
121
|
oid: ObjectIdentifier,
|
|
104
122
|
key: PropertyName,
|
|
105
|
-
authz?:
|
|
123
|
+
authz?: AuthorizationKey,
|
|
106
124
|
): Operation[] => {
|
|
107
125
|
return [
|
|
108
126
|
{
|
|
@@ -121,8 +139,9 @@ export class PatchCreator {
|
|
|
121
139
|
oid: ObjectIdentifier,
|
|
122
140
|
index: number,
|
|
123
141
|
value: any,
|
|
124
|
-
authz?:
|
|
142
|
+
authz?: AuthorizationKey,
|
|
125
143
|
): Operation[] => {
|
|
144
|
+
assert(index >= 0, 'List index must be non-negative');
|
|
126
145
|
if (this.isPrimitive(value)) {
|
|
127
146
|
return [
|
|
128
147
|
{
|
|
@@ -166,7 +185,7 @@ export class PatchCreator {
|
|
|
166
185
|
createListPush = (
|
|
167
186
|
oid: ObjectIdentifier,
|
|
168
187
|
value: any,
|
|
169
|
-
authz?:
|
|
188
|
+
authz?: AuthorizationKey,
|
|
170
189
|
): Operation[] => {
|
|
171
190
|
if (this.isPrimitive(value)) {
|
|
172
191
|
return [
|
|
@@ -202,7 +221,7 @@ export class PatchCreator {
|
|
|
202
221
|
createListAdd = (
|
|
203
222
|
oid: ObjectIdentifier,
|
|
204
223
|
value: any,
|
|
205
|
-
authz?:
|
|
224
|
+
authz?: AuthorizationKey,
|
|
206
225
|
): Operation[] => {
|
|
207
226
|
if (!this.isPrimitive(value)) {
|
|
208
227
|
return [
|
|
@@ -235,8 +254,9 @@ export class PatchCreator {
|
|
|
235
254
|
oid: ObjectIdentifier,
|
|
236
255
|
index: number,
|
|
237
256
|
value: any,
|
|
238
|
-
authz?:
|
|
257
|
+
authz?: AuthorizationKey,
|
|
239
258
|
): Operation[] => {
|
|
259
|
+
assert(index >= 0, 'List index must be non-negative');
|
|
240
260
|
if (this.isPrimitive(value)) {
|
|
241
261
|
return [
|
|
242
262
|
{
|
|
@@ -253,9 +273,18 @@ export class PatchCreator {
|
|
|
253
273
|
} else {
|
|
254
274
|
const itemOid = createSubOid(oid, this.createSubId);
|
|
255
275
|
return [
|
|
256
|
-
...initialToPatches(
|
|
257
|
-
|
|
258
|
-
|
|
276
|
+
...initialToPatches(
|
|
277
|
+
value,
|
|
278
|
+
itemOid,
|
|
279
|
+
this.getNow,
|
|
280
|
+
this.createSubId,
|
|
281
|
+
undefined,
|
|
282
|
+
authz
|
|
283
|
+
? {
|
|
284
|
+
authz,
|
|
285
|
+
}
|
|
286
|
+
: undefined,
|
|
287
|
+
),
|
|
259
288
|
{
|
|
260
289
|
oid,
|
|
261
290
|
timestamp: this.getNow(),
|
|
@@ -270,11 +299,49 @@ export class PatchCreator {
|
|
|
270
299
|
}
|
|
271
300
|
};
|
|
272
301
|
|
|
302
|
+
createListInsertMany = (
|
|
303
|
+
oid: ObjectIdentifier,
|
|
304
|
+
index: number,
|
|
305
|
+
values: any[],
|
|
306
|
+
authz?: AuthorizationKey,
|
|
307
|
+
): Operation[] => {
|
|
308
|
+
assert(index >= 0, 'List index must be non-negative');
|
|
309
|
+
const operations: Operation[] = [];
|
|
310
|
+
const refs = values.map((value) => {
|
|
311
|
+
if (this.isPrimitive(value)) return value;
|
|
312
|
+
const subOid = createSubOid(oid, this.createSubId);
|
|
313
|
+
operations.push(
|
|
314
|
+
...initialToPatches(
|
|
315
|
+
value,
|
|
316
|
+
subOid,
|
|
317
|
+
this.getNow,
|
|
318
|
+
this.createSubId,
|
|
319
|
+
undefined,
|
|
320
|
+
{
|
|
321
|
+
authz,
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
return createRef(subOid);
|
|
326
|
+
});
|
|
327
|
+
operations.push({
|
|
328
|
+
oid,
|
|
329
|
+
timestamp: this.getNow(),
|
|
330
|
+
data: {
|
|
331
|
+
op: 'list-insert',
|
|
332
|
+
values: refs,
|
|
333
|
+
index,
|
|
334
|
+
},
|
|
335
|
+
authz,
|
|
336
|
+
});
|
|
337
|
+
return operations;
|
|
338
|
+
};
|
|
339
|
+
|
|
273
340
|
createListRemove = (
|
|
274
341
|
oid: ObjectIdentifier,
|
|
275
|
-
value:
|
|
342
|
+
value: PropertyValue,
|
|
276
343
|
only?: 'first' | 'last',
|
|
277
|
-
authz?:
|
|
344
|
+
authz?: AuthorizationKey,
|
|
278
345
|
): Operation[] => {
|
|
279
346
|
return [
|
|
280
347
|
{
|
|
@@ -294,8 +361,10 @@ export class PatchCreator {
|
|
|
294
361
|
oid: ObjectIdentifier,
|
|
295
362
|
index: number,
|
|
296
363
|
count: number = 1,
|
|
297
|
-
authz?:
|
|
364
|
+
authz?: AuthorizationKey,
|
|
298
365
|
): Operation[] => {
|
|
366
|
+
assert(index >= 0, 'List index must be non-negative');
|
|
367
|
+
assert(count > 0, 'Count must be positive and non-zero');
|
|
299
368
|
return [
|
|
300
369
|
{
|
|
301
370
|
oid,
|
|
@@ -314,8 +383,9 @@ export class PatchCreator {
|
|
|
314
383
|
oid: ObjectIdentifier,
|
|
315
384
|
value: ObjectRef | FileRef,
|
|
316
385
|
index: number,
|
|
317
|
-
authz?:
|
|
386
|
+
authz?: AuthorizationKey,
|
|
318
387
|
): Operation[] => {
|
|
388
|
+
assert(index >= 0, 'List index must be non-negative');
|
|
319
389
|
return [
|
|
320
390
|
{
|
|
321
391
|
oid,
|
|
@@ -334,8 +404,10 @@ export class PatchCreator {
|
|
|
334
404
|
oid: ObjectIdentifier,
|
|
335
405
|
fromIndex: number,
|
|
336
406
|
toIndex: number,
|
|
337
|
-
authz?:
|
|
407
|
+
authz?: AuthorizationKey,
|
|
338
408
|
): Operation[] => {
|
|
409
|
+
assert(fromIndex >= 0, 'List move from index must be non-negative');
|
|
410
|
+
assert(toIndex >= 0, 'List move to index must be non-negative');
|
|
339
411
|
return [
|
|
340
412
|
{
|
|
341
413
|
oid,
|
|
@@ -350,7 +422,10 @@ export class PatchCreator {
|
|
|
350
422
|
];
|
|
351
423
|
};
|
|
352
424
|
|
|
353
|
-
createDelete = (
|
|
425
|
+
createDelete = (
|
|
426
|
+
oid: ObjectIdentifier,
|
|
427
|
+
authz?: AuthorizationKey,
|
|
428
|
+
): Operation[] => {
|
|
354
429
|
return [
|
|
355
430
|
{
|
|
356
431
|
oid,
|
package/src/schema/fields.ts
CHANGED
|
@@ -156,3 +156,11 @@ export function traverseCollectionFieldsAndRemoveExtraProperties(
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
export function isPrimitive(fieldSchema: StorageFieldSchema) {
|
|
161
|
+
return (
|
|
162
|
+
fieldSchema.type === 'string' ||
|
|
163
|
+
fieldSchema.type === 'number' ||
|
|
164
|
+
fieldSchema.type === 'boolean'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createFileRef } from '../files.js';
|
|
3
|
+
import { createRef } from '../oids.js';
|
|
4
|
+
import { schema, validateEntityField } from './index.js';
|
|
5
|
+
|
|
6
|
+
describe('validation', () => {
|
|
7
|
+
it('validates shallowly with refs', () => {
|
|
8
|
+
const fieldSchema = schema.fields.object({
|
|
9
|
+
fields: {
|
|
10
|
+
obj: schema.fields.object({
|
|
11
|
+
fields: {},
|
|
12
|
+
}),
|
|
13
|
+
file: schema.fields.file(),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
expect(
|
|
17
|
+
validateEntityField({
|
|
18
|
+
field: fieldSchema,
|
|
19
|
+
value: {
|
|
20
|
+
obj: createRef('some/id:1'),
|
|
21
|
+
file: createFileRef('somefile'),
|
|
22
|
+
},
|
|
23
|
+
fieldPath: [],
|
|
24
|
+
expectRefs: true,
|
|
25
|
+
}),
|
|
26
|
+
).toBeUndefined();
|
|
27
|
+
expect(
|
|
28
|
+
validateEntityField({
|
|
29
|
+
field: fieldSchema,
|
|
30
|
+
value: {
|
|
31
|
+
obj: createRef('some/id:1'),
|
|
32
|
+
file: createRef('some/object:1'),
|
|
33
|
+
},
|
|
34
|
+
fieldPath: [],
|
|
35
|
+
expectRefs: true,
|
|
36
|
+
}),
|
|
37
|
+
).toMatchInlineSnapshot(`
|
|
38
|
+
{
|
|
39
|
+
"fieldPath": [
|
|
40
|
+
"file",
|
|
41
|
+
],
|
|
42
|
+
"message": "Expected file ref for field file, got {"@@type":"ref","id":"some/object:1"}",
|
|
43
|
+
"type": "invalid-type",
|
|
44
|
+
}
|
|
45
|
+
`);
|
|
46
|
+
expect(
|
|
47
|
+
validateEntityField({
|
|
48
|
+
field: fieldSchema,
|
|
49
|
+
value: {
|
|
50
|
+
obj: 'foo',
|
|
51
|
+
file: createFileRef('somefile'),
|
|
52
|
+
},
|
|
53
|
+
fieldPath: [],
|
|
54
|
+
expectRefs: true,
|
|
55
|
+
}),
|
|
56
|
+
).toMatchInlineSnapshot(`
|
|
57
|
+
{
|
|
58
|
+
"fieldPath": [
|
|
59
|
+
"obj",
|
|
60
|
+
],
|
|
61
|
+
"message": "Expected ref for field obj, got "foo"",
|
|
62
|
+
"type": "invalid-type",
|
|
63
|
+
}
|
|
64
|
+
`);
|
|
65
|
+
});
|
|
66
|
+
});
|