@verdant-web/common 1.12.0 → 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 (62) 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 +2 -0
  5. package/dist/cjs/oids.js +29 -3
  6. package/dist/cjs/oids.js.map +1 -1
  7. package/dist/cjs/operation.js +12 -3
  8. package/dist/cjs/operation.js.map +1 -1
  9. package/dist/cjs/operation.test.js +151 -1
  10. package/dist/cjs/operation.test.js.map +1 -1
  11. package/dist/cjs/schema/defaults.test.js +3 -3
  12. package/dist/cjs/schema/defaults.test.js.map +1 -1
  13. package/dist/cjs/schema/{defaults.d.ts → fields.d.ts} +3 -0
  14. package/dist/cjs/schema/{defaults.js → fields.js} +48 -3
  15. package/dist/cjs/schema/fields.js.map +1 -0
  16. package/dist/cjs/schema/index.d.ts +1 -1
  17. package/dist/cjs/schema/index.js +1 -1
  18. package/dist/cjs/schema/index.js.map +1 -1
  19. package/dist/cjs/schema/validation.d.ts +3 -0
  20. package/dist/cjs/schema/validation.js +87 -0
  21. package/dist/cjs/schema/validation.js.map +1 -0
  22. package/dist/cjs/utils.d.ts +5 -1
  23. package/dist/cjs/utils.js +9 -5
  24. package/dist/cjs/utils.js.map +1 -1
  25. package/dist/esm/migration.d.ts +7 -0
  26. package/dist/esm/migration.js +47 -7
  27. package/dist/esm/migration.js.map +1 -1
  28. package/dist/esm/oids.d.ts +2 -0
  29. package/dist/esm/oids.js +26 -2
  30. package/dist/esm/oids.js.map +1 -1
  31. package/dist/esm/operation.js +13 -4
  32. package/dist/esm/operation.js.map +1 -1
  33. package/dist/esm/operation.test.js +151 -1
  34. package/dist/esm/operation.test.js.map +1 -1
  35. package/dist/esm/schema/defaults.test.js +1 -1
  36. package/dist/esm/schema/defaults.test.js.map +1 -1
  37. package/dist/esm/schema/{defaults.d.ts → fields.d.ts} +3 -0
  38. package/dist/esm/schema/{defaults.js → fields.js} +44 -2
  39. package/dist/esm/schema/fields.js.map +1 -0
  40. package/dist/esm/schema/index.d.ts +1 -1
  41. package/dist/esm/schema/index.js +1 -1
  42. package/dist/esm/schema/index.js.map +1 -1
  43. package/dist/esm/schema/validation.d.ts +3 -0
  44. package/dist/esm/schema/validation.js +82 -0
  45. package/dist/esm/schema/validation.js.map +1 -0
  46. package/dist/esm/utils.d.ts +5 -1
  47. package/dist/esm/utils.js +9 -5
  48. package/dist/esm/utils.js.map +1 -1
  49. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/src/migration.ts +74 -15
  53. package/src/oids.ts +30 -2
  54. package/src/operation.test.ts +157 -1
  55. package/src/operation.ts +19 -3
  56. package/src/schema/defaults.test.ts +1 -1
  57. package/src/schema/{defaults.ts → fields.ts} +55 -1
  58. package/src/schema/index.ts +1 -1
  59. package/src/schema/validation.ts +99 -0
  60. package/src/utils.ts +9 -5
  61. package/dist/cjs/schema/defaults.js.map +0 -1
  62. package/dist/esm/schema/defaults.js.map +0 -1
package/src/migration.ts CHANGED
@@ -6,6 +6,11 @@ import {
6
6
  StorageDocument,
7
7
  CollectionFilter,
8
8
  StorageDocumentInit,
9
+ removeExtraProperties,
10
+ assert,
11
+ hasOid,
12
+ assignOid,
13
+ getOid,
9
14
  } from './index.js';
10
15
 
11
16
  export interface DroppedCollectionMigrationStrategy<
@@ -147,6 +152,12 @@ export interface MigrationTools<
147
152
  > {
148
153
  migrate: MigrationRunner<Old, New>;
149
154
  identity: <T>(val: T) => T;
155
+ /**
156
+ * @deprecated - default field values are automatically
157
+ * applied during migration, you don't need to use this.
158
+ * Please remove it from your migrations - even old ones
159
+ * (old migrations can be updated!)
160
+ */
150
161
  withDefaults: (collectionName: string, value: any) => any;
151
162
  queries: MigrationQueries<Old>;
152
163
  mutations: MigrationMutations<New>;
@@ -171,6 +182,7 @@ export interface MigrationEngine<
171
182
  newOids: string[];
172
183
  /** Promises that should be resolved before completing the migration */
173
184
  awaitables: Promise<any>[];
185
+ log: (...messages: any[]) => void;
174
186
  }
175
187
 
176
188
  type MigrationProcedure<
@@ -226,6 +238,24 @@ export function migrate(
226
238
  const addedCollections = Object.keys(newSchema.collections).filter(
227
239
  (key) => !oldSchema.collections[key],
228
240
  );
241
+ // collections which added one or more field defaults
242
+ const autoMigratedCollections = new Set<string>();
243
+ for (const collection of changedCollections) {
244
+ const oldFields = oldSchema.collections[collection].fields;
245
+ const newFields = newSchema.collections[collection].fields;
246
+ // a new default was added - we can auto-migrate it
247
+ if (
248
+ Object.keys(newFields).some(
249
+ (key) => !oldFields[key]?.default && newFields[key]?.default,
250
+ )
251
+ ) {
252
+ autoMigratedCollections.add(collection);
253
+ }
254
+ // a field was removed - we can auto-migrate it
255
+ if (Object.keys(oldFields).some((key) => !newFields[key])) {
256
+ autoMigratedCollections.add(collection);
257
+ }
258
+ }
229
259
 
230
260
  const addedIndexes: Record<string, MigrationIndexDescription[]> = {};
231
261
  const removedIndexes: Record<string, MigrationIndexDescription[]> = {};
@@ -246,20 +276,39 @@ export function migrate(
246
276
  }
247
277
  }
248
278
 
279
+ const withDefaults = (collectionName: string, val: any) => {
280
+ return addFieldDefaults(newSchema.collections[collectionName], val);
281
+ };
282
+ const autoMigration = (collectionName: string) => (val: any) => {
283
+ const collection = newSchema.collections[collectionName];
284
+ return addFieldDefaults(collection, removeExtraProperties(collection, val));
285
+ };
286
+
249
287
  return {
250
288
  version: newSchema.version,
251
289
  migrate: async (engine: MigrationEngine<any, any>) => {
252
290
  const migratedCollections: string[] = [];
253
291
  await procedure({
254
292
  migrate: async (collection: any, strategy: any) => {
293
+ const auto = autoMigration(collection);
294
+ const wrapped = async (val: any) => {
295
+ const baseValue = await strategy(val);
296
+ // assign OID from original value in case user's strategy
297
+ // involves cloning
298
+ assignOid(baseValue, getOid(val));
299
+ const result = auto(baseValue);
300
+ return result;
301
+ };
255
302
  // @ts-ignore
256
- await engine.migrate(collection, strategy);
303
+ await engine.migrate(collection, wrapped);
257
304
  migratedCollections.push(collection);
305
+ // since the user migrated this one and we wrap their
306
+ // strategy with auto-migration, we can remove it from
307
+ // the auto-migration list
308
+ autoMigratedCollections.delete(collection);
258
309
  },
259
310
  identity: (val: any) => val,
260
- withDefaults: (collectionName: string, val: any) => {
261
- return addFieldDefaults(newSchema.collections[collectionName], val);
262
- },
311
+ withDefaults,
263
312
  info: {
264
313
  changedCollections,
265
314
  addedCollections,
@@ -269,10 +318,24 @@ export function migrate(
269
318
  mutations: engine.mutations,
270
319
  });
271
320
 
321
+ // mandatory migration of fields which had defaults added or
322
+ // fields removed but weren't migrated by the user
323
+
324
+ engine.log(
325
+ 'debug',
326
+ 'auto-migrating collections with new defaults',
327
+ autoMigratedCollections,
328
+ );
329
+ for (const name of autoMigratedCollections) {
330
+ await engine.migrate(name, autoMigration(name));
331
+ migratedCollections.push(name);
332
+ }
333
+
272
334
  const unmigrated = changedCollections.filter(
273
335
  (collection) => !migratedCollections.includes(collection),
274
336
  );
275
337
  if (unmigrated.length > 0) {
338
+ // TODO: does this deserve a full-on error?
276
339
  console.error('Unmigrated changed collections:', unmigrated);
277
340
  }
278
341
  },
@@ -376,16 +439,12 @@ export function createDefaultMigration(
376
439
  version: 0,
377
440
  collections: {},
378
441
  };
379
- return migrate(
380
- oldSchema,
381
- newSchema || schema,
382
- async ({ migrate, withDefaults, info }) => {
383
- if ((newSchema || schema).version === 1) return;
442
+ return migrate(oldSchema, newSchema || schema, async ({ migrate, info }) => {
443
+ if ((newSchema || schema).version === 1) return;
384
444
 
385
- for (const collection of info.changedCollections as any) {
386
- // @ts-ignore indefinite type resolution
387
- await migrate(collection, (old) => withDefaults(collection, old));
388
- }
389
- },
390
- );
445
+ for (const collection of info.changedCollections as any) {
446
+ // @ts-ignore indefinite type resolution
447
+ await migrate(collection, (old) => old);
448
+ }
449
+ });
391
450
  }
package/src/oids.ts CHANGED
@@ -68,6 +68,9 @@ export function assignOid(obj: any, oid: ObjectIdentifier) {
68
68
  isObject(obj),
69
69
  `Only objects can be assigned OIDs, received ${JSON.stringify(obj)}`,
70
70
  );
71
+ if (hasOid(obj)) {
72
+ removeOid(obj);
73
+ }
71
74
  oidMap.set(obj, oid);
72
75
  return obj;
73
76
  }
@@ -103,6 +106,27 @@ export function ensureOid(
103
106
  }
104
107
  }
105
108
 
109
+ export function ensureCompatibleOid(
110
+ obj: any,
111
+ rootOid: ObjectIdentifier,
112
+ createSubId?: () => string,
113
+ ) {
114
+ if (!hasOid(obj)) {
115
+ const oid = createSubOid(rootOid, createSubId);
116
+ assignOid(obj, oid);
117
+ return oid;
118
+ } else {
119
+ const existingOid = getOid(obj);
120
+ if (!areOidsRelated(existingOid, rootOid)) {
121
+ const oid = createSubOid(rootOid, createSubId);
122
+ assignOid(obj, oid);
123
+ return oid;
124
+ } else {
125
+ return getOid(obj);
126
+ }
127
+ }
128
+ }
129
+
106
130
  const SANTIIZE_PLACEHOLDERS = {
107
131
  '.': '&dot;',
108
132
  '/': '&slash;',
@@ -188,14 +212,14 @@ export function assignOidsToAllSubObjects(
188
212
  for (let i = 0; i < obj.length; i++) {
189
213
  item = obj[i];
190
214
  if (isObject(item) && !isRef(item)) {
191
- ensureOid(item, rootOid, createSubId);
215
+ ensureCompatibleOid(item, rootOid, createSubId);
192
216
  assignOidsToAllSubObjects(item, createSubId);
193
217
  }
194
218
  }
195
219
  } else if (isObject(obj) && !isRef(obj)) {
196
220
  for (const key of Object.keys(obj)) {
197
221
  if (isObject(obj[key]) && !isRef(obj[key])) {
198
- ensureOid(obj[key], rootOid, createSubId);
222
+ ensureCompatibleOid(obj[key], rootOid, createSubId);
199
223
  assignOidsToAllSubObjects(obj[key], createSubId);
200
224
  }
201
225
  }
@@ -421,3 +445,7 @@ export function getRoots(oids: ObjectIdentifier[]) {
421
445
  }
422
446
  return Array.from(set);
423
447
  }
448
+
449
+ export function areOidsRelated(oidA: ObjectIdentifier, oidB: ObjectIdentifier) {
450
+ return getOidRoot(oidA) === getOidRoot(oidB);
451
+ }
@@ -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', () => {
@@ -509,6 +515,156 @@ describe('creating diff patch operations', () => {
509
515
  ]
510
516
  `);
511
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
+ });
512
668
  });
513
669
 
514
670
  describe('substituting refs with objects', () => {
package/src/operation.ts CHANGED
@@ -1,9 +1,11 @@
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,
8
+ createSubOid,
7
9
  ensureOid,
8
10
  getOid,
9
11
  getOidRoot,
@@ -192,13 +194,19 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
192
194
  }
193
195
  } else {
194
196
  const oldValueOid = maybeGetOid(oldValue);
195
- let valueOid;
196
- if (!options.mergeUnknownObjects) {
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) {
197
206
  valueOid = ensureOid(value, oid, createSubId);
198
207
  } else {
199
208
  // if merge unknown objects is requested, we copy the previous value's oid
200
209
  // to any mirrored new value if it doesn't have one assigned already.
201
- valueOid = maybeGetOid(value);
202
210
  if (!valueOid && oldValueOid) {
203
211
  assignOid(value, oldValueOid);
204
212
  valueOid = oldValueOid;
@@ -206,6 +214,14 @@ export function diffToPatches<T extends { [key: string]: any } | any[]>(
206
214
  valueOid = ensureOid(value, oid, createSubId);
207
215
  }
208
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
+ );
209
225
 
210
226
  if (oldValue === undefined || valueOid !== oldValueOid) {
211
227
  // first case: previous value exists but the OIDs are different,
@@ -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', () => {
@@ -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';
@@ -0,0 +1,99 @@
1
+ import { LEGACY_OID_KEY, OID_KEY } from '../oids.js';
2
+ import { isObject } from '../utils.js';
3
+ import { StorageCollectionSchema, StorageFieldSchema } from './types.js';
4
+
5
+ export function validateEntity(schema: StorageCollectionSchema, entity: any) {
6
+ for (const [key, value] of Object.entries(entity)) {
7
+ if (!schema.fields[key]) {
8
+ throw new Error(`Invalid field "${key}"`);
9
+ }
10
+ if (value) {
11
+ validateEntityField(schema.fields[key], value);
12
+ }
13
+ }
14
+ }
15
+
16
+ export function validateEntityField(
17
+ field: StorageFieldSchema,
18
+ value: any,
19
+ fieldPath: string[] = [],
20
+ ) {
21
+ if (field.type === 'object') {
22
+ if (!isObject(value)) {
23
+ if (value === null && field.nullable) return;
24
+ throw new Error(
25
+ `Expected object${
26
+ field.nullable ? ' or null' : ''
27
+ } for field ${formatField(fieldPath)}}`,
28
+ );
29
+ }
30
+ for (const [key, subField] of Object.entries(field.properties)) {
31
+ if (value[key]) {
32
+ validateEntityField(subField, value[key]);
33
+ }
34
+ }
35
+ for (const key of Object.keys(value)) {
36
+ if (!field.properties[key]) {
37
+ throw new Error(
38
+ `Invalid field "${key}" on value ${formatField(fieldPath)}`,
39
+ );
40
+ }
41
+ }
42
+ } else if (field.type === 'array') {
43
+ if (!Array.isArray(value)) {
44
+ if (value === null && field.nullable) return;
45
+ throw new Error(
46
+ `Expected array${
47
+ field.nullable ? ' or null' : ''
48
+ } for field ${formatField(fieldPath)}}`,
49
+ );
50
+ }
51
+ for (const item of value) {
52
+ validateEntityField(field.items, item);
53
+ }
54
+ } else if (field.type === 'map') {
55
+ if (!isObject(value)) {
56
+ throw new Error(`Expected map for field ${formatField(fieldPath)}}`);
57
+ }
58
+ for (const [key, item] of Object.entries(value)) {
59
+ // santiy check to weed out any id field
60
+ if (key === OID_KEY || key === LEGACY_OID_KEY) continue;
61
+ validateEntityField(field.values, item);
62
+ }
63
+ } else if (field.type === 'string') {
64
+ if (typeof value !== 'string') {
65
+ if (value === null && field.nullable) return;
66
+ throw new Error(
67
+ `Expected string ${
68
+ field.nullable ? ' or null' : ''
69
+ } for field ${formatField(fieldPath)}}`,
70
+ );
71
+ }
72
+ } else if (field.type === 'boolean') {
73
+ if (typeof value !== 'boolean') {
74
+ if (value === null && field.nullable) return;
75
+ throw new Error(
76
+ `Expected boolean ${
77
+ field.nullable ? ' or null' : ''
78
+ } for field ${formatField(fieldPath)}}`,
79
+ );
80
+ }
81
+ } else if (field.type === 'number') {
82
+ if (typeof value !== 'number') {
83
+ if (value === null && field.nullable) return;
84
+ throw new Error(
85
+ `Expected number ${
86
+ field.nullable ? ' or null' : ''
87
+ } for field ${formatField(fieldPath)}}`,
88
+ );
89
+ }
90
+ } else if (field.type === 'file') {
91
+ if (value === null && !field.nullable) {
92
+ throw new Error(`Expected file for field ${formatField(fieldPath)}}`);
93
+ }
94
+ }
95
+ }
96
+
97
+ function formatField(fieldPath: string[]) {
98
+ return fieldPath.join('.');
99
+ }
package/src/utils.ts CHANGED
@@ -60,19 +60,23 @@ export function stableStringify(obj: any) {
60
60
  return JSON.stringify(obj, orderedReplacer);
61
61
  }
62
62
 
63
- export function cloneDeep<T>(obj: T): T {
64
- if (isObject(obj)) {
63
+ /**
64
+ * A version of structured cloning which preserves object identity
65
+ * references in the system.
66
+ */
67
+ export function cloneDeep<T>(obj: T, copyOids = true): T {
68
+ if (isObject(obj) || Array.isArray(obj)) {
65
69
  const oid = maybeGetOid(obj);
66
70
  let clone: any;
67
71
  if (Array.isArray(obj)) {
68
- clone = obj.map(cloneDeep) as T;
72
+ clone = obj.map((v) => cloneDeep(v, copyOids)) as T;
69
73
  } else {
70
74
  clone = {};
71
75
  for (const [key, value] of Object.entries(obj as any)) {
72
- clone[key] = cloneDeep(value);
76
+ clone[key] = cloneDeep(value, copyOids);
73
77
  }
74
78
  }
75
- if (oid) {
79
+ if (copyOids && oid) {
76
80
  assignOid(clone, oid);
77
81
  }
78
82
  return clone;
@@ -1 +0,0 @@
1
- {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../../src/schema/defaults.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../../src/schema/defaults.ts"],"names":[],"mappings":"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"}