@verdant-web/store 3.6.3 → 3.6.4
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/bundle/index.js +6 -6
- package/dist/bundle/index.js.map +3 -3
- package/dist/esm/__tests__/documents.test.js +22 -0
- package/dist/esm/__tests__/documents.test.js.map +1 -1
- package/dist/esm/__tests__/mutations.test.js +40 -0
- package/dist/esm/__tests__/mutations.test.js.map +1 -1
- package/dist/esm/entities/Entity.d.ts +24 -1
- package/dist/esm/entities/Entity.js +66 -20
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/Entity.test.js +1 -0
- package/dist/esm/entities/Entity.test.js.map +1 -1
- package/dist/esm/entities/EntityStore.js +1 -0
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/__tests__/documents.test.ts +28 -0
- package/src/__tests__/mutations.test.ts +53 -0
- package/src/entities/Entity.test.ts +1 -0
- package/src/entities/Entity.ts +71 -18
- package/src/entities/EntityStore.ts +1 -0
- package/src/entities/types.ts +11 -0
|
@@ -42,6 +42,7 @@ export type DeepPartial<T> = T extends object ? {
|
|
|
42
42
|
} : T;
|
|
43
43
|
export interface ObjectEntity<Init, Value extends BaseEntityValue, Snapshot = DataFromInit<Init>> extends BaseEntity<Init, Value, Snapshot> {
|
|
44
44
|
keys(): string[];
|
|
45
|
+
readonly size: number;
|
|
45
46
|
entries(): [string, Exclude<Value[keyof Value], undefined>][];
|
|
46
47
|
values(): Exclude<Value[keyof Value], undefined>[];
|
|
47
48
|
set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
|
|
@@ -69,6 +70,16 @@ export interface ObjectEntity<Init, Value extends BaseEntityValue, Snapshot = Da
|
|
|
69
70
|
*/
|
|
70
71
|
merge?: boolean;
|
|
71
72
|
}): void;
|
|
73
|
+
/**
|
|
74
|
+
* Deletes the entity from either its parent (if it's a nested value)
|
|
75
|
+
* or the database itself. WARNING: this method is tricky. It will
|
|
76
|
+
* throw an error on nested fields which are not deletable in the
|
|
77
|
+
* schema. Deleting any entity and then attempting to access its
|
|
78
|
+
* data will also result in an error.
|
|
79
|
+
*
|
|
80
|
+
* Prefer using client.<collection>.delete(id) instead.
|
|
81
|
+
*/
|
|
82
|
+
deleteSelf(): void;
|
|
72
83
|
readonly isList: false;
|
|
73
84
|
}
|
|
74
85
|
export interface ListEntity<Init, Value extends BaseEntityValue, Snapshot = DataFromInit<Init>> extends Iterable<ListItemValue<Value>>, BaseEntity<Init, Value, Snapshot> {
|
package/package.json
CHANGED
|
@@ -463,6 +463,20 @@ describe('storage documents', () => {
|
|
|
463
463
|
expect(item1.get('tags').deepUpdatedAt).not.toEqual(time.getTime());
|
|
464
464
|
});
|
|
465
465
|
|
|
466
|
+
it('should expose namespace', async () => {
|
|
467
|
+
const storage = await createTestStorage();
|
|
468
|
+
|
|
469
|
+
const item1 = await storage.todos.put({
|
|
470
|
+
content: 'item 1',
|
|
471
|
+
done: false,
|
|
472
|
+
tags: [],
|
|
473
|
+
category: 'general',
|
|
474
|
+
attachments: [],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect(item1.namespace).toBe('test');
|
|
478
|
+
});
|
|
479
|
+
|
|
466
480
|
it('should allow creating a new document from another document snapshot', async () => {
|
|
467
481
|
const storage = await createTestStorage();
|
|
468
482
|
const item1 = await storage.todos.put({
|
|
@@ -592,4 +606,18 @@ describe('storage documents', () => {
|
|
|
592
606
|
expect(fooArray.get(0)).toEqual('bar');
|
|
593
607
|
expect(arrayMap.get('foo').get(0)).toEqual('bar');
|
|
594
608
|
});
|
|
609
|
+
|
|
610
|
+
it('should not fire change event on delete', async () => {
|
|
611
|
+
const storage = await createTestStorage();
|
|
612
|
+
const created = await storage.weirds.put({});
|
|
613
|
+
const weird = await storage.weirds.get(created.get('id')).resolved!;
|
|
614
|
+
|
|
615
|
+
weird.subscribe('change', () => {
|
|
616
|
+
// never get a change with deleted data
|
|
617
|
+
expect(weird.deleted).toBe(false);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
await storage.weirds.delete(weird.get('id'));
|
|
621
|
+
expect(true).toBe(true);
|
|
622
|
+
});
|
|
595
623
|
});
|
|
@@ -54,4 +54,57 @@ describe('mutations', () => {
|
|
|
54
54
|
expect(itemAExists).toBeNull();
|
|
55
55
|
expect(itemBExists === itemB).toBe(true);
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('on entities', () => {
|
|
59
|
+
it('should allow them to delete themselves (root level)', async () => {
|
|
60
|
+
const client = await createTestStorage();
|
|
61
|
+
|
|
62
|
+
const itemA = await client.todos.put({
|
|
63
|
+
id: '1',
|
|
64
|
+
content: 'itemA',
|
|
65
|
+
category: 'test',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await itemA.deleteSelf();
|
|
69
|
+
|
|
70
|
+
const itemAExists = await client.todos.get('1').resolved;
|
|
71
|
+
|
|
72
|
+
expect(itemAExists).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should allow them to delete themselves (nested array)', async () => {
|
|
76
|
+
const client = await createTestStorage();
|
|
77
|
+
|
|
78
|
+
const itemA = await client.todos.put({
|
|
79
|
+
id: '1',
|
|
80
|
+
content: 'itemA',
|
|
81
|
+
category: 'test',
|
|
82
|
+
attachments: [
|
|
83
|
+
{
|
|
84
|
+
name: 'foo',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
itemA.get('attachments').get(0).deleteSelf();
|
|
90
|
+
|
|
91
|
+
expect(itemA.get('attachments').length).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should allow them to delete themselves (nested map)', async () => {
|
|
95
|
+
const client = await createTestStorage();
|
|
96
|
+
|
|
97
|
+
const weird = await client.weirds.put({
|
|
98
|
+
objectMap: {
|
|
99
|
+
foo: {
|
|
100
|
+
content: 'bar',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
weird.get('objectMap').get('foo').deleteSelf();
|
|
106
|
+
|
|
107
|
+
expect(weird.get('objectMap').size).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
57
110
|
});
|
package/src/entities/Entity.ts
CHANGED
|
@@ -58,6 +58,7 @@ export interface EntityInit {
|
|
|
58
58
|
fieldPath?: (string | number)[];
|
|
59
59
|
patchCreator: PatchCreator;
|
|
60
60
|
events: EntityStoreEvents;
|
|
61
|
+
deleteSelf: () => void;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export class Entity<
|
|
@@ -94,6 +95,9 @@ export class Entity<
|
|
|
94
95
|
// only used for root entities to track delete/restore state.
|
|
95
96
|
private wasDeletedLastChange = false;
|
|
96
97
|
private cachedView: any | undefined = undefined;
|
|
98
|
+
// provided from external creators, this is a method to delete
|
|
99
|
+
// this entity.
|
|
100
|
+
private _deleteSelf: () => void;
|
|
97
101
|
|
|
98
102
|
constructor({
|
|
99
103
|
oid,
|
|
@@ -106,6 +110,7 @@ export class Entity<
|
|
|
106
110
|
files,
|
|
107
111
|
patchCreator,
|
|
108
112
|
events,
|
|
113
|
+
deleteSelf,
|
|
109
114
|
}: EntityInit) {
|
|
110
115
|
super();
|
|
111
116
|
|
|
@@ -125,6 +130,7 @@ export class Entity<
|
|
|
125
130
|
this.metadataFamily = metadataFamily;
|
|
126
131
|
this.events = events;
|
|
127
132
|
this.parent = parent;
|
|
133
|
+
this._deleteSelf = deleteSelf;
|
|
128
134
|
|
|
129
135
|
// TODO: should any but the root entity be listening to these?
|
|
130
136
|
if (!this.parent) {
|
|
@@ -366,6 +372,15 @@ export class Entity<
|
|
|
366
372
|
return this.viewData.fromOlderVersion;
|
|
367
373
|
}
|
|
368
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Returns the storage namespace this entity came from. For example, if you
|
|
377
|
+
* have multiple stores initialized from the same schema, you can use this
|
|
378
|
+
* to figure out where an isolated entity was created / stored.
|
|
379
|
+
*/
|
|
380
|
+
get namespace() {
|
|
381
|
+
return this.ctx.namespace;
|
|
382
|
+
}
|
|
383
|
+
|
|
369
384
|
/**
|
|
370
385
|
* Pruning - when entities have invalid children, we 'prune' that
|
|
371
386
|
* data up to the nearest prunable point - a nullable field,
|
|
@@ -465,25 +480,10 @@ export class Entity<
|
|
|
465
480
|
// reset cached view
|
|
466
481
|
this._viewData = undefined;
|
|
467
482
|
this.cachedView = undefined;
|
|
468
|
-
// chain deepChanges to parents
|
|
469
|
-
this.deepChange(this, ev);
|
|
470
|
-
// emit the change, it's for us
|
|
471
|
-
this.ctx.log('Emitting change event', this.oid);
|
|
472
|
-
this.emit('change', { isLocal: ev.isLocal });
|
|
473
|
-
// for root entities, we need to go ahead and decide if we're
|
|
474
|
-
// deleted or not - so queries can exclude us if we are.
|
|
475
483
|
if (!this.parent) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.emit('delete', { isLocal: ev.isLocal });
|
|
480
|
-
this.wasDeletedLastChange = true;
|
|
481
|
-
} else if (!this.deleted && this.wasDeletedLastChange) {
|
|
482
|
-
this.ctx.log('debug', 'Entity restored', this.oid);
|
|
483
|
-
// newly restored - emit event
|
|
484
|
-
this.emit('restore', { isLocal: ev.isLocal });
|
|
485
|
-
this.wasDeletedLastChange = false;
|
|
486
|
-
}
|
|
484
|
+
this.changeRoot(ev);
|
|
485
|
+
} else {
|
|
486
|
+
this.changeNested(ev);
|
|
487
487
|
}
|
|
488
488
|
} else {
|
|
489
489
|
// forward it to the correct family member. if none exists
|
|
@@ -494,6 +494,36 @@ export class Entity<
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
};
|
|
497
|
+
private changeRoot = (ev: EntityChange) => {
|
|
498
|
+
// for root entities, we need to determine if we're deleted or not
|
|
499
|
+
// before firing any events
|
|
500
|
+
if (this.deleted) {
|
|
501
|
+
if (!this.wasDeletedLastChange) {
|
|
502
|
+
this.ctx.log('debug', 'Entity deleted', this.oid);
|
|
503
|
+
this.emit('delete', { isLocal: ev.isLocal });
|
|
504
|
+
this.wasDeletedLastChange = true;
|
|
505
|
+
}
|
|
506
|
+
// already deleted, do nothing.
|
|
507
|
+
} else {
|
|
508
|
+
if (this.wasDeletedLastChange) {
|
|
509
|
+
this.ctx.log('debug', 'Entity restored', this.oid);
|
|
510
|
+
this.emit('restore', { isLocal: ev.isLocal });
|
|
511
|
+
this.wasDeletedLastChange = false;
|
|
512
|
+
}
|
|
513
|
+
// emit deepchange, too
|
|
514
|
+
this.deepChange(this, ev);
|
|
515
|
+
// emit the change, it's for us
|
|
516
|
+
this.ctx.log('Emitting change event', this.oid);
|
|
517
|
+
this.emit('change', { isLocal: ev.isLocal });
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
private changeNested = (ev: EntityChange) => {
|
|
521
|
+
// chain deepChanges to parents
|
|
522
|
+
this.deepChange(this, ev);
|
|
523
|
+
// emit the change, it's for us
|
|
524
|
+
this.ctx.log('Emitting change event', this.oid);
|
|
525
|
+
this.emit('change', { isLocal: ev.isLocal });
|
|
526
|
+
};
|
|
497
527
|
protected deepChange = (target: Entity, ev: EntityChange) => {
|
|
498
528
|
// reset cached deep updated at timestamp; either this
|
|
499
529
|
// entity or children have changed
|
|
@@ -530,6 +560,7 @@ export class Entity<
|
|
|
530
560
|
fieldPath: [...this.fieldPath, key],
|
|
531
561
|
patchCreator: this.patchCreator,
|
|
532
562
|
events: this.events,
|
|
563
|
+
deleteSelf: this.delete.bind(this, key),
|
|
533
564
|
});
|
|
534
565
|
};
|
|
535
566
|
|
|
@@ -781,6 +812,13 @@ export class Entity<
|
|
|
781
812
|
return Object.values(this.view);
|
|
782
813
|
};
|
|
783
814
|
|
|
815
|
+
get size() {
|
|
816
|
+
if (this.isList) {
|
|
817
|
+
return this.length;
|
|
818
|
+
}
|
|
819
|
+
return this.keys().length;
|
|
820
|
+
}
|
|
821
|
+
|
|
784
822
|
update = (
|
|
785
823
|
data: any,
|
|
786
824
|
{
|
|
@@ -976,6 +1014,21 @@ export class Entity<
|
|
|
976
1014
|
|
|
977
1015
|
includes = this.has;
|
|
978
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Deletes this entity. WARNING: this can be tricky to
|
|
1019
|
+
* use correctly. You must not reference this entity
|
|
1020
|
+
* instance in any way after the deletion happens, or
|
|
1021
|
+
* you will get an error!
|
|
1022
|
+
*
|
|
1023
|
+
* It's a little easier to delete using client.delete
|
|
1024
|
+
* if you can manage it with your app's code. For example,
|
|
1025
|
+
* in React, use hooks.useClient() to get the client and
|
|
1026
|
+
* call delete from there.
|
|
1027
|
+
*/
|
|
1028
|
+
deleteSelf = () => {
|
|
1029
|
+
return this._deleteSelf();
|
|
1030
|
+
};
|
|
1031
|
+
|
|
979
1032
|
// TODO: make these escape hatches unnecessary
|
|
980
1033
|
__getViewData__ = (oid: ObjectIdentifier, type: 'confirmed' | 'pending') => {
|
|
981
1034
|
return this.metadataFamily.get(oid).computeView(type === 'confirmed');
|
package/src/entities/types.ts
CHANGED
|
@@ -84,6 +84,7 @@ export interface ObjectEntity<
|
|
|
84
84
|
Snapshot = DataFromInit<Init>,
|
|
85
85
|
> extends BaseEntity<Init, Value, Snapshot> {
|
|
86
86
|
keys(): string[];
|
|
87
|
+
readonly size: number;
|
|
87
88
|
entries(): [string, Exclude<Value[keyof Value], undefined>][];
|
|
88
89
|
values(): Exclude<Value[keyof Value], undefined>[];
|
|
89
90
|
set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
|
|
@@ -114,6 +115,16 @@ export interface ObjectEntity<
|
|
|
114
115
|
merge?: boolean;
|
|
115
116
|
},
|
|
116
117
|
): void;
|
|
118
|
+
/**
|
|
119
|
+
* Deletes the entity from either its parent (if it's a nested value)
|
|
120
|
+
* or the database itself. WARNING: this method is tricky. It will
|
|
121
|
+
* throw an error on nested fields which are not deletable in the
|
|
122
|
+
* schema. Deleting any entity and then attempting to access its
|
|
123
|
+
* data will also result in an error.
|
|
124
|
+
*
|
|
125
|
+
* Prefer using client.<collection>.delete(id) instead.
|
|
126
|
+
*/
|
|
127
|
+
deleteSelf(): void;
|
|
117
128
|
readonly isList: false;
|
|
118
129
|
}
|
|
119
130
|
|