@verdant-web/common 1.11.1 → 1.13.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 (76) hide show
  1. package/dist/cjs/migration.d.ts +7 -0
  2. package/dist/cjs/migration.js +46 -6
  3. package/dist/cjs/migration.js.map +1 -1
  4. package/dist/cjs/oids.d.ts +5 -5
  5. package/dist/cjs/oids.js +37 -22
  6. package/dist/cjs/oids.js.map +1 -1
  7. package/dist/cjs/oids.test.js +62 -44
  8. package/dist/cjs/oids.test.js.map +1 -1
  9. package/dist/cjs/operation.d.ts +2 -2
  10. package/dist/cjs/operation.js +16 -7
  11. package/dist/cjs/operation.js.map +1 -1
  12. package/dist/cjs/operation.test.js +180 -30
  13. package/dist/cjs/operation.test.js.map +1 -1
  14. package/dist/cjs/patch.d.ts +3 -3
  15. package/dist/cjs/patch.js +6 -6
  16. package/dist/cjs/patch.js.map +1 -1
  17. package/dist/cjs/schema/defaults.test.js +4 -4
  18. package/dist/cjs/schema/defaults.test.js.map +1 -1
  19. package/dist/cjs/schema/{defaults.d.ts → fields.d.ts} +3 -0
  20. package/dist/cjs/schema/{defaults.js → fields.js} +48 -3
  21. package/dist/cjs/schema/fields.js.map +1 -0
  22. package/dist/cjs/schema/index.d.ts +1 -1
  23. package/dist/cjs/schema/index.js +1 -1
  24. package/dist/cjs/schema/index.js.map +1 -1
  25. package/dist/cjs/schema/validation.d.ts +3 -0
  26. package/dist/cjs/schema/validation.js +87 -0
  27. package/dist/cjs/schema/validation.js.map +1 -0
  28. package/dist/cjs/utils.d.ts +5 -1
  29. package/dist/cjs/utils.js +9 -5
  30. package/dist/cjs/utils.js.map +1 -1
  31. package/dist/esm/migration.d.ts +7 -0
  32. package/dist/esm/migration.js +47 -7
  33. package/dist/esm/migration.js.map +1 -1
  34. package/dist/esm/oids.d.ts +5 -5
  35. package/dist/esm/oids.js +34 -21
  36. package/dist/esm/oids.js.map +1 -1
  37. package/dist/esm/oids.test.js +62 -44
  38. package/dist/esm/oids.test.js.map +1 -1
  39. package/dist/esm/operation.d.ts +2 -2
  40. package/dist/esm/operation.js +18 -9
  41. package/dist/esm/operation.js.map +1 -1
  42. package/dist/esm/operation.test.js +180 -30
  43. package/dist/esm/operation.test.js.map +1 -1
  44. package/dist/esm/patch.d.ts +3 -3
  45. package/dist/esm/patch.js +6 -6
  46. package/dist/esm/patch.js.map +1 -1
  47. package/dist/esm/schema/defaults.test.js +2 -2
  48. package/dist/esm/schema/defaults.test.js.map +1 -1
  49. package/dist/esm/schema/{defaults.d.ts → fields.d.ts} +3 -0
  50. package/dist/esm/schema/{defaults.js → fields.js} +44 -2
  51. package/dist/esm/schema/fields.js.map +1 -0
  52. package/dist/esm/schema/index.d.ts +1 -1
  53. package/dist/esm/schema/index.js +1 -1
  54. package/dist/esm/schema/index.js.map +1 -1
  55. package/dist/esm/schema/validation.d.ts +3 -0
  56. package/dist/esm/schema/validation.js +82 -0
  57. package/dist/esm/schema/validation.js.map +1 -0
  58. package/dist/esm/utils.d.ts +5 -1
  59. package/dist/esm/utils.js +9 -5
  60. package/dist/esm/utils.js.map +1 -1
  61. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +1 -1
  64. package/src/migration.ts +74 -15
  65. package/src/oids.test.ts +62 -51
  66. package/src/oids.ts +35 -26
  67. package/src/operation.test.ts +189 -41
  68. package/src/operation.ts +23 -19
  69. package/src/patch.ts +5 -15
  70. package/src/schema/defaults.test.ts +2 -2
  71. package/src/schema/{defaults.ts → fields.ts} +55 -1
  72. package/src/schema/index.ts +1 -1
  73. package/src/schema/validation.ts +99 -0
  74. package/src/utils.ts +9 -5
  75. package/dist/cjs/schema/defaults.js.map +0 -1
  76. package/dist/esm/schema/defaults.js.map +0 -1
@@ -18,7 +18,13 @@ import {
18
18
 
19
19
  function createClock(init = 0) {
20
20
  let i = init;
21
- return () => (i++).toString();
21
+ const clock = () => (i++).toString();
22
+ Object.defineProperty(clock, 'current', {
23
+ get() {
24
+ return i;
25
+ },
26
+ });
27
+ return clock;
22
28
  }
23
29
 
24
30
  describe('creating diff patch operations', () => {
@@ -28,7 +34,7 @@ describe('creating diff patch operations', () => {
28
34
  assignOid(from, 'test/a');
29
35
  const to = { foo: 'bar', baz: 'pop' };
30
36
  assignOid(to, 'test/a');
31
- const ops = diffToPatches(from, to, createClock(), []);
37
+ const ops = diffToPatches(from, to, createClock());
32
38
  expect(ops).toMatchInlineSnapshot(`
33
39
  [
34
40
  {
@@ -70,7 +76,7 @@ describe('creating diff patch operations', () => {
70
76
  },
71
77
  'test/a',
72
78
  );
73
- const patches = diffToPatches(from, to, createClock(), []);
79
+ const patches = diffToPatches(from, to, createClock());
74
80
  expect(patches).toMatchInlineSnapshot(`
75
81
  [
76
82
  {
@@ -118,7 +124,7 @@ describe('creating diff patch operations', () => {
118
124
  };
119
125
  assignOid(to, 'test/a');
120
126
  assignOid(to.baz, 'test/a:1');
121
- const patches = diffToPatches(from, to, createClock(), []);
127
+ const patches = diffToPatches(from, to, createClock());
122
128
  expect(patches).toMatchInlineSnapshot(`
123
129
  [
124
130
  {
@@ -166,7 +172,7 @@ describe('creating diff patch operations', () => {
166
172
  };
167
173
  assignOid(to, 'test/a');
168
174
  assignOid(to.baz, 'test/a:0');
169
- const patches = diffToPatches(from, to, createClock(), []);
175
+ const patches = diffToPatches(from, to, createClock());
170
176
  expect(patches).toEqual([]);
171
177
  });
172
178
  it('patches sub-objects with same identity', () => {
@@ -182,7 +188,7 @@ describe('creating diff patch operations', () => {
182
188
  };
183
189
  assignOid(to, 'test/a');
184
190
  assignOid(to.baz, 'test/a:0');
185
- const patches = diffToPatches(from, to, createClock(), []);
191
+ const patches = diffToPatches(from, to, createClock());
186
192
  expect(patches).toMatchInlineSnapshot(`
187
193
  [
188
194
  {
@@ -208,18 +214,10 @@ describe('creating diff patch operations', () => {
208
214
  foo: 'bip',
209
215
  };
210
216
  assignOid(to, 'test/a');
211
- const patches = diffToPatches(
212
- from,
213
- to,
214
- createClock(),
215
- [],
216
- undefined,
217
- [],
218
- {
219
- defaultUndefined: true,
220
- mergeUnknownObjects: true,
221
- },
222
- );
217
+ const patches = diffToPatches(from, to, createClock(), undefined, [], {
218
+ defaultUndefined: true,
219
+ mergeUnknownObjects: true,
220
+ });
223
221
  expect(patches).toMatchInlineSnapshot(`
224
222
  [
225
223
  {
@@ -303,7 +301,7 @@ describe('creating diff patch operations', () => {
303
301
  assignOid(to.baz, 'test/a:3');
304
302
  assignOid(to.baz[0], 'test/a:4');
305
303
  assignOid(to.baz[1], 'test/a:5');
306
- expect(diffToPatches(from, to, createClock(), [])).toMatchInlineSnapshot(`
304
+ expect(diffToPatches(from, to, createClock())).toMatchInlineSnapshot(`
307
305
  [
308
306
  {
309
307
  "data": {
@@ -400,7 +398,7 @@ describe('creating diff patch operations', () => {
400
398
  ],
401
399
  };
402
400
  expect(
403
- diffToPatches(from, to, createClock(), [], createClock(5), [], {
401
+ diffToPatches(from, to, createClock(), createClock(5), [], {
404
402
  mergeUnknownObjects: true,
405
403
  }),
406
404
  ).toMatchInlineSnapshot(`
@@ -411,7 +409,7 @@ describe('creating diff patch operations', () => {
411
409
  "index": 2,
412
410
  "op": "list-delete",
413
411
  },
414
- "oid": "test/a.foo.bar:1",
412
+ "oid": "test/a:1",
415
413
  "timestamp": "0",
416
414
  },
417
415
  {
@@ -421,7 +419,7 @@ describe('creating diff patch operations', () => {
421
419
  0,
422
420
  ],
423
421
  },
424
- "oid": "test/a.foo.bop:5",
422
+ "oid": "test/a:5",
425
423
  "timestamp": "1",
426
424
  },
427
425
  {
@@ -430,10 +428,10 @@ describe('creating diff patch operations', () => {
430
428
  "op": "set",
431
429
  "value": {
432
430
  "@@type": "ref",
433
- "id": "test/a.foo.bop:5",
431
+ "id": "test/a:5",
434
432
  },
435
433
  },
436
- "oid": "test/a.foo:0",
434
+ "oid": "test/a:0",
437
435
  "timestamp": "2",
438
436
  },
439
437
  {
@@ -442,7 +440,7 @@ describe('creating diff patch operations', () => {
442
440
  "op": "set",
443
441
  "value": false,
444
442
  },
445
- "oid": "test/a.baz.#:3",
443
+ "oid": "test/a:3",
446
444
  "timestamp": "3",
447
445
  },
448
446
  {
@@ -452,7 +450,7 @@ describe('creating diff patch operations', () => {
452
450
  "corge": false,
453
451
  },
454
452
  },
455
- "oid": "test/a.baz.#:6",
453
+ "oid": "test/a:6",
456
454
  "timestamp": "4",
457
455
  },
458
456
  {
@@ -461,10 +459,10 @@ describe('creating diff patch operations', () => {
461
459
  "op": "set",
462
460
  "value": {
463
461
  "@@type": "ref",
464
- "id": "test/a.baz.#:6",
462
+ "id": "test/a:6",
465
463
  },
466
464
  },
467
- "oid": "test/a.baz:2",
465
+ "oid": "test/a:2",
468
466
  "timestamp": "5",
469
467
  },
470
468
  ]
@@ -488,7 +486,7 @@ describe('creating diff patch operations', () => {
488
486
  bar: [createFileRef('def456')],
489
487
  };
490
488
  expect(
491
- diffToPatches(from, to, createClock(), [], createClock(5), [], {
489
+ diffToPatches(from, to, createClock(), createClock(5), [], {
492
490
  mergeUnknownObjects: true,
493
491
  }),
494
492
  ).toMatchInlineSnapshot(`
@@ -502,7 +500,7 @@ describe('creating diff patch operations', () => {
502
500
  "id": "abc456",
503
501
  },
504
502
  },
505
- "oid": "test/a.foo:0",
503
+ "oid": "test/a:0",
506
504
  "timestamp": "0",
507
505
  },
508
506
  {
@@ -511,12 +509,162 @@ describe('creating diff patch operations', () => {
511
509
  "index": 1,
512
510
  "op": "list-delete",
513
511
  },
514
- "oid": "test/a.bar:1",
512
+ "oid": "test/a:1",
515
513
  "timestamp": "1",
516
514
  },
517
515
  ]
518
516
  `);
519
517
  });
518
+
519
+ it('should assign a new OID to objects which had an incompatible existing OID', () => {
520
+ const from = {
521
+ foo: {
522
+ bar: [1, 2, 3],
523
+ },
524
+ };
525
+ assignOid(from, 'test/a');
526
+ assignOidsToAllSubObjects(from, createClock());
527
+ const to = {
528
+ foo: {
529
+ bar: [1, 2],
530
+ },
531
+ };
532
+ assignOid(to, 'test/a');
533
+ assignOid(to.foo, 'uff/b');
534
+ assignOidsToAllSubObjects(to.foo, createClock());
535
+ expect(
536
+ diffToPatches(from, to, createClock(), createClock(5), [], {
537
+ mergeUnknownObjects: true,
538
+ }),
539
+ ).toMatchInlineSnapshot(`
540
+ [
541
+ {
542
+ "data": {
543
+ "op": "initialize",
544
+ "value": [
545
+ 1,
546
+ 2,
547
+ ],
548
+ },
549
+ "oid": "test/a:6",
550
+ "timestamp": "0",
551
+ },
552
+ {
553
+ "data": {
554
+ "op": "initialize",
555
+ "value": {
556
+ "bar": {
557
+ "@@type": "ref",
558
+ "id": "test/a:6",
559
+ },
560
+ },
561
+ },
562
+ "oid": "test/a:5",
563
+ "timestamp": "1",
564
+ },
565
+ {
566
+ "data": {
567
+ "name": "foo",
568
+ "op": "set",
569
+ "value": {
570
+ "@@type": "ref",
571
+ "id": "test/a:5",
572
+ },
573
+ },
574
+ "oid": "test/a",
575
+ "timestamp": "2",
576
+ },
577
+ {
578
+ "data": {
579
+ "op": "delete",
580
+ },
581
+ "oid": "test/a:0",
582
+ "timestamp": "3",
583
+ },
584
+ ]
585
+ `);
586
+ });
587
+
588
+ // an edge case - if a subobject which contains nested objects changes identity,
589
+ // this gets written as a new init patch tree - we want to ensure all init subobjects
590
+ // in that tree have compatible OIDs.
591
+ it('should assign a new OID to objects which have an incompatible OID when parent identity changes', () => {
592
+ const from = {
593
+ bar: [],
594
+ };
595
+ assignOid(from, 'test/a');
596
+ assignOid(from.bar, 'test/random');
597
+ const to = {
598
+ bar: [{ baz: 1 }, { baz: 2 }],
599
+ };
600
+ assignOid(to, 'test/a');
601
+ assignOidsToAllSubObjects(to, createClock());
602
+ assignOid(to.bar[0], 'uff/b');
603
+ expect(
604
+ diffToPatches(from, to, createClock(), createClock(5), [], {
605
+ mergeUnknownObjects: true,
606
+ }),
607
+ ).toMatchInlineSnapshot(`
608
+ [
609
+ {
610
+ "data": {
611
+ "op": "initialize",
612
+ "value": {
613
+ "baz": 1,
614
+ },
615
+ },
616
+ "oid": "test/a:5",
617
+ "timestamp": "0",
618
+ },
619
+ {
620
+ "data": {
621
+ "op": "initialize",
622
+ "value": {
623
+ "baz": 2,
624
+ },
625
+ },
626
+ "oid": "test/a:2",
627
+ "timestamp": "1",
628
+ },
629
+ {
630
+ "data": {
631
+ "op": "initialize",
632
+ "value": [
633
+ {
634
+ "@@type": "ref",
635
+ "id": "test/a:5",
636
+ },
637
+ {
638
+ "@@type": "ref",
639
+ "id": "test/a:2",
640
+ },
641
+ ],
642
+ },
643
+ "oid": "test/a:0",
644
+ "timestamp": "2",
645
+ },
646
+ {
647
+ "data": {
648
+ "name": "bar",
649
+ "op": "set",
650
+ "value": {
651
+ "@@type": "ref",
652
+ "id": "test/a:0",
653
+ },
654
+ },
655
+ "oid": "test/a",
656
+ "timestamp": "3",
657
+ },
658
+ {
659
+ "data": {
660
+ "op": "delete",
661
+ },
662
+ "oid": "test/random",
663
+ "timestamp": "4",
664
+ },
665
+ ]
666
+ `);
667
+ });
520
668
  });
521
669
 
522
670
  describe('substituting refs with objects', () => {
@@ -730,7 +878,7 @@ describe('creating patches from initial state', () => {
730
878
  "bar": "baz",
731
879
  },
732
880
  },
733
- "oid": "test/a.foo:0",
881
+ "oid": "test/a:0",
734
882
  "timestamp": "0",
735
883
  },
736
884
  {
@@ -740,7 +888,7 @@ describe('creating patches from initial state', () => {
740
888
  "corge": "grault",
741
889
  },
742
890
  },
743
- "oid": "test/a.qux.#:2",
891
+ "oid": "test/a:2",
744
892
  "timestamp": "1",
745
893
  },
746
894
  {
@@ -750,7 +898,7 @@ describe('creating patches from initial state', () => {
750
898
  "oof": 1,
751
899
  },
752
900
  },
753
- "oid": "test/a.qux.#.bin:4",
901
+ "oid": "test/a:4",
754
902
  "timestamp": "2",
755
903
  },
756
904
  {
@@ -759,11 +907,11 @@ describe('creating patches from initial state', () => {
759
907
  "value": {
760
908
  "bin": {
761
909
  "@@type": "ref",
762
- "id": "test/a.qux.#.bin:4",
910
+ "id": "test/a:4",
763
911
  },
764
912
  },
765
913
  },
766
- "oid": "test/a.qux.#:3",
914
+ "oid": "test/a:3",
767
915
  "timestamp": "3",
768
916
  },
769
917
  {
@@ -772,15 +920,15 @@ describe('creating patches from initial state', () => {
772
920
  "value": [
773
921
  {
774
922
  "@@type": "ref",
775
- "id": "test/a.qux.#:2",
923
+ "id": "test/a:2",
776
924
  },
777
925
  {
778
926
  "@@type": "ref",
779
- "id": "test/a.qux.#:3",
927
+ "id": "test/a:3",
780
928
  },
781
929
  ],
782
930
  },
783
- "oid": "test/a.qux:1",
931
+ "oid": "test/a:1",
784
932
  "timestamp": "4",
785
933
  },
786
934
  {
@@ -789,11 +937,11 @@ describe('creating patches from initial state', () => {
789
937
  "value": {
790
938
  "foo": {
791
939
  "@@type": "ref",
792
- "id": "test/a.foo:0",
940
+ "id": "test/a:0",
793
941
  },
794
942
  "qux": {
795
943
  "@@type": "ref",
796
- "id": "test/a.qux:1",
944
+ "id": "test/a:1",
797
945
  },
798
946
  },
799
947
  },
package/src/operation.ts CHANGED
@@ -1,23 +1,22 @@
1
1
  import { DocumentBaseline } from './baseline.js';
2
2
  import { FileRef } from './files.js';
3
3
  import {
4
+ areOidsRelated,
4
5
  assignOid,
5
6
  assignOidsToAllSubObjects,
6
7
  createRef,
7
- decomposeOid,
8
+ createSubOid,
8
9
  ensureOid,
9
10
  getOid,
10
11
  getOidRoot,
11
12
  isOidKey,
12
- KeyPath,
13
13
  maybeGetOid,
14
14
  normalize,
15
15
  ObjectIdentifier,
16
- OID_KEY,
17
16
  removeOid,
18
17
  } from './oids.js';
19
18
  import { compareRefs, isRef } from './refs.js';
20
- import { isObject, assert, cloneDeep, findLastIndex } from './utils.js';
19
+ import { assert, cloneDeep, findLastIndex, isObject } from './utils.js';
21
20
 
22
21
  // export type ObjectIdentifier<
23
22
  // CollectionName extends string = string,
@@ -161,7 +160,6 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
161
160
  from: T,
162
161
  to: T,
163
162
  getNow: () => string,
164
- keyPath: KeyPath,
165
163
  createSubId?: () => string,
166
164
  patches: Operation[] = [],
167
165
  options: {
@@ -196,20 +194,34 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
196
194
  }
197
195
  } else {
198
196
  const oldValueOid = maybeGetOid(oldValue);
199
- let valueOid;
200
- if (!options.mergeUnknownObjects) {
201
- valueOid = ensureOid(value, oid, key, createSubId);
197
+ let valueOid = maybeGetOid(value);
198
+ // the user supplied an object which was already assigned an OID,
199
+ // but that OID is from a separate entity: this object must
200
+ // be cloned to a new identity.
201
+ if (valueOid && !areOidsRelated(oid, valueOid)) {
202
+ value = cloneDeep(value, false);
203
+ valueOid = createSubOid(oid, createSubId);
204
+ assignOid(value, valueOid);
205
+ } else if (!options.mergeUnknownObjects) {
206
+ valueOid = ensureOid(value, oid, createSubId);
202
207
  } else {
203
208
  // if merge unknown objects is requested, we copy the previous value's oid
204
209
  // to any mirrored new value if it doesn't have one assigned already.
205
- valueOid = maybeGetOid(value);
206
210
  if (!valueOid && oldValueOid) {
207
211
  assignOid(value, oldValueOid);
208
212
  valueOid = oldValueOid;
209
213
  } else {
210
- valueOid = ensureOid(value, oid, key, createSubId);
214
+ valueOid = ensureOid(value, oid, createSubId);
211
215
  }
212
216
  }
217
+ assert(
218
+ !!valueOid,
219
+ 'Error: no value OID was resolved during diff. This is a bug in Verdant.',
220
+ );
221
+ assert(
222
+ areOidsRelated(oid, valueOid),
223
+ `Error: value OID ${valueOid} is not related to parent OID ${oid}. This is a bug in Verdant.`,
224
+ );
213
225
 
214
226
  if (oldValue === undefined || valueOid !== oldValueOid) {
215
227
  // first case: previous value exists but the OIDs are different,
@@ -239,15 +251,7 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
239
251
  } else {
240
252
  // third case: OIDs are the same, meaning the identity is the same,
241
253
  // and we must diff the objects
242
- diffToPatches(
243
- oldValue,
244
- value,
245
- getNow,
246
- [...keyPath, key],
247
- createSubId,
248
- patches,
249
- options,
250
- );
254
+ diffToPatches(oldValue, value, getNow, createSubId, patches, options);
251
255
  }
252
256
  }
253
257
  }
package/src/patch.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * High-level patch creation for use with complex nested objects.
3
3
  */
4
4
 
5
- import { createRef, createSubOid, KeyPath, ObjectIdentifier } from './oids.js';
5
+ import { createRef, createSubOid, ObjectIdentifier } from './oids.js';
6
6
  import {
7
7
  diffToPatches,
8
8
  initialToPatches,
@@ -22,18 +22,9 @@ export class PatchCreator {
22
22
  createDiff = (
23
23
  from: any,
24
24
  to: any,
25
- keyPath: KeyPath,
26
25
  options: { mergeUnknownObjects?: boolean; defaultUndefined?: boolean } = {},
27
26
  ) => {
28
- return diffToPatches(
29
- from,
30
- to,
31
- this.getNow,
32
- keyPath,
33
- this.createSubId,
34
- [],
35
- options,
36
- );
27
+ return diffToPatches(from, to, this.getNow, this.createSubId, [], options);
37
28
  };
38
29
 
39
30
  createInitialize = (obj: any, oid: ObjectIdentifier) => {
@@ -44,7 +35,6 @@ export class PatchCreator {
44
35
  oid: ObjectIdentifier,
45
36
  key: PropertyName,
46
37
  value: any,
47
- keyPath: KeyPath,
48
38
  ): Operation[] => {
49
39
  // incoming value must be normalized. if it's not a primitive, it and all sub-objects
50
40
  // must be created
@@ -61,7 +51,7 @@ export class PatchCreator {
61
51
  },
62
52
  ];
63
53
  } else {
64
- const itemOid = createSubOid(oid, key, this.createSubId);
54
+ const itemOid = createSubOid(oid, this.createSubId);
65
55
  return [
66
56
  // since we're setting a complex nested object, we can initialize it wholesale.
67
57
  // no diffing to do.
@@ -106,7 +96,7 @@ export class PatchCreator {
106
96
  },
107
97
  ];
108
98
  } else {
109
- const itemOid = createSubOid(oid, 0, this.createSubId);
99
+ const itemOid = createSubOid(oid, this.createSubId);
110
100
  return [
111
101
  ...initialToPatches(value, itemOid, this.getNow),
112
102
  {
@@ -165,7 +155,7 @@ export class PatchCreator {
165
155
  },
166
156
  ];
167
157
  } else {
168
- const itemOid = createSubOid(oid, index, this.createSubId);
158
+ const itemOid = createSubOid(oid, this.createSubId);
169
159
  return [
170
160
  ...initialToPatches(value, itemOid, this.getNow),
171
161
  {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getFieldDefault } from './defaults.js';
2
+ import { getFieldDefault } from './fields.js';
3
3
  import { assignOid, createOid, getOid, maybeGetOid } from '../oids.js';
4
4
 
5
5
  describe('getting field defaults', () => {
@@ -12,7 +12,7 @@ describe('getting field defaults', () => {
12
12
  },
13
13
  } as const;
14
14
  const first = getFieldDefault(field);
15
- assignOid(first, createOid('test', '1', []));
15
+ assignOid(first, createOid('test', '1'));
16
16
  const second = getFieldDefault(field);
17
17
  expect(maybeGetOid(second)).toBe(undefined);
18
18
  });
@@ -1,13 +1,25 @@
1
1
  import { LEGACY_OID_KEY, OID_KEY } from '../oids.js';
2
+ import { isObject } from '../utils.js';
2
3
  import type { StorageFieldSchema, StorageCollectionSchema } from './types.js';
3
4
 
5
+ export function isNullable(field: StorageFieldSchema) {
6
+ if (field.type === 'any') return true;
7
+ if (field.type === 'map') return false;
8
+ return field.nullable;
9
+ }
10
+
4
11
  export function addFieldDefaults(
5
12
  collection: StorageCollectionSchema,
6
13
  value: any,
7
14
  ) {
8
15
  for (const [key, field] of Object.entries(collection.fields)) {
9
16
  const defaultValue = getFieldDefault(field);
10
- if (defaultValue !== undefined && value[key] === undefined) {
17
+ if (
18
+ (defaultValue !== undefined && value[key] === undefined) ||
19
+ // covers the case where a previously nullable field
20
+ // now has a default during a new migration
21
+ (!isNullable(field) && value[key] === null)
22
+ ) {
11
23
  value[key] = defaultValue;
12
24
  }
13
25
  if (value[key]) {
@@ -70,3 +82,45 @@ export function getFieldDefault(field: StorageFieldSchema) {
70
82
  }
71
83
  return undefined;
72
84
  }
85
+
86
+ export function removeExtraProperties(
87
+ collection: StorageCollectionSchema,
88
+ value: any,
89
+ ) {
90
+ for (const [key, fieldValue] of Object.entries(value)) {
91
+ // MUST NOT DELETE THESE!
92
+ if (key === OID_KEY || key === LEGACY_OID_KEY) continue;
93
+
94
+ if (!collection.fields[key]) {
95
+ delete value[key];
96
+ } else {
97
+ traverseCollectionFieldsAndRemoveExtraProperties(
98
+ fieldValue,
99
+ collection.fields[key],
100
+ );
101
+ }
102
+ }
103
+ return value;
104
+ }
105
+
106
+ export function traverseCollectionFieldsAndRemoveExtraProperties(
107
+ value: any,
108
+ field: StorageFieldSchema,
109
+ ) {
110
+ if (isObject(value) && field.type === 'object') {
111
+ for (const [key, fieldValue] of Object.entries(value)) {
112
+ if (!field.properties[key]) {
113
+ delete value[key];
114
+ } else {
115
+ traverseCollectionFieldsAndRemoveExtraProperties(
116
+ fieldValue,
117
+ field.properties[key],
118
+ );
119
+ }
120
+ }
121
+ } else if (Array.isArray(value) && field.type === 'array') {
122
+ for (const item of value) {
123
+ traverseCollectionFieldsAndRemoveExtraProperties(item, field.items);
124
+ }
125
+ }
126
+ }
@@ -25,4 +25,4 @@ export function schema<
25
25
  export * from './types.js';
26
26
 
27
27
  export * from './indexFilters.js';
28
- export * from './defaults.js';
28
+ export * from './fields.js';