@verdant-web/common 2.7.3 → 2.9.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.
Files changed (52) hide show
  1. package/dist/esm/authz.js.map +1 -1
  2. package/dist/esm/baseline.d.ts +2 -1
  3. package/dist/esm/diffing.d.ts +40 -0
  4. package/dist/esm/diffing.js +317 -0
  5. package/dist/esm/diffing.js.map +1 -0
  6. package/dist/esm/diffing.test.d.ts +1 -0
  7. package/dist/esm/diffing.test.js +1680 -0
  8. package/dist/esm/diffing.test.js.map +1 -0
  9. package/dist/esm/index.d.ts +20 -19
  10. package/dist/esm/index.js +17 -16
  11. package/dist/esm/index.js.map +1 -1
  12. package/dist/esm/oids.d.ts +5 -0
  13. package/dist/esm/oids.js +23 -1
  14. package/dist/esm/oids.js.map +1 -1
  15. package/dist/esm/operation.d.ts +11 -24
  16. package/dist/esm/operation.js +32 -270
  17. package/dist/esm/operation.js.map +1 -1
  18. package/dist/esm/operation.test.js +122 -592
  19. package/dist/esm/operation.test.js.map +1 -1
  20. package/dist/esm/patch.d.ts +19 -15
  21. package/dist/esm/patch.js +79 -18
  22. package/dist/esm/patch.js.map +1 -1
  23. package/dist/esm/schema/validation.d.ts +8 -1
  24. package/dist/esm/schema/validation.js +133 -63
  25. package/dist/esm/schema/validation.js.map +1 -1
  26. package/dist/esm/schema/validation.test.d.ts +1 -0
  27. package/dist/esm/schema/validation.test.js +60 -0
  28. package/dist/esm/schema/validation.test.js.map +1 -0
  29. package/dist/esm/timestamp.d.ts +1 -0
  30. package/dist/esm/timestamp.js +3 -0
  31. package/dist/esm/timestamp.js.map +1 -1
  32. package/dist/esm/undo.d.ts +2 -1
  33. package/dist/esm/undo.js +53 -146
  34. package/dist/esm/undo.js.map +1 -1
  35. package/dist/esm/undo.test.d.ts +1 -0
  36. package/dist/esm/undo.test.js +81 -0
  37. package/dist/esm/undo.test.js.map +1 -0
  38. package/package.json +1 -1
  39. package/src/authz.ts +2 -0
  40. package/src/baseline.ts +3 -1
  41. package/src/diffing.test.ts +1803 -0
  42. package/src/diffing.ts +402 -0
  43. package/src/index.ts +21 -20
  44. package/src/oids.ts +23 -1
  45. package/src/operation.test.ts +148 -623
  46. package/src/operation.ts +45 -378
  47. package/src/patch.ts +146 -14
  48. package/src/schema/validation.test.ts +66 -0
  49. package/src/schema/validation.ts +156 -73
  50. package/src/timestamp.ts +4 -1
  51. package/src/undo.test.ts +100 -0
  52. package/src/undo.ts +83 -144
package/src/patch.ts CHANGED
@@ -3,22 +3,24 @@
3
3
  */
4
4
 
5
5
  import { AuthorizationKey } from './authz.js';
6
+ import { diffToPatches } from './diffing.js';
6
7
  import { FileRef } from './files.js';
7
8
  import { createRef, createSubOid, ObjectIdentifier } from './oids.js';
8
9
  import {
9
- diffToPatches,
10
10
  initialToPatches,
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(
20
- private getNow: () => string,
21
- private createSubId?: () => string,
22
+ readonly getNow: () => string,
23
+ readonly createSubId?: () => string,
22
24
  ) {}
23
25
 
24
26
  isPrimitive = (value: any) => {
@@ -28,7 +30,13 @@ export class PatchCreator {
28
30
  createDiff = (
29
31
  from: any,
30
32
  to: any,
31
- options: { mergeUnknownObjects?: boolean; defaultUndefined?: boolean } = {},
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,6 +70,7 @@ export class PatchCreator {
52
70
  oid: ObjectIdentifier,
53
71
  key: PropertyName,
54
72
  value: any,
73
+ authz?: AuthorizationKey,
55
74
  ): Operation[] => {
56
75
  // incoming value must be normalized. if it's not a primitive, it and all sub-objects
57
76
  // must be created
@@ -65,6 +84,7 @@ export class PatchCreator {
65
84
  name: key,
66
85
  value,
67
86
  },
87
+ authz,
68
88
  },
69
89
  ];
70
90
  } else {
@@ -72,7 +92,16 @@ export class PatchCreator {
72
92
  return [
73
93
  // since we're setting a complex nested object, we can initialize it wholesale.
74
94
  // no diffing to do.
75
- ...initialToPatches(value, itemOid, this.getNow),
95
+ ...initialToPatches(
96
+ value,
97
+ itemOid,
98
+ this.getNow,
99
+ this.createSubId,
100
+ undefined,
101
+ {
102
+ authz,
103
+ },
104
+ ),
76
105
  // then set the reference to the object
77
106
  {
78
107
  oid,
@@ -82,12 +111,17 @@ export class PatchCreator {
82
111
  value: createRef(itemOid),
83
112
  name: key,
84
113
  },
114
+ authz,
85
115
  },
86
116
  ];
87
117
  }
88
118
  };
89
119
 
90
- createRemove = (oid: ObjectIdentifier, key: PropertyName): Operation[] => {
120
+ createRemove = (
121
+ oid: ObjectIdentifier,
122
+ key: PropertyName,
123
+ authz?: AuthorizationKey,
124
+ ): Operation[] => {
91
125
  return [
92
126
  {
93
127
  oid,
@@ -96,6 +130,7 @@ export class PatchCreator {
96
130
  op: 'remove',
97
131
  name: key,
98
132
  },
133
+ authz,
99
134
  },
100
135
  ];
101
136
  };
@@ -104,7 +139,9 @@ export class PatchCreator {
104
139
  oid: ObjectIdentifier,
105
140
  index: number,
106
141
  value: any,
142
+ authz?: AuthorizationKey,
107
143
  ): Operation[] => {
144
+ assert(index >= 0, 'List index must be non-negative');
108
145
  if (this.isPrimitive(value)) {
109
146
  return [
110
147
  {
@@ -115,12 +152,22 @@ export class PatchCreator {
115
152
  index,
116
153
  value,
117
154
  },
155
+ authz,
118
156
  },
119
157
  ];
120
158
  } else {
121
159
  const itemOid = createSubOid(oid, this.createSubId);
122
160
  return [
123
- ...initialToPatches(value, itemOid, this.getNow),
161
+ ...initialToPatches(
162
+ value,
163
+ itemOid,
164
+ this.getNow,
165
+ this.createSubId,
166
+ undefined,
167
+ {
168
+ authz,
169
+ },
170
+ ),
124
171
  {
125
172
  oid,
126
173
  timestamp: this.getNow(),
@@ -129,12 +176,17 @@ export class PatchCreator {
129
176
  index,
130
177
  value: createRef(itemOid),
131
178
  },
179
+ authz,
132
180
  },
133
181
  ];
134
182
  }
135
183
  };
136
184
 
137
- createListPush = (oid: ObjectIdentifier, value: any): Operation[] => {
185
+ createListPush = (
186
+ oid: ObjectIdentifier,
187
+ value: any,
188
+ authz?: AuthorizationKey,
189
+ ): Operation[] => {
138
190
  if (this.isPrimitive(value)) {
139
191
  return [
140
192
  {
@@ -144,12 +196,15 @@ export class PatchCreator {
144
196
  op: 'list-push',
145
197
  value,
146
198
  },
199
+ authz,
147
200
  },
148
201
  ];
149
202
  } else {
150
203
  const itemOid = createSubOid(oid, this.createSubId);
151
204
  return [
152
- ...initialToPatches(value, itemOid, this.getNow),
205
+ ...initialToPatches(value, itemOid, this.getNow, undefined, undefined, {
206
+ authz,
207
+ }),
153
208
  {
154
209
  oid,
155
210
  timestamp: this.getNow(),
@@ -157,12 +212,17 @@ export class PatchCreator {
157
212
  op: 'list-push',
158
213
  value: createRef(itemOid),
159
214
  },
215
+ authz,
160
216
  },
161
217
  ];
162
218
  }
163
219
  };
164
220
 
165
- createListAdd = (oid: ObjectIdentifier, value: any): Operation[] => {
221
+ createListAdd = (
222
+ oid: ObjectIdentifier,
223
+ value: any,
224
+ authz?: AuthorizationKey,
225
+ ): Operation[] => {
166
226
  if (!this.isPrimitive(value)) {
167
227
  return [
168
228
  {
@@ -172,6 +232,7 @@ export class PatchCreator {
172
232
  op: 'list-add',
173
233
  value: createRef(value),
174
234
  },
235
+ authz,
175
236
  },
176
237
  ];
177
238
  } else {
@@ -183,6 +244,7 @@ export class PatchCreator {
183
244
  op: 'list-add',
184
245
  value,
185
246
  },
247
+ authz,
186
248
  },
187
249
  ];
188
250
  }
@@ -192,7 +254,9 @@ export class PatchCreator {
192
254
  oid: ObjectIdentifier,
193
255
  index: number,
194
256
  value: any,
257
+ authz?: AuthorizationKey,
195
258
  ): Operation[] => {
259
+ assert(index >= 0, 'List index must be non-negative');
196
260
  if (this.isPrimitive(value)) {
197
261
  return [
198
262
  {
@@ -203,12 +267,24 @@ export class PatchCreator {
203
267
  value,
204
268
  index,
205
269
  },
270
+ authz,
206
271
  },
207
272
  ];
208
273
  } else {
209
274
  const itemOid = createSubOid(oid, this.createSubId);
210
275
  return [
211
- ...initialToPatches(value, itemOid, this.getNow),
276
+ ...initialToPatches(
277
+ value,
278
+ itemOid,
279
+ this.getNow,
280
+ this.createSubId,
281
+ undefined,
282
+ authz
283
+ ? {
284
+ authz,
285
+ }
286
+ : undefined,
287
+ ),
212
288
  {
213
289
  oid,
214
290
  timestamp: this.getNow(),
@@ -217,15 +293,55 @@ export class PatchCreator {
217
293
  value: createRef(itemOid),
218
294
  index,
219
295
  },
296
+ authz,
220
297
  },
221
298
  ];
222
299
  }
223
300
  };
224
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
+
225
340
  createListRemove = (
226
341
  oid: ObjectIdentifier,
227
- value: any,
342
+ value: PropertyValue,
228
343
  only?: 'first' | 'last',
344
+ authz?: AuthorizationKey,
229
345
  ): Operation[] => {
230
346
  return [
231
347
  {
@@ -236,6 +352,7 @@ export class PatchCreator {
236
352
  value,
237
353
  only,
238
354
  },
355
+ authz,
239
356
  },
240
357
  ];
241
358
  };
@@ -244,7 +361,10 @@ export class PatchCreator {
244
361
  oid: ObjectIdentifier,
245
362
  index: number,
246
363
  count: number = 1,
364
+ authz?: AuthorizationKey,
247
365
  ): Operation[] => {
366
+ assert(index >= 0, 'List index must be non-negative');
367
+ assert(count > 0, 'Count must be positive and non-zero');
248
368
  return [
249
369
  {
250
370
  oid,
@@ -254,6 +374,7 @@ export class PatchCreator {
254
374
  index,
255
375
  count,
256
376
  },
377
+ authz,
257
378
  },
258
379
  ];
259
380
  };
@@ -262,7 +383,9 @@ export class PatchCreator {
262
383
  oid: ObjectIdentifier,
263
384
  value: ObjectRef | FileRef,
264
385
  index: number,
386
+ authz?: AuthorizationKey,
265
387
  ): Operation[] => {
388
+ assert(index >= 0, 'List index must be non-negative');
266
389
  return [
267
390
  {
268
391
  oid,
@@ -272,6 +395,7 @@ export class PatchCreator {
272
395
  value,
273
396
  index,
274
397
  },
398
+ authz,
275
399
  },
276
400
  ];
277
401
  };
@@ -280,7 +404,10 @@ export class PatchCreator {
280
404
  oid: ObjectIdentifier,
281
405
  fromIndex: number,
282
406
  toIndex: number,
407
+ authz?: AuthorizationKey,
283
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');
284
411
  return [
285
412
  {
286
413
  oid,
@@ -290,11 +417,15 @@ export class PatchCreator {
290
417
  from: fromIndex,
291
418
  to: toIndex,
292
419
  },
420
+ authz,
293
421
  },
294
422
  ];
295
423
  };
296
424
 
297
- createDelete = (oid: ObjectIdentifier): Operation[] => {
425
+ createDelete = (
426
+ oid: ObjectIdentifier,
427
+ authz?: AuthorizationKey,
428
+ ): Operation[] => {
298
429
  return [
299
430
  {
300
431
  oid,
@@ -302,6 +433,7 @@ export class PatchCreator {
302
433
  data: {
303
434
  op: 'delete',
304
435
  },
436
+ authz,
305
437
  },
306
438
  ];
307
439
  };
@@ -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
+ });