@verdant-web/store 2.8.5 → 3.0.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/bundle/index.js +9 -10
- package/dist/bundle/index.js.map +4 -4
- package/dist/cjs/DocumentManager.d.ts +1 -1
- package/dist/cjs/DocumentManager.js +1 -1
- package/dist/cjs/DocumentManager.js.map +1 -1
- package/dist/cjs/IDBService.d.ts +28 -7
- package/dist/cjs/IDBService.js +50 -13
- package/dist/cjs/IDBService.js.map +1 -1
- package/dist/cjs/UndoHistory.d.ts +1 -1
- package/dist/cjs/UndoHistory.js +6 -2
- package/dist/cjs/UndoHistory.js.map +1 -1
- package/dist/cjs/__tests__/batching.test.js +3 -1
- package/dist/cjs/__tests__/batching.test.js.map +1 -1
- package/dist/cjs/__tests__/documents.test.js +37 -6
- package/dist/cjs/__tests__/documents.test.js.map +1 -1
- package/dist/cjs/__tests__/fixtures/testStorage.d.ts +2 -2
- package/dist/cjs/__tests__/fixtures/testStorage.js +2 -1
- package/dist/cjs/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/cjs/__tests__/legacyOids.test.js +50 -17
- package/dist/cjs/__tests__/legacyOids.test.js.map +1 -1
- package/dist/cjs/__tests__/mutations.test.js +9 -3
- package/dist/cjs/__tests__/mutations.test.js.map +1 -1
- package/dist/cjs/__tests__/queries.test.js +6 -2
- package/dist/cjs/__tests__/queries.test.js.map +1 -1
- package/dist/cjs/__tests__/setup/indexedDB.d.ts +1 -1
- package/dist/cjs/__tests__/setup/indexedDB.js +8 -1
- package/dist/cjs/__tests__/setup/indexedDB.js.map +1 -1
- package/dist/cjs/__tests__/undo.test.js +16 -9
- package/dist/cjs/__tests__/undo.test.js.map +1 -1
- package/dist/cjs/client/Client.d.ts +1 -1
- package/dist/cjs/client/Client.js +7 -3
- package/dist/cjs/client/Client.js.map +1 -1
- package/dist/cjs/client/ClientDescriptor.js +21 -6
- package/dist/cjs/client/ClientDescriptor.js.map +1 -1
- package/dist/cjs/context.d.ts +10 -1
- package/dist/cjs/entities/Entity.d.ts +106 -178
- package/dist/cjs/entities/Entity.js +558 -376
- package/dist/cjs/entities/Entity.js.map +1 -1
- package/dist/cjs/entities/Entity.test.d.ts +1 -0
- package/dist/cjs/entities/Entity.test.js +194 -0
- package/dist/cjs/entities/Entity.test.js.map +1 -0
- package/dist/cjs/entities/EntityCache.d.ts +15 -0
- package/dist/cjs/entities/EntityCache.js +39 -0
- package/dist/cjs/entities/EntityCache.js.map +1 -0
- package/dist/cjs/entities/EntityMetadata.d.ts +68 -0
- package/dist/cjs/entities/EntityMetadata.js +261 -0
- package/dist/cjs/entities/EntityMetadata.js.map +1 -0
- package/dist/cjs/entities/EntityStore.d.ts +63 -68
- package/dist/cjs/entities/EntityStore.js +294 -438
- package/dist/cjs/entities/EntityStore.js.map +1 -1
- package/dist/cjs/entities/OperationBatcher.d.ts +52 -0
- package/dist/cjs/entities/OperationBatcher.js +165 -0
- package/dist/cjs/entities/OperationBatcher.js.map +1 -0
- package/dist/cjs/entities/types.d.ts +84 -0
- package/dist/cjs/entities/types.js +3 -0
- package/dist/cjs/entities/types.js.map +1 -0
- package/dist/cjs/files/EntityFile.d.ts +5 -2
- package/dist/cjs/files/EntityFile.js +8 -4
- package/dist/cjs/files/EntityFile.js.map +1 -1
- package/dist/cjs/files/FileManager.d.ts +3 -1
- package/dist/cjs/files/FileManager.js +5 -3
- package/dist/cjs/files/FileManager.js.map +1 -1
- package/dist/cjs/files/FileStorage.js +7 -7
- package/dist/cjs/files/FileStorage.js.map +1 -1
- package/dist/cjs/files/utils.d.ts +2 -0
- package/dist/cjs/files/utils.js +5 -2
- package/dist/cjs/files/utils.js.map +1 -1
- package/dist/cjs/idb.d.ts +2 -0
- package/dist/cjs/idb.js +50 -4
- package/dist/cjs/idb.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/metadata/AckInfoStore.js +1 -1
- package/dist/cjs/metadata/AckInfoStore.js.map +1 -1
- package/dist/cjs/metadata/BaselinesStore.d.ts +4 -1
- package/dist/cjs/metadata/BaselinesStore.js +19 -10
- package/dist/cjs/metadata/BaselinesStore.js.map +1 -1
- package/dist/cjs/metadata/LocalReplicaStore.d.ts +1 -1
- package/dist/cjs/metadata/LocalReplicaStore.js +11 -5
- package/dist/cjs/metadata/LocalReplicaStore.js.map +1 -1
- package/dist/cjs/metadata/Metadata.d.ts +26 -5
- package/dist/cjs/metadata/Metadata.js +55 -18
- package/dist/cjs/metadata/Metadata.js.map +1 -1
- package/dist/cjs/metadata/OperationsStore.d.ts +3 -0
- package/dist/cjs/metadata/OperationsStore.js +35 -15
- package/dist/cjs/metadata/OperationsStore.js.map +1 -1
- package/dist/cjs/migration/openDatabase.js +31 -10
- package/dist/cjs/migration/openDatabase.js.map +1 -1
- package/dist/cjs/queries/BaseQuery.js +13 -1
- package/dist/cjs/queries/BaseQuery.js.map +1 -1
- package/dist/cjs/queries/CollectionQueries.js +1 -1
- package/dist/cjs/queries/CollectionQueries.js.map +1 -1
- package/dist/cjs/queries/FindAllQuery.js +1 -0
- package/dist/cjs/queries/FindAllQuery.js.map +1 -1
- package/dist/cjs/queries/QueryCache.d.ts +1 -0
- package/dist/cjs/queries/QueryCache.js +4 -0
- package/dist/cjs/queries/QueryCache.js.map +1 -1
- package/dist/cjs/queries/QueryableStorage.d.ts +20 -0
- package/dist/cjs/queries/QueryableStorage.js +84 -0
- package/dist/cjs/queries/QueryableStorage.js.map +1 -0
- package/dist/cjs/queries/dbQueries.js +13 -3
- package/dist/cjs/queries/dbQueries.js.map +1 -1
- package/dist/cjs/sync/FileSync.d.ts +1 -0
- package/dist/cjs/sync/FileSync.js +1 -0
- package/dist/cjs/sync/FileSync.js.map +1 -1
- package/dist/cjs/sync/PushPullSync.d.ts +2 -1
- package/dist/cjs/sync/PushPullSync.js +7 -1
- package/dist/cjs/sync/PushPullSync.js.map +1 -1
- package/dist/cjs/sync/Sync.d.ts +6 -3
- package/dist/cjs/sync/Sync.js +9 -4
- package/dist/cjs/sync/Sync.js.map +1 -1
- package/dist/cjs/sync/WebSocketSync.d.ts +4 -1
- package/dist/cjs/sync/WebSocketSync.js +41 -11
- package/dist/cjs/sync/WebSocketSync.js.map +1 -1
- package/dist/esm/DocumentManager.d.ts +1 -1
- package/dist/esm/DocumentManager.js +1 -1
- package/dist/esm/DocumentManager.js.map +1 -1
- package/dist/esm/IDBService.d.ts +28 -7
- package/dist/esm/IDBService.js +51 -14
- package/dist/esm/IDBService.js.map +1 -1
- package/dist/esm/UndoHistory.d.ts +1 -1
- package/dist/esm/UndoHistory.js +6 -2
- package/dist/esm/UndoHistory.js.map +1 -1
- package/dist/esm/__tests__/batching.test.js +3 -1
- package/dist/esm/__tests__/batching.test.js.map +1 -1
- package/dist/esm/__tests__/documents.test.js +37 -6
- package/dist/esm/__tests__/documents.test.js.map +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +2 -2
- package/dist/esm/__tests__/fixtures/testStorage.js +2 -1
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/__tests__/legacyOids.test.js +50 -17
- package/dist/esm/__tests__/legacyOids.test.js.map +1 -1
- package/dist/esm/__tests__/mutations.test.js +9 -3
- package/dist/esm/__tests__/mutations.test.js.map +1 -1
- package/dist/esm/__tests__/queries.test.js +6 -2
- package/dist/esm/__tests__/queries.test.js.map +1 -1
- package/dist/esm/__tests__/setup/indexedDB.d.ts +1 -1
- package/dist/esm/__tests__/setup/indexedDB.js +8 -1
- package/dist/esm/__tests__/setup/indexedDB.js.map +1 -1
- package/dist/esm/__tests__/undo.test.js +16 -9
- package/dist/esm/__tests__/undo.test.js.map +1 -1
- package/dist/esm/client/Client.d.ts +1 -1
- package/dist/esm/client/Client.js +7 -3
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.js +21 -6
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/context.d.ts +10 -1
- package/dist/esm/entities/Entity.d.ts +106 -178
- package/dist/esm/entities/Entity.js +559 -376
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/Entity.test.d.ts +1 -0
- package/dist/esm/entities/Entity.test.js +192 -0
- package/dist/esm/entities/Entity.test.js.map +1 -0
- package/dist/esm/entities/EntityCache.d.ts +15 -0
- package/dist/esm/entities/EntityCache.js +35 -0
- package/dist/esm/entities/EntityCache.js.map +1 -0
- package/dist/esm/entities/EntityMetadata.d.ts +68 -0
- package/dist/esm/entities/EntityMetadata.js +256 -0
- package/dist/esm/entities/EntityMetadata.js.map +1 -0
- package/dist/esm/entities/EntityStore.d.ts +63 -68
- package/dist/esm/entities/EntityStore.js +295 -439
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/OperationBatcher.d.ts +52 -0
- package/dist/esm/entities/OperationBatcher.js +161 -0
- package/dist/esm/entities/OperationBatcher.js.map +1 -0
- package/dist/esm/entities/types.d.ts +84 -0
- package/dist/esm/entities/types.js +2 -0
- package/dist/esm/entities/types.js.map +1 -0
- package/dist/esm/files/EntityFile.d.ts +5 -2
- package/dist/esm/files/EntityFile.js +8 -4
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.d.ts +3 -1
- package/dist/esm/files/FileManager.js +5 -3
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/files/FileStorage.js +7 -7
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/files/utils.d.ts +2 -0
- package/dist/esm/files/utils.js +4 -2
- package/dist/esm/files/utils.js.map +1 -1
- package/dist/esm/idb.d.ts +2 -0
- package/dist/esm/idb.js +47 -3
- package/dist/esm/idb.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/metadata/AckInfoStore.js +1 -1
- package/dist/esm/metadata/AckInfoStore.js.map +1 -1
- package/dist/esm/metadata/BaselinesStore.d.ts +4 -1
- package/dist/esm/metadata/BaselinesStore.js +19 -10
- package/dist/esm/metadata/BaselinesStore.js.map +1 -1
- package/dist/esm/metadata/LocalReplicaStore.d.ts +1 -1
- package/dist/esm/metadata/LocalReplicaStore.js +11 -5
- package/dist/esm/metadata/LocalReplicaStore.js.map +1 -1
- package/dist/esm/metadata/Metadata.d.ts +26 -5
- package/dist/esm/metadata/Metadata.js +56 -19
- package/dist/esm/metadata/Metadata.js.map +1 -1
- package/dist/esm/metadata/OperationsStore.d.ts +3 -0
- package/dist/esm/metadata/OperationsStore.js +35 -15
- package/dist/esm/metadata/OperationsStore.js.map +1 -1
- package/dist/esm/migration/openDatabase.js +32 -11
- package/dist/esm/migration/openDatabase.js.map +1 -1
- package/dist/esm/queries/BaseQuery.js +13 -1
- package/dist/esm/queries/BaseQuery.js.map +1 -1
- package/dist/esm/queries/CollectionQueries.js +1 -1
- package/dist/esm/queries/CollectionQueries.js.map +1 -1
- package/dist/esm/queries/FindAllQuery.js +1 -0
- package/dist/esm/queries/FindAllQuery.js.map +1 -1
- package/dist/esm/queries/QueryCache.d.ts +1 -0
- package/dist/esm/queries/QueryCache.js +4 -0
- package/dist/esm/queries/QueryCache.js.map +1 -1
- package/dist/esm/queries/QueryableStorage.d.ts +20 -0
- package/dist/esm/queries/QueryableStorage.js +80 -0
- package/dist/esm/queries/QueryableStorage.js.map +1 -0
- package/dist/esm/queries/dbQueries.js +13 -3
- package/dist/esm/queries/dbQueries.js.map +1 -1
- package/dist/esm/sync/FileSync.d.ts +1 -0
- package/dist/esm/sync/FileSync.js +1 -0
- package/dist/esm/sync/FileSync.js.map +1 -1
- package/dist/esm/sync/PushPullSync.d.ts +2 -1
- package/dist/esm/sync/PushPullSync.js +7 -1
- package/dist/esm/sync/PushPullSync.js.map +1 -1
- package/dist/esm/sync/Sync.d.ts +6 -3
- package/dist/esm/sync/Sync.js +9 -4
- package/dist/esm/sync/Sync.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.d.ts +4 -1
- package/dist/esm/sync/WebSocketSync.js +41 -11
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/dist/tsconfig-cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/DocumentManager.ts +1 -1
- package/src/IDBService.ts +78 -17
- package/src/UndoHistory.ts +5 -3
- package/src/__tests__/batching.test.ts +5 -2
- package/src/__tests__/documents.test.ts +44 -6
- package/src/__tests__/fixtures/testStorage.ts +3 -0
- package/src/__tests__/legacyOids.test.ts +53 -17
- package/src/__tests__/mutations.test.ts +9 -3
- package/src/__tests__/queries.test.ts +6 -2
- package/src/__tests__/setup/indexedDB.ts +8 -1
- package/src/__tests__/undo.test.ts +17 -9
- package/src/client/Client.ts +7 -3
- package/src/client/ClientDescriptor.ts +24 -8
- package/src/context.ts +16 -1
- package/src/entities/Entity.test.ts +218 -0
- package/src/entities/Entity.ts +696 -616
- package/src/entities/EntityCache.ts +41 -0
- package/src/entities/EntityMetadata.ts +364 -0
- package/src/entities/EntityStore.ts +384 -621
- package/src/entities/OperationBatcher.ts +251 -0
- package/src/entities/types.ts +154 -0
- package/src/files/EntityFile.ts +9 -4
- package/src/files/FileManager.ts +5 -3
- package/src/files/FileStorage.ts +7 -13
- package/src/files/utils.ts +6 -2
- package/src/idb.ts +51 -3
- package/src/index.ts +1 -1
- package/src/metadata/AckInfoStore.ts +1 -1
- package/src/metadata/BaselinesStore.ts +16 -24
- package/src/metadata/LocalReplicaStore.ts +13 -6
- package/src/metadata/Metadata.ts +109 -24
- package/src/metadata/OperationsStore.ts +37 -16
- package/src/migration/openDatabase.ts +32 -10
- package/src/queries/BaseQuery.ts +14 -1
- package/src/queries/CollectionQueries.ts +1 -1
- package/src/queries/FindAllQuery.ts +4 -0
- package/src/queries/QueryCache.ts +5 -0
- package/src/queries/QueryableStorage.ts +107 -0
- package/src/queries/dbQueries.ts +10 -3
- package/src/sync/FileSync.ts +2 -0
- package/src/sync/PushPullSync.ts +8 -1
- package/src/sync/Sync.ts +14 -6
- package/src/sync/WebSocketSync.ts +47 -10
- package/dist/cjs/entities/DocumentFamiliyCache.d.ts +0 -96
- package/dist/cjs/entities/DocumentFamiliyCache.js +0 -287
- package/dist/cjs/entities/DocumentFamiliyCache.js.map +0 -1
- package/dist/esm/entities/DocumentFamiliyCache.d.ts +0 -96
- package/dist/esm/entities/DocumentFamiliyCache.js +0 -283
- package/dist/esm/entities/DocumentFamiliyCache.js.map +0 -1
- package/src/entities/DocumentFamiliyCache.ts +0 -426
- package/src/entities/design.tldr +0 -808
|
@@ -1,181 +1,253 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
get hasSubscribers() {
|
|
17
|
-
var _c, _d;
|
|
18
|
-
if (this.events.totalSubscriberCount() > 0) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
// even if nobody subscribes directly to this entity, if a parent
|
|
22
|
-
// has a deep subscription that counts.
|
|
23
|
-
let parent = (_c = this.parent) === null || _c === void 0 ? void 0 : _c.deref();
|
|
24
|
-
while (parent) {
|
|
25
|
-
if (parent.hasSubscribersToDeepChanges()) {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
parent = (_d = parent.parent) === null || _d === void 0 ? void 0 : _d.deref();
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
get deleted() {
|
|
33
|
-
return this._deleted;
|
|
34
|
-
}
|
|
35
|
-
get value() {
|
|
36
|
-
return this._current;
|
|
37
|
-
}
|
|
38
|
-
get isList() {
|
|
39
|
-
return Array.isArray(this._current);
|
|
40
|
-
}
|
|
41
|
-
get updatedAt() {
|
|
42
|
-
return this._updatedAt;
|
|
43
|
-
}
|
|
44
|
-
get deepUpdatedAt() {
|
|
45
|
-
if (this.cachedDeepUpdatedAt)
|
|
46
|
-
return this.cachedDeepUpdatedAt;
|
|
47
|
-
// iterate over all children and take the latest timestamp
|
|
48
|
-
let latest = this._updatedAt;
|
|
49
|
-
if (this.isList) {
|
|
50
|
-
this.forEach((child) => {
|
|
51
|
-
if (child instanceof Entity) {
|
|
52
|
-
const childTimestamp = child.deepUpdatedAt;
|
|
53
|
-
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
54
|
-
latest = childTimestamp;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
this.values().forEach((child) => {
|
|
61
|
-
if (child instanceof Entity) {
|
|
62
|
-
const childTimestamp = child.deepUpdatedAt;
|
|
63
|
-
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
64
|
-
latest = childTimestamp;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
this.cachedDeepUpdatedAt = latest;
|
|
70
|
-
return latest;
|
|
71
|
-
}
|
|
72
|
-
get uid() {
|
|
73
|
-
return this.oid;
|
|
74
|
-
}
|
|
75
|
-
constructor({ oid, store, fieldSchema, cache, parent, onAllUnsubscribed, readonlyKeys = [], fieldPath = [], }) {
|
|
76
|
-
// if current is null, the entity was deleted.
|
|
77
|
-
this._current = null;
|
|
78
|
-
this._deleted = false;
|
|
79
|
-
this.cachedSnapshot = null;
|
|
80
|
-
this.cachedDestructure = null;
|
|
1
|
+
import { EventSubscriber, assert, assignOid, cloneDeep, compareRefs, createFileRef, createRef, getChildFieldSchema, getDefault, hasDefault, isFileRef, isNullable, isObject, isRef, maybeGetOid, memoByKeys, traverseCollectionFieldsAndApplyDefaults, validateEntityField, } from '@verdant-web/common';
|
|
2
|
+
import { isFile, processValueFiles } from '../files/utils.js';
|
|
3
|
+
import { EntityFile } from '../index.js';
|
|
4
|
+
import { EntityCache } from './EntityCache.js';
|
|
5
|
+
export class Entity extends EventSubscriber {
|
|
6
|
+
constructor({ oid, schema, entityFamily: childCache, parent, ctx, metadataFamily, readonlyKeys, files, patchCreator, events, }) {
|
|
7
|
+
super();
|
|
8
|
+
this.fieldPath = [];
|
|
9
|
+
// an internal representation of this Entity.
|
|
10
|
+
// if present, this is the cached, known value. If null,
|
|
11
|
+
// the entity is deleted. If undefined, we need to recompute
|
|
12
|
+
// the view.
|
|
13
|
+
this._viewData = undefined;
|
|
14
|
+
this.validationError = undefined;
|
|
81
15
|
this.cachedDeepUpdatedAt = null;
|
|
82
|
-
|
|
83
|
-
this
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.cachedDestructure = null;
|
|
89
|
-
this._updatedAt = lastTimestamp ? lastTimestamp : null;
|
|
90
|
-
this.cachedDeepUpdatedAt = null;
|
|
91
|
-
if (this._deleted) {
|
|
92
|
-
this.events.emit('delete', info);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
this.events.emit('change', info);
|
|
96
|
-
this[DEEP_CHANGE](this, info);
|
|
97
|
-
}
|
|
98
|
-
if (restored) {
|
|
99
|
-
this.cachedSnapshot = null;
|
|
100
|
-
this.events.emit('restore', info);
|
|
16
|
+
// only used for root entities to track delete/restore state.
|
|
17
|
+
this.wasDeletedLastChange = false;
|
|
18
|
+
this.cachedView = undefined;
|
|
19
|
+
this.onAdd = (_store, data) => {
|
|
20
|
+
if (data.oid === this.oid) {
|
|
21
|
+
this.addConfirmedData(data);
|
|
101
22
|
}
|
|
102
23
|
};
|
|
103
|
-
this
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.cachedDeepUpdatedAt = null;
|
|
107
|
-
this.events.emit('changeDeep', source, info);
|
|
108
|
-
const parent = (_c = this.parent) === null || _c === void 0 ? void 0 : _c.deref();
|
|
109
|
-
if (parent) {
|
|
110
|
-
parent[DEEP_CHANGE](source, info);
|
|
24
|
+
this.onReplace = (_store, data) => {
|
|
25
|
+
if (data.oid === this.oid) {
|
|
26
|
+
this.replaceAllData(data);
|
|
111
27
|
}
|
|
112
28
|
};
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
|
|
29
|
+
this.onResetAll = () => {
|
|
30
|
+
this.resetAllData();
|
|
31
|
+
};
|
|
32
|
+
this.childIsNull = (child) => {
|
|
33
|
+
if (child instanceof Entity) {
|
|
34
|
+
const childView = child.view;
|
|
35
|
+
return childView === null || childView === undefined;
|
|
116
36
|
}
|
|
117
|
-
|
|
118
|
-
|
|
37
|
+
return child === null || child === undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Pruning - when entities have invalid children, we 'prune' that
|
|
41
|
+
* data up to the nearest prunable point - a nullable field,
|
|
42
|
+
* or a list.
|
|
43
|
+
*/
|
|
44
|
+
this.validate = memoByKeys(() => {
|
|
45
|
+
var _a;
|
|
46
|
+
this.validationError =
|
|
47
|
+
(_a = validateEntityField({
|
|
48
|
+
field: this.schema,
|
|
49
|
+
value: this.rawView,
|
|
50
|
+
fieldPath: this.fieldPath,
|
|
51
|
+
depth: 1,
|
|
52
|
+
})) !== null && _a !== void 0 ? _a : undefined;
|
|
53
|
+
return this.validationError;
|
|
54
|
+
}, () => [this.viewData]);
|
|
55
|
+
this.viewWithMappedChildren = (mapper) => {
|
|
56
|
+
const view = this.view;
|
|
57
|
+
if (!view) {
|
|
58
|
+
return null;
|
|
119
59
|
}
|
|
120
|
-
|
|
121
|
-
|
|
60
|
+
if (Array.isArray(view)) {
|
|
61
|
+
const mapped = view.map((value) => {
|
|
62
|
+
if (value instanceof Entity || value instanceof EntityFile) {
|
|
63
|
+
return mapper(value);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
assignOid(mapped, this.oid);
|
|
70
|
+
return mapped;
|
|
122
71
|
}
|
|
123
|
-
else
|
|
124
|
-
|
|
72
|
+
else {
|
|
73
|
+
const mapped = Object.entries(view).reduce((acc, [key, value]) => {
|
|
74
|
+
if (value instanceof Entity || value instanceof EntityFile) {
|
|
75
|
+
acc[key] = mapper(value);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
acc[key] = value;
|
|
79
|
+
}
|
|
80
|
+
return acc;
|
|
81
|
+
}, {});
|
|
82
|
+
assignOid(mapped, this.oid);
|
|
83
|
+
return mapped;
|
|
125
84
|
}
|
|
126
|
-
throw new Error('Invalid field schema');
|
|
127
85
|
};
|
|
128
|
-
|
|
129
|
-
|
|
86
|
+
/**
|
|
87
|
+
* A current snapshot of this Entity's data, including nested
|
|
88
|
+
* Entities.
|
|
89
|
+
*/
|
|
90
|
+
this.getSnapshot = () => {
|
|
91
|
+
return this.viewWithMappedChildren((child) => child.getSnapshot());
|
|
92
|
+
};
|
|
93
|
+
// change management methods (internal use only)
|
|
94
|
+
this.addPendingOperations = (operations) => {
|
|
95
|
+
this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
|
|
96
|
+
const changes = this.metadataFamily.addPendingData(operations);
|
|
97
|
+
for (const change of changes) {
|
|
98
|
+
this.change(change);
|
|
99
|
+
}
|
|
130
100
|
};
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
101
|
+
this.addConfirmedData = (data) => {
|
|
102
|
+
this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
|
|
103
|
+
const changes = this.metadataFamily.addConfirmedData(data);
|
|
104
|
+
for (const change of changes) {
|
|
105
|
+
this.change(change);
|
|
106
|
+
}
|
|
134
107
|
};
|
|
135
|
-
this.
|
|
136
|
-
this.
|
|
108
|
+
this.replaceAllData = (data) => {
|
|
109
|
+
this.ctx.log('debug', 'Entity: replacing all data', this.oid);
|
|
110
|
+
const changes = this.metadataFamily.replaceAllData(data);
|
|
111
|
+
for (const change of changes) {
|
|
112
|
+
this.change(change);
|
|
113
|
+
}
|
|
137
114
|
};
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
|
|
115
|
+
this.resetAllData = () => {
|
|
116
|
+
this.ctx.log('debug', 'Entity: resetting all data', this.oid);
|
|
117
|
+
this.cachedDeepUpdatedAt = null;
|
|
118
|
+
this.cachedView = undefined;
|
|
119
|
+
this._viewData = undefined;
|
|
120
|
+
const changes = this.metadataFamily.replaceAllData({});
|
|
121
|
+
for (const change of changes) {
|
|
122
|
+
this.change(change);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
this.change = (ev) => {
|
|
126
|
+
if (ev.oid === this.oid) {
|
|
127
|
+
// reset cached view
|
|
128
|
+
this._viewData = undefined;
|
|
129
|
+
this.cachedView = undefined;
|
|
130
|
+
// chain deepChanges to parents
|
|
131
|
+
this.deepChange(this, ev);
|
|
132
|
+
// emit the change, it's for us
|
|
133
|
+
this.ctx.log('Emitting change event', this.oid);
|
|
134
|
+
this.emit('change', { isLocal: ev.isLocal });
|
|
135
|
+
// for root entities, we need to go ahead and decide if we're
|
|
136
|
+
// deleted or not - so queries can exclude us if we are.
|
|
137
|
+
if (!this.parent) {
|
|
138
|
+
// newly deleted - emit event
|
|
139
|
+
if (this.deleted && !this.wasDeletedLastChange) {
|
|
140
|
+
this.ctx.log('debug', 'Entity deleted', this.oid);
|
|
141
|
+
this.emit('delete', { isLocal: ev.isLocal });
|
|
142
|
+
this.wasDeletedLastChange = true;
|
|
143
|
+
}
|
|
144
|
+
else if (!this.deleted && this.wasDeletedLastChange) {
|
|
145
|
+
this.ctx.log('debug', 'Entity restored', this.oid);
|
|
146
|
+
// newly restored - emit event
|
|
147
|
+
this.emit('restore', { isLocal: ev.isLocal });
|
|
148
|
+
this.wasDeletedLastChange = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// forward it to the correct family member. if none exists
|
|
154
|
+
// in cache, no one will hear it anyways.
|
|
155
|
+
const other = this.entityFamily.getCached(ev.oid);
|
|
156
|
+
if (other && other instanceof Entity) {
|
|
157
|
+
other.change(ev);
|
|
158
|
+
}
|
|
141
159
|
}
|
|
142
|
-
return cloneDeep(this._current);
|
|
143
160
|
};
|
|
144
|
-
this.
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
this.deepChange = (target, ev) => {
|
|
162
|
+
var _a;
|
|
163
|
+
// reset cached deep updated at timestamp; either this
|
|
164
|
+
// entity or children have changed
|
|
165
|
+
this.cachedDeepUpdatedAt = null;
|
|
166
|
+
// reset this flag to recompute snapshot data - children
|
|
167
|
+
// or self has changed. new pruning needs to happen.
|
|
168
|
+
this.cachedView = undefined;
|
|
169
|
+
this.ctx.log('debug', 'Deep change detected at', this.oid, 'reset cached view');
|
|
170
|
+
this.ctx.log('debug', 'Emitting deep change event', this.oid);
|
|
171
|
+
this.emit('changeDeep', target, ev);
|
|
172
|
+
(_a = this.parent) === null || _a === void 0 ? void 0 : _a.deepChange(target, ev);
|
|
173
|
+
};
|
|
174
|
+
this.getChild = (key, oid) => {
|
|
175
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
176
|
+
if (!schema) {
|
|
177
|
+
throw new Error(`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`);
|
|
178
|
+
}
|
|
179
|
+
return this.entityFamily.get({
|
|
150
180
|
oid,
|
|
151
|
-
|
|
181
|
+
schema,
|
|
182
|
+
entityFamily: this.entityFamily,
|
|
183
|
+
metadataFamily: this.metadataFamily,
|
|
152
184
|
parent: this,
|
|
153
|
-
|
|
185
|
+
ctx: this.ctx,
|
|
186
|
+
files: this.files,
|
|
187
|
+
fieldPath: [...this.fieldPath, key],
|
|
188
|
+
patchCreator: this.patchCreator,
|
|
189
|
+
events: this.events,
|
|
154
190
|
});
|
|
155
191
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
// generic entity methods
|
|
193
|
+
/**
|
|
194
|
+
* Gets a value from this Entity. If the value
|
|
195
|
+
* is an object, it will be wrapped in another
|
|
196
|
+
* Entity.
|
|
197
|
+
*/
|
|
198
|
+
this.get = (key) => {
|
|
199
|
+
assertNotSymbol(key);
|
|
200
|
+
const view = this.rawView;
|
|
201
|
+
if (!view) {
|
|
202
|
+
throw new Error(`Cannot access data at key ${key} on deleted entity ${this.oid}`);
|
|
203
|
+
}
|
|
204
|
+
const child = view[key];
|
|
205
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
206
|
+
if (!schema) {
|
|
207
|
+
throw new Error(`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`);
|
|
208
|
+
}
|
|
209
|
+
if (isRef(child)) {
|
|
210
|
+
if (isFileRef(child)) {
|
|
211
|
+
if (schema.type !== 'file') {
|
|
212
|
+
throw new Error(`Expected file schema for key ${String(key)}, got ${schema.type}`);
|
|
213
|
+
}
|
|
214
|
+
const file = this.files.get(child.id, {
|
|
215
|
+
downloadRemote: !!schema.downloadRemote,
|
|
216
|
+
});
|
|
217
|
+
// FIXME: this seems bad and inconsistent
|
|
168
218
|
file.subscribe('change', () => {
|
|
169
|
-
this
|
|
170
|
-
isLocal: false,
|
|
171
|
-
});
|
|
219
|
+
this.deepChange(this, { isLocal: false, oid: this.oid });
|
|
172
220
|
});
|
|
173
221
|
return file;
|
|
174
222
|
}
|
|
223
|
+
else {
|
|
224
|
+
return this.getChild(key, child.id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// prune invalid primitive fields
|
|
229
|
+
if (validateEntityField({
|
|
230
|
+
field: schema,
|
|
231
|
+
value: child,
|
|
232
|
+
fieldPath: [...this.fieldPath, key],
|
|
233
|
+
depth: 1,
|
|
234
|
+
requireDefaults: true,
|
|
235
|
+
})) {
|
|
236
|
+
if (hasDefault(schema)) {
|
|
237
|
+
return getDefault(schema);
|
|
238
|
+
}
|
|
239
|
+
if (isNullable(schema)) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
return child;
|
|
175
245
|
}
|
|
176
|
-
return value;
|
|
177
246
|
};
|
|
178
247
|
this.processInputValue = (value, key) => {
|
|
248
|
+
if (this.readonlyKeys.includes(key)) {
|
|
249
|
+
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
250
|
+
}
|
|
179
251
|
// disassociate incoming OIDs on values and generally break object
|
|
180
252
|
// references. cloning doesn't work on files so those are
|
|
181
253
|
// filtered out.
|
|
@@ -190,324 +262,427 @@ export class Entity {
|
|
|
190
262
|
// referenced in multiple entities, which could mean introduction
|
|
191
263
|
// of foreign OIDs, or one object being assigned different OIDs
|
|
192
264
|
// with unexpected results.
|
|
193
|
-
if (!(value
|
|
265
|
+
if (!isFile(value)) {
|
|
194
266
|
value = cloneDeep(value, false);
|
|
195
267
|
}
|
|
196
|
-
const fieldSchema = this.
|
|
268
|
+
const fieldSchema = getChildFieldSchema(this.schema, key);
|
|
197
269
|
if (fieldSchema) {
|
|
198
270
|
traverseCollectionFieldsAndApplyDefaults(value, fieldSchema);
|
|
271
|
+
const validationError = validateEntityField({
|
|
272
|
+
field: fieldSchema,
|
|
273
|
+
value,
|
|
274
|
+
fieldPath: [...this.fieldPath, key],
|
|
275
|
+
});
|
|
276
|
+
if (validationError) {
|
|
277
|
+
// TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
|
|
278
|
+
// but also we don't really want invalid data supplied.
|
|
279
|
+
throw new Error(validationError.message);
|
|
280
|
+
}
|
|
199
281
|
}
|
|
200
|
-
|
|
201
|
-
...this.fieldPath,
|
|
202
|
-
key,
|
|
203
|
-
]);
|
|
204
|
-
if (validationError) {
|
|
205
|
-
// TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
|
|
206
|
-
// but also we don't really want invalid data supplied.
|
|
207
|
-
throw new Error(validationError);
|
|
208
|
-
}
|
|
209
|
-
return processValueFiles(value, this.store.addFile);
|
|
210
|
-
};
|
|
211
|
-
this.get = (key) => {
|
|
212
|
-
if (this.value === undefined || this.value === null) {
|
|
213
|
-
throw new Error('Cannot access deleted entity');
|
|
214
|
-
}
|
|
215
|
-
const value = this.value[key];
|
|
216
|
-
return this.wrapValue(value, key);
|
|
282
|
+
return processValueFiles(value, this.files.add);
|
|
217
283
|
};
|
|
218
|
-
this.
|
|
219
|
-
if (this.
|
|
220
|
-
|
|
284
|
+
this.getDeleteMode = (key) => {
|
|
285
|
+
if (this.readonlyKeys.includes(key)) {
|
|
286
|
+
return false;
|
|
221
287
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (Array.isArray(this.value)) {
|
|
226
|
-
result = this.value.map((value, index) => this.wrapValue(value, index));
|
|
288
|
+
// any is always deletable, and map values
|
|
289
|
+
if (this.schema.type === 'any' || this.schema.type === 'map') {
|
|
290
|
+
return 'delete';
|
|
227
291
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
292
|
+
if (this.schema.type === 'object') {
|
|
293
|
+
const property = this.schema.properties[key];
|
|
294
|
+
if (!property) {
|
|
295
|
+
// huh, the property doesn't exist. it's ok to
|
|
296
|
+
// remove I suppose.
|
|
297
|
+
return 'delete';
|
|
232
298
|
}
|
|
299
|
+
if (property.type === 'any')
|
|
300
|
+
return 'delete';
|
|
301
|
+
// map can't be nullable. should it be?
|
|
302
|
+
if (property.type === 'map')
|
|
303
|
+
return false;
|
|
304
|
+
if (property.nullable)
|
|
305
|
+
return 'null';
|
|
233
306
|
}
|
|
234
|
-
|
|
235
|
-
return
|
|
307
|
+
// no other types are deletable
|
|
308
|
+
return false;
|
|
236
309
|
};
|
|
237
310
|
/**
|
|
238
|
-
* Returns
|
|
239
|
-
*
|
|
311
|
+
* Returns the referent value of an item in the list, used for
|
|
312
|
+
* operations which act on items. if the item is an object,
|
|
313
|
+
* it will attempt to create an OID reference to it. If it
|
|
314
|
+
* is a primitive, it will return the primitive.
|
|
240
315
|
*/
|
|
241
|
-
this.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
if (this.deleted) {
|
|
247
|
-
return null;
|
|
316
|
+
this.getItemRefValue = (item) => {
|
|
317
|
+
if (item instanceof Entity) {
|
|
318
|
+
return createRef(item.oid);
|
|
248
319
|
}
|
|
249
|
-
if (
|
|
250
|
-
return
|
|
320
|
+
if (item instanceof EntityFile) {
|
|
321
|
+
return createFileRef(item.id);
|
|
251
322
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
else if (isFileRef(item)) {
|
|
260
|
-
return this.getFileSnapshot(item);
|
|
261
|
-
}
|
|
262
|
-
return item;
|
|
263
|
-
});
|
|
323
|
+
if (typeof item === 'object') {
|
|
324
|
+
const itemOid = maybeGetOid(item);
|
|
325
|
+
if (!itemOid || !this.entityFamily.has(itemOid)) {
|
|
326
|
+
throw new Error(`Cannot move object ${JSON.stringify(item)} which does not exist in this list`);
|
|
327
|
+
}
|
|
328
|
+
return createRef(itemOid);
|
|
264
329
|
}
|
|
265
330
|
else {
|
|
266
|
-
|
|
267
|
-
for (const [key, value] of Object.entries(snapshot)) {
|
|
268
|
-
if (isObjectRef(value)) {
|
|
269
|
-
snapshot[key] = (_c = this.getSubObject(value.id, key)) === null || _c === void 0 ? void 0 : _c.getSnapshot();
|
|
270
|
-
}
|
|
271
|
-
else if (isFileRef(value)) {
|
|
272
|
-
snapshot[key] = this.getFileSnapshot(value);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
331
|
+
return item;
|
|
275
332
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
333
|
+
};
|
|
334
|
+
this.set = (key, value) => {
|
|
335
|
+
assertNotSymbol(key);
|
|
336
|
+
this.addPendingOperations(this.patchCreator.createSet(this.oid, key, this.processInputValue(value, key)));
|
|
279
337
|
};
|
|
280
338
|
/**
|
|
281
|
-
*
|
|
339
|
+
* Returns a destructured version of this Entity, where child
|
|
340
|
+
* Entities are accessible at their respective keys.
|
|
282
341
|
*/
|
|
283
|
-
this.
|
|
284
|
-
return
|
|
285
|
-
};
|
|
286
|
-
this.entries = () => {
|
|
287
|
-
return Object.entries(this.getAll());
|
|
288
|
-
};
|
|
289
|
-
this.values = () => {
|
|
290
|
-
return Object.values(this.getAll());
|
|
291
|
-
};
|
|
292
|
-
this.set = (key, value) => {
|
|
293
|
-
if (this.readonlyKeys.includes(key)) {
|
|
294
|
-
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
295
|
-
}
|
|
296
|
-
this.addPatches(this.store.patchCreator.createSet(this.oid, key, this.processInputValue(value, key)));
|
|
342
|
+
this.getAll = () => {
|
|
343
|
+
return this.view;
|
|
297
344
|
};
|
|
298
345
|
this.delete = (key) => {
|
|
299
|
-
if (
|
|
300
|
-
|
|
346
|
+
if (this.isList) {
|
|
347
|
+
assertNumber(key);
|
|
348
|
+
this.addPendingOperations(this.patchCreator.createListDelete(this.oid, key));
|
|
301
349
|
}
|
|
302
350
|
else {
|
|
303
|
-
// the key must be deletable - i.e. optional in the schema
|
|
351
|
+
// the key must be deletable - i.e. optional in the schema.
|
|
304
352
|
const deleteMode = this.getDeleteMode(key);
|
|
305
353
|
if (!deleteMode) {
|
|
306
|
-
throw new Error(`Cannot delete key ${key} - the property is not marked as optional in the schema
|
|
354
|
+
throw new Error(`Cannot delete key ${key.toString()} - the property is not marked as optional in the schema.`);
|
|
307
355
|
}
|
|
308
356
|
if (deleteMode === 'delete') {
|
|
309
|
-
this.
|
|
357
|
+
this.addPendingOperations(this.patchCreator.createRemove(this.oid, key));
|
|
310
358
|
}
|
|
311
359
|
else {
|
|
312
|
-
this.
|
|
360
|
+
this.addPendingOperations(this.patchCreator.createSet(this.oid, key, null));
|
|
313
361
|
}
|
|
314
362
|
}
|
|
315
363
|
};
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (property.type === 'any')
|
|
332
|
-
return 'delete';
|
|
333
|
-
// map can't be nullable
|
|
334
|
-
// TODO: should it be?
|
|
335
|
-
if (property.type === 'map')
|
|
336
|
-
return false;
|
|
337
|
-
// nullable properties can only be set null
|
|
338
|
-
if (property.nullable)
|
|
339
|
-
return 'null';
|
|
340
|
-
}
|
|
341
|
-
// no other parent objects support deleting
|
|
342
|
-
return false;
|
|
364
|
+
// object entity methods
|
|
365
|
+
this.keys = () => {
|
|
366
|
+
if (!this.view)
|
|
367
|
+
return [];
|
|
368
|
+
return Object.keys(this.view);
|
|
369
|
+
};
|
|
370
|
+
this.entries = () => {
|
|
371
|
+
if (!this.view)
|
|
372
|
+
return [];
|
|
373
|
+
return Object.entries(this.view);
|
|
374
|
+
};
|
|
375
|
+
this.values = () => {
|
|
376
|
+
if (!this.view)
|
|
377
|
+
return [];
|
|
378
|
+
return Object.values(this.view);
|
|
343
379
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
this.update = (value, { replaceSubObjects = false, merge = true, } = {
|
|
347
|
-
replaceSubObjects: false,
|
|
348
|
-
merge: true,
|
|
349
|
-
}) => {
|
|
350
|
-
if (!merge &&
|
|
351
|
-
this.fieldSchema.type !== 'any' &&
|
|
352
|
-
this.fieldSchema.type !== 'map') {
|
|
380
|
+
this.update = (data, { merge = true, replaceSubObjects = false, } = {}) => {
|
|
381
|
+
if (!merge && this.schema.type !== 'any' && this.schema.type !== 'map') {
|
|
353
382
|
throw new Error('Cannot use .update without merge if the field has a strict schema type. merge: false is only available on "any" or "map" types.');
|
|
354
383
|
}
|
|
355
|
-
|
|
384
|
+
const changes = {};
|
|
385
|
+
assignOid(changes, this.oid);
|
|
386
|
+
for (const [key, field] of Object.entries(data)) {
|
|
356
387
|
if (this.readonlyKeys.includes(key)) {
|
|
357
388
|
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
358
389
|
}
|
|
359
|
-
const fieldSchema = this.
|
|
390
|
+
const fieldSchema = getChildFieldSchema(this.schema, key);
|
|
360
391
|
if (fieldSchema) {
|
|
361
392
|
traverseCollectionFieldsAndApplyDefaults(field, fieldSchema);
|
|
362
393
|
}
|
|
394
|
+
changes[key] = this.processInputValue(field, key);
|
|
363
395
|
}
|
|
364
|
-
|
|
365
|
-
this.addPatches(this.store.patchCreator.createDiff(this.getSnapshot(), assignOid(withoutFiles, this.oid), {
|
|
396
|
+
this.addPendingOperations(this.patchCreator.createDiff(this.getSnapshot(), changes, {
|
|
366
397
|
mergeUnknownObjects: !replaceSubObjects,
|
|
367
398
|
defaultUndefined: merge,
|
|
368
399
|
}));
|
|
369
400
|
};
|
|
370
|
-
/**
|
|
371
|
-
* List methods
|
|
372
|
-
*/
|
|
373
|
-
/**
|
|
374
|
-
* Returns the referent value of an item in the list, used for
|
|
375
|
-
* operations which act on items. if the item is an object,
|
|
376
|
-
* it will attempt to create an OID reference to it. If it
|
|
377
|
-
* is a primitive, it will return the primitive.
|
|
378
|
-
*/
|
|
379
|
-
this.getItemRefValue = (item) => {
|
|
380
|
-
if (typeof item === 'object') {
|
|
381
|
-
const itemOid = maybeGetOid(item);
|
|
382
|
-
if (!itemOid || !this.cache.hasOid(itemOid)) {
|
|
383
|
-
throw new Error(`Cannot move object ${JSON.stringify(item)} which does not exist in this list`);
|
|
384
|
-
}
|
|
385
|
-
return itemOid;
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
return item;
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
401
|
this.push = (value) => {
|
|
392
|
-
this.
|
|
402
|
+
this.addPendingOperations(this.patchCreator.createListPush(this.oid, this.processInputValue(value, this.view.length)));
|
|
393
403
|
};
|
|
394
404
|
this.insert = (index, value) => {
|
|
395
|
-
this.
|
|
405
|
+
this.addPendingOperations(this.patchCreator.createListInsert(this.oid, index, this.processInputValue(value, index)));
|
|
396
406
|
};
|
|
397
407
|
this.move = (from, to) => {
|
|
398
|
-
this.
|
|
408
|
+
this.addPendingOperations(this.patchCreator.createListMoveByIndex(this.oid, from, to));
|
|
399
409
|
};
|
|
400
410
|
this.moveItem = (item, to) => {
|
|
401
411
|
const itemRef = this.getItemRefValue(item);
|
|
402
|
-
if (
|
|
403
|
-
this.
|
|
412
|
+
if (isRef(itemRef)) {
|
|
413
|
+
this.addPendingOperations(this.patchCreator.createListMoveByRef(this.oid, itemRef, to));
|
|
404
414
|
}
|
|
405
415
|
else {
|
|
406
|
-
const index = this.
|
|
407
|
-
|
|
416
|
+
const index = this.view.indexOf(item);
|
|
417
|
+
if (index === -1) {
|
|
418
|
+
throw new Error(`Cannot move item ${JSON.stringify(item)} which does not exist in this list`);
|
|
419
|
+
}
|
|
420
|
+
this.move(index, to);
|
|
408
421
|
}
|
|
409
422
|
};
|
|
423
|
+
this.add = (value) => {
|
|
424
|
+
this.addPendingOperations(this.patchCreator.createListAdd(this.oid, this.processInputValue(value, this.view.length)));
|
|
425
|
+
};
|
|
410
426
|
this.removeAll = (item) => {
|
|
411
|
-
this.
|
|
427
|
+
this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item)));
|
|
412
428
|
};
|
|
413
429
|
this.removeFirst = (item) => {
|
|
414
|
-
this.
|
|
430
|
+
this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'first'));
|
|
415
431
|
};
|
|
416
432
|
this.removeLast = (item) => {
|
|
417
|
-
this.
|
|
418
|
-
};
|
|
419
|
-
this.add = (item) => {
|
|
420
|
-
this.addPatches(this.store.patchCreator.createListAdd(this.oid, this.processInputValue(item, this.value.length)));
|
|
421
|
-
};
|
|
422
|
-
this.has = (item) => {
|
|
423
|
-
if (typeof item === 'object') {
|
|
424
|
-
return this.value.some((val) => {
|
|
425
|
-
if (isObjectRef(val))
|
|
426
|
-
return val.id === maybeGetOid(item);
|
|
427
|
-
// Sets of files don't work right now, there's no way to compare them
|
|
428
|
-
// effectively.
|
|
429
|
-
if (isFileRef(val))
|
|
430
|
-
return false;
|
|
431
|
-
return false;
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
return this.value.includes(item);
|
|
435
|
-
};
|
|
436
|
-
// additional access methods
|
|
437
|
-
this.getAsWrapped = () => {
|
|
438
|
-
if (!this.isList)
|
|
439
|
-
throw new Error('Cannot map items of a non-list');
|
|
440
|
-
return this.value.map(this.wrapValue);
|
|
433
|
+
this.addPendingOperations(this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item), 'last'));
|
|
441
434
|
};
|
|
442
435
|
this.map = (callback) => {
|
|
443
|
-
return this.
|
|
436
|
+
return this.view.map(callback);
|
|
444
437
|
};
|
|
445
438
|
this.filter = (callback) => {
|
|
446
|
-
return this.
|
|
447
|
-
|
|
448
|
-
|
|
439
|
+
return this.view.filter(callback);
|
|
440
|
+
};
|
|
441
|
+
this.has = (value) => {
|
|
442
|
+
if (!this.isList) {
|
|
443
|
+
throw new Error('has() is only available on list entities');
|
|
444
|
+
}
|
|
445
|
+
const itemRef = this.getItemRefValue(value);
|
|
446
|
+
if (isRef(itemRef)) {
|
|
447
|
+
return this.view.some((item) => {
|
|
448
|
+
if (isRef(item)) {
|
|
449
|
+
return compareRefs(item, itemRef);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
return this.view.includes(value);
|
|
455
|
+
}
|
|
449
456
|
};
|
|
450
457
|
this.forEach = (callback) => {
|
|
451
|
-
this.
|
|
458
|
+
this.view.forEach(callback);
|
|
452
459
|
};
|
|
453
460
|
this.some = (predicate) => {
|
|
454
|
-
return this.
|
|
461
|
+
return this.view.some(predicate);
|
|
455
462
|
};
|
|
456
463
|
this.every = (predicate) => {
|
|
457
|
-
return this.
|
|
464
|
+
return this.view.every(predicate);
|
|
458
465
|
};
|
|
459
466
|
this.find = (predicate) => {
|
|
460
|
-
return this.
|
|
467
|
+
return this.view.find(predicate);
|
|
461
468
|
};
|
|
462
|
-
this.includes =
|
|
463
|
-
|
|
469
|
+
this.includes = this.has;
|
|
470
|
+
// TODO: make these escape hatches unnecessary
|
|
471
|
+
this.__getViewData__ = (oid, type) => {
|
|
472
|
+
return this.metadataFamily.get(oid).computeView(type === 'confirmed');
|
|
464
473
|
};
|
|
474
|
+
this.__getFamilyOids__ = () => this.metadataFamily.getAllOids();
|
|
475
|
+
assert(!!oid, 'oid is required');
|
|
465
476
|
this.oid = oid;
|
|
466
|
-
|
|
467
|
-
this.
|
|
468
|
-
this.
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.
|
|
476
|
-
this.
|
|
477
|
-
this.
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (this.oid.includes('.') && !this.parent) {
|
|
485
|
-
throw new Error('Parent must be provided for sub entities');
|
|
477
|
+
this.readonlyKeys = readonlyKeys || [];
|
|
478
|
+
this.ctx = ctx;
|
|
479
|
+
this.files = files;
|
|
480
|
+
this.schema = schema;
|
|
481
|
+
this.entityFamily =
|
|
482
|
+
childCache ||
|
|
483
|
+
new EntityCache({
|
|
484
|
+
initial: [this],
|
|
485
|
+
});
|
|
486
|
+
this.patchCreator = patchCreator;
|
|
487
|
+
this.metadataFamily = metadataFamily;
|
|
488
|
+
this.events = events;
|
|
489
|
+
this.parent = parent;
|
|
490
|
+
// TODO: should any but the root entity be listening to these?
|
|
491
|
+
if (!this.parent) {
|
|
492
|
+
events.add.attach(this.onAdd);
|
|
493
|
+
events.replace.attach(this.onReplace);
|
|
494
|
+
events.resetAll.attach(this.onResetAll);
|
|
486
495
|
}
|
|
487
|
-
assert(!!fieldSchema, 'Field schema must be provided');
|
|
488
496
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
497
|
+
get metadata() {
|
|
498
|
+
return this.metadataFamily.get(this.oid);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* The view of this Entity, not including nested
|
|
502
|
+
* entities (that's the snapshot - see #getSnapshot())
|
|
503
|
+
*
|
|
504
|
+
* Nested entities are represented by refs.
|
|
505
|
+
*/
|
|
506
|
+
get viewData() {
|
|
507
|
+
if (this._viewData === undefined) {
|
|
508
|
+
this._viewData = this.metadata.computeView();
|
|
509
|
+
this.validate();
|
|
493
510
|
}
|
|
494
|
-
|
|
495
|
-
|
|
511
|
+
return this._viewData;
|
|
512
|
+
}
|
|
513
|
+
/** convenience getter for viewData.view */
|
|
514
|
+
get rawView() {
|
|
515
|
+
return this.viewData.view;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* An Entity's View includes the rendering of its underlying data,
|
|
519
|
+
* connecting of children where refs were, and validation
|
|
520
|
+
* and pruning according to schema.
|
|
521
|
+
*/
|
|
522
|
+
get view() {
|
|
523
|
+
if (this.cachedView !== undefined) {
|
|
524
|
+
return this.cachedView;
|
|
525
|
+
}
|
|
526
|
+
if (this.viewData.deleted) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
// can't use invalid data - but this should be bubbled up to
|
|
530
|
+
// a prune point
|
|
531
|
+
const rawView = this.rawView;
|
|
532
|
+
const viewIsWrongType = (!rawView && !isNullable(this.schema)) ||
|
|
533
|
+
(this.schema.type === 'array' && !Array.isArray(rawView)) ||
|
|
534
|
+
((this.schema.type === 'object' || this.schema.type === 'map') &&
|
|
535
|
+
!isObject(rawView));
|
|
536
|
+
if (viewIsWrongType) {
|
|
537
|
+
// this will cover lists and maps, too.
|
|
538
|
+
if (hasDefault(this.schema)) {
|
|
539
|
+
return getDefault(this.schema);
|
|
540
|
+
}
|
|
541
|
+
// force null - invalid - will require parent prune
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
this.cachedView = this.isList ? [] : {};
|
|
545
|
+
assignOid(this.cachedView, this.oid);
|
|
546
|
+
if (Array.isArray(rawView)) {
|
|
547
|
+
const schema = getChildFieldSchema(this.schema, 0);
|
|
548
|
+
if (!schema) {
|
|
549
|
+
/**
|
|
550
|
+
* PRUNE - this is a prune point. we can't continue
|
|
551
|
+
* to render this data, so we'll just return [].
|
|
552
|
+
* This skips the loop.
|
|
553
|
+
*/
|
|
554
|
+
this.ctx.log('error', 'No child field schema for list entity.', this.oid);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
for (let i = 0; i < rawView.length; i++) {
|
|
558
|
+
const child = this.get(i);
|
|
559
|
+
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
560
|
+
this.ctx.log('error', 'Child missing in non-nullable field', this.oid, 'index:', i);
|
|
561
|
+
// this item will be pruned.
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
this.cachedView.push(child);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else if (isObject(rawView)) {
|
|
570
|
+
// iterate over known properties in object-type entities;
|
|
571
|
+
// for maps, we just iterate over the keys.
|
|
572
|
+
const keys = this.schema.type === 'object'
|
|
573
|
+
? Object.keys(this.schema.properties)
|
|
574
|
+
: Object.keys(rawView);
|
|
575
|
+
for (const key of keys) {
|
|
576
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
577
|
+
if (!schema) {
|
|
578
|
+
/**
|
|
579
|
+
* PRUNE - this is a prune point. we can't continue
|
|
580
|
+
* to render this data. If this is a map, it will be
|
|
581
|
+
* pruned empty. Otherwise, prune moves upward.
|
|
582
|
+
*
|
|
583
|
+
* This exits the loop.
|
|
584
|
+
*/
|
|
585
|
+
this.ctx.log('error', 'No child field schema for object entity at key', key);
|
|
586
|
+
if (this.schema.type === 'map') {
|
|
587
|
+
// it's valid to prune here if it's a map
|
|
588
|
+
this.cachedView = {};
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// otherwise prune moves upward
|
|
592
|
+
this.cachedView = null;
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
const child = this.get(key);
|
|
597
|
+
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
598
|
+
this.ctx.log('error', 'Child entity is missing for non-nullable field', this.oid, 'key:', key);
|
|
599
|
+
/**
|
|
600
|
+
* PRUNE - this is a prune point. we can't continue
|
|
601
|
+
* to render this data. If this is a map, we can ignore
|
|
602
|
+
* this value. Otherwise we must prune upward.
|
|
603
|
+
* This exits the loop.
|
|
604
|
+
*/
|
|
605
|
+
if (this.schema.type !== 'map') {
|
|
606
|
+
this.cachedView = null;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.cachedView[key] = child;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return this.cachedView;
|
|
616
|
+
}
|
|
617
|
+
get uid() {
|
|
618
|
+
return this.oid;
|
|
619
|
+
}
|
|
620
|
+
get deleted() {
|
|
621
|
+
return this.viewData.deleted || this.view === null;
|
|
622
|
+
}
|
|
623
|
+
get invalid() {
|
|
624
|
+
return !!this.validate();
|
|
625
|
+
}
|
|
626
|
+
get isList() {
|
|
627
|
+
// have to turn TS off here as our two interfaces both implement
|
|
628
|
+
// const values for this boolean.
|
|
629
|
+
return (this.schema.type === 'array' || Array.isArray(this.viewData.view));
|
|
630
|
+
}
|
|
631
|
+
get updatedAt() {
|
|
632
|
+
return this.viewData.updatedAt;
|
|
633
|
+
}
|
|
634
|
+
get deepUpdatedAt() {
|
|
635
|
+
if (this.cachedDeepUpdatedAt)
|
|
636
|
+
return this.cachedDeepUpdatedAt;
|
|
637
|
+
// iterate over all children and take the latest timestamp
|
|
638
|
+
let latest = this.updatedAt;
|
|
639
|
+
if (this.isList) {
|
|
640
|
+
this.forEach((child) => {
|
|
641
|
+
if (child instanceof Entity) {
|
|
642
|
+
const childTimestamp = child.deepUpdatedAt;
|
|
643
|
+
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
644
|
+
latest = childTimestamp;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
496
648
|
}
|
|
497
649
|
else {
|
|
498
|
-
|
|
650
|
+
this.values().forEach((child) => {
|
|
651
|
+
if (child instanceof Entity) {
|
|
652
|
+
const childTimestamp = child.deepUpdatedAt;
|
|
653
|
+
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
654
|
+
latest = childTimestamp;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
});
|
|
499
658
|
}
|
|
659
|
+
this.cachedDeepUpdatedAt = latest;
|
|
660
|
+
return latest;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* @internal - this is relevant to Verdant's system, not users.
|
|
664
|
+
*
|
|
665
|
+
* Indicates whether this document is from an outdated version
|
|
666
|
+
* of the schema - which means it cannot be used until it is upgraded.
|
|
667
|
+
*/
|
|
668
|
+
get isOutdatedVersion() {
|
|
669
|
+
if (this.parent)
|
|
670
|
+
return this.parent.isOutdatedVersion;
|
|
671
|
+
return this.viewData.fromOlderVersion;
|
|
500
672
|
}
|
|
673
|
+
// array entity methods
|
|
501
674
|
get length() {
|
|
502
|
-
return this.
|
|
675
|
+
return this.view.length;
|
|
503
676
|
}
|
|
504
677
|
// list implements an iterator which maps items to wrapped
|
|
505
678
|
// versions
|
|
506
|
-
[
|
|
679
|
+
[Symbol.iterator]() {
|
|
680
|
+
var _a;
|
|
507
681
|
let index = 0;
|
|
682
|
+
let length = (_a = this.view) === null || _a === void 0 ? void 0 : _a.length;
|
|
508
683
|
return {
|
|
509
684
|
next: () => {
|
|
510
|
-
if (index <
|
|
685
|
+
if (index < length) {
|
|
511
686
|
return {
|
|
512
687
|
value: this.get(index++),
|
|
513
688
|
done: false,
|
|
@@ -521,4 +696,12 @@ export class Entity {
|
|
|
521
696
|
};
|
|
522
697
|
}
|
|
523
698
|
}
|
|
699
|
+
function assertNotSymbol(key) {
|
|
700
|
+
if (typeof key === 'symbol')
|
|
701
|
+
throw new Error("Symbol keys aren't supported");
|
|
702
|
+
}
|
|
703
|
+
function assertNumber(key) {
|
|
704
|
+
if (typeof key !== 'number')
|
|
705
|
+
throw new Error('Only number keys are supported in list entities');
|
|
706
|
+
}
|
|
524
707
|
//# sourceMappingURL=Entity.js.map
|