@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.
- package/dist/cjs/migration.d.ts +7 -0
- package/dist/cjs/migration.js +46 -6
- package/dist/cjs/migration.js.map +1 -1
- package/dist/cjs/oids.d.ts +2 -0
- package/dist/cjs/oids.js +29 -3
- package/dist/cjs/oids.js.map +1 -1
- package/dist/cjs/operation.js +12 -3
- package/dist/cjs/operation.js.map +1 -1
- package/dist/cjs/operation.test.js +151 -1
- package/dist/cjs/operation.test.js.map +1 -1
- package/dist/cjs/schema/defaults.test.js +3 -3
- package/dist/cjs/schema/defaults.test.js.map +1 -1
- package/dist/cjs/schema/{defaults.d.ts → fields.d.ts} +3 -0
- package/dist/cjs/schema/{defaults.js → fields.js} +48 -3
- package/dist/cjs/schema/fields.js.map +1 -0
- package/dist/cjs/schema/index.d.ts +1 -1
- package/dist/cjs/schema/index.js +1 -1
- package/dist/cjs/schema/index.js.map +1 -1
- package/dist/cjs/schema/validation.d.ts +3 -0
- package/dist/cjs/schema/validation.js +87 -0
- package/dist/cjs/schema/validation.js.map +1 -0
- package/dist/cjs/utils.d.ts +5 -1
- package/dist/cjs/utils.js +9 -5
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/migration.d.ts +7 -0
- package/dist/esm/migration.js +47 -7
- package/dist/esm/migration.js.map +1 -1
- package/dist/esm/oids.d.ts +2 -0
- package/dist/esm/oids.js +26 -2
- package/dist/esm/oids.js.map +1 -1
- package/dist/esm/operation.js +13 -4
- package/dist/esm/operation.js.map +1 -1
- package/dist/esm/operation.test.js +151 -1
- package/dist/esm/operation.test.js.map +1 -1
- package/dist/esm/schema/defaults.test.js +1 -1
- package/dist/esm/schema/defaults.test.js.map +1 -1
- package/dist/esm/schema/{defaults.d.ts → fields.d.ts} +3 -0
- package/dist/esm/schema/{defaults.js → fields.js} +44 -2
- package/dist/esm/schema/fields.js.map +1 -0
- package/dist/esm/schema/index.d.ts +1 -1
- package/dist/esm/schema/index.js +1 -1
- package/dist/esm/schema/index.js.map +1 -1
- package/dist/esm/schema/validation.d.ts +3 -0
- package/dist/esm/schema/validation.js +82 -0
- package/dist/esm/schema/validation.js.map +1 -0
- package/dist/esm/utils.d.ts +5 -1
- package/dist/esm/utils.js +9 -5
- package/dist/esm/utils.js.map +1 -1
- package/dist/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/migration.ts +74 -15
- package/src/oids.ts +30 -2
- package/src/operation.test.ts +157 -1
- package/src/operation.ts +19 -3
- package/src/schema/defaults.test.ts +1 -1
- package/src/schema/{defaults.ts → fields.ts} +55 -1
- package/src/schema/index.ts +1 -1
- package/src/schema/validation.ts +99 -0
- package/src/utils.ts +9 -5
- package/dist/cjs/schema/defaults.js.map +0 -1
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
'.': '˙',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/operation.test.ts
CHANGED
|
@@ -18,7 +18,13 @@ import {
|
|
|
18
18
|
|
|
19
19
|
function createClock(init = 0) {
|
|
20
20
|
let i = init;
|
|
21
|
-
|
|
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
|
-
|
|
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,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 (
|
|
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
|
+
}
|
package/src/schema/index.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
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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"}
|