@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
package/src/entities/Entity.ts
CHANGED
|
@@ -1,185 +1,340 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DocumentBaseline,
|
|
3
|
+
EntityValidationProblem,
|
|
4
|
+
EventSubscriber,
|
|
5
|
+
ObjectIdentifier,
|
|
6
|
+
Operation,
|
|
7
|
+
PatchCreator,
|
|
8
|
+
StorageFieldSchema,
|
|
9
|
+
StorageFieldsSchema,
|
|
2
10
|
assert,
|
|
3
11
|
assignOid,
|
|
4
12
|
cloneDeep,
|
|
13
|
+
compareRefs,
|
|
14
|
+
createFileRef,
|
|
5
15
|
createRef,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
FileRef,
|
|
16
|
+
getChildFieldSchema,
|
|
17
|
+
getDefault,
|
|
18
|
+
hasDefault,
|
|
10
19
|
isFileRef,
|
|
20
|
+
isNullable,
|
|
21
|
+
isObject,
|
|
11
22
|
isObjectRef,
|
|
23
|
+
isPrunePoint,
|
|
24
|
+
isRef,
|
|
12
25
|
maybeGetOid,
|
|
13
|
-
|
|
14
|
-
Operation,
|
|
15
|
-
PatchCreator,
|
|
16
|
-
StorageFieldSchema,
|
|
17
|
-
StorageFieldsSchema,
|
|
18
|
-
TimestampProvider,
|
|
26
|
+
memoByKeys,
|
|
19
27
|
traverseCollectionFieldsAndApplyDefaults,
|
|
20
28
|
validateEntityField,
|
|
21
29
|
} from '@verdant-web/common';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
import { Context } from '../context.js';
|
|
31
|
+
import { FileManager } from '../files/FileManager.js';
|
|
32
|
+
import { isFile, processValueFiles } from '../files/utils.js';
|
|
33
|
+
import { EntityFile } from '../index.js';
|
|
34
|
+
import { EntityCache } from './EntityCache.js';
|
|
35
|
+
import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
|
|
36
|
+
import {
|
|
37
|
+
BaseEntityValue,
|
|
38
|
+
DataFromInit,
|
|
39
|
+
DeepPartial,
|
|
40
|
+
EntityChange,
|
|
41
|
+
EntityEvents,
|
|
42
|
+
ListEntity,
|
|
43
|
+
ListItemInit,
|
|
44
|
+
ListItemValue,
|
|
45
|
+
ObjectEntity,
|
|
46
|
+
} from './types.js';
|
|
47
|
+
import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
|
|
48
|
+
|
|
49
|
+
export interface EntityInit {
|
|
50
|
+
oid: ObjectIdentifier;
|
|
51
|
+
schema: StorageFieldSchema;
|
|
52
|
+
entityFamily?: EntityCache;
|
|
53
|
+
metadataFamily: EntityFamilyMetadata;
|
|
54
|
+
parent?: Entity;
|
|
55
|
+
ctx: Context;
|
|
56
|
+
files: FileManager;
|
|
57
|
+
readonlyKeys?: string[];
|
|
58
|
+
fieldPath?: (string | number)[];
|
|
49
59
|
patchCreator: PatchCreator;
|
|
50
|
-
|
|
51
|
-
getFile: (id: string) => EntityFile;
|
|
52
|
-
time: TimestampProvider;
|
|
53
|
-
now: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type AccessibleEntityProperty<T> = T extends Array<any>
|
|
57
|
-
? number
|
|
58
|
-
: T extends object
|
|
59
|
-
? keyof T
|
|
60
|
-
: never;
|
|
61
|
-
|
|
62
|
-
type DataFromInit<Init> = Init extends { [key: string]: any }
|
|
63
|
-
? {
|
|
64
|
-
[Key in keyof Init]: Init[Key];
|
|
65
|
-
}
|
|
66
|
-
: Init extends Array<any>
|
|
67
|
-
? Init
|
|
68
|
-
: any;
|
|
69
|
-
|
|
70
|
-
export type EntityShape<E extends Entity<any, any>> = E extends Entity<
|
|
71
|
-
infer Value,
|
|
72
|
-
any
|
|
73
|
-
>
|
|
74
|
-
? Value
|
|
75
|
-
: never;
|
|
76
|
-
|
|
77
|
-
// reduces keys of an object to only ones with an optional
|
|
78
|
-
// value
|
|
79
|
-
type DeletableKeys<T> = keyof {
|
|
80
|
-
[Key in keyof T as IfNullableThen<T[Key], Key>]: Key;
|
|
81
|
-
};
|
|
82
|
-
type IfNullableThen<T, Out> = undefined extends T
|
|
83
|
-
? Out
|
|
84
|
-
: null extends T
|
|
85
|
-
? Out
|
|
86
|
-
: never;
|
|
87
|
-
|
|
88
|
-
export function refreshEntity(
|
|
89
|
-
entity: Entity<any, any>,
|
|
90
|
-
info: EntityChangeInfo,
|
|
91
|
-
) {
|
|
92
|
-
return entity[REFRESH](info);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export interface EntityChangeInfo {
|
|
96
|
-
isLocal?: boolean;
|
|
60
|
+
events: EntityStoreEvents;
|
|
97
61
|
}
|
|
98
62
|
|
|
99
|
-
type EntityEvents = {
|
|
100
|
-
change: (info: EntityChangeInfo) => void;
|
|
101
|
-
changeDeep: (target: Entity<any, any>, info: EntityChangeInfo) => void;
|
|
102
|
-
delete: (info: EntityChangeInfo) => void;
|
|
103
|
-
restore: (info: EntityChangeInfo) => void;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
type BaseEntityValue = { [Key: string]: any } | any[];
|
|
107
|
-
|
|
108
63
|
export class Entity<
|
|
109
64
|
Init = any,
|
|
110
65
|
KeyValue extends BaseEntityValue = any,
|
|
111
66
|
Snapshot extends any = DataFromInit<Init>,
|
|
112
67
|
>
|
|
68
|
+
extends EventSubscriber<EntityEvents>
|
|
113
69
|
implements
|
|
114
70
|
ObjectEntity<Init, KeyValue, Snapshot>,
|
|
115
71
|
ListEntity<Init, KeyValue, Snapshot>
|
|
116
72
|
{
|
|
117
|
-
// if current is null, the entity was deleted.
|
|
118
|
-
protected _current: any | null = null;
|
|
119
|
-
|
|
120
73
|
readonly oid: ObjectIdentifier;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
private
|
|
131
|
-
private
|
|
74
|
+
private readonlyKeys: string[];
|
|
75
|
+
private fieldPath: (string | number)[] = [];
|
|
76
|
+
// these are shared between all entities in this family
|
|
77
|
+
private entityFamily: EntityCache;
|
|
78
|
+
private metadataFamily;
|
|
79
|
+
|
|
80
|
+
private schema;
|
|
81
|
+
private parent: Entity | undefined;
|
|
82
|
+
private ctx;
|
|
83
|
+
private files;
|
|
84
|
+
private patchCreator;
|
|
85
|
+
private events;
|
|
86
|
+
|
|
87
|
+
// an internal representation of this Entity.
|
|
88
|
+
// if present, this is the cached, known value. If null,
|
|
89
|
+
// the entity is deleted. If undefined, we need to recompute
|
|
90
|
+
// the view.
|
|
91
|
+
private _viewData: EntityMetadataView | undefined = undefined;
|
|
92
|
+
private validationError: EntityValidationProblem | undefined = undefined;
|
|
132
93
|
private cachedDeepUpdatedAt: number | null = null;
|
|
94
|
+
// only used for root entities to track delete/restore state.
|
|
95
|
+
private wasDeletedLastChange = false;
|
|
96
|
+
private cachedView: any | undefined = undefined;
|
|
133
97
|
|
|
134
|
-
|
|
98
|
+
constructor({
|
|
99
|
+
oid,
|
|
100
|
+
schema,
|
|
101
|
+
entityFamily: childCache,
|
|
102
|
+
parent,
|
|
103
|
+
ctx,
|
|
104
|
+
metadataFamily,
|
|
105
|
+
readonlyKeys,
|
|
106
|
+
files,
|
|
107
|
+
patchCreator,
|
|
108
|
+
events,
|
|
109
|
+
}: EntityInit) {
|
|
110
|
+
super();
|
|
135
111
|
|
|
136
|
-
|
|
112
|
+
assert(!!oid, 'oid is required');
|
|
137
113
|
|
|
138
|
-
|
|
139
|
-
|
|
114
|
+
this.oid = oid;
|
|
115
|
+
this.readonlyKeys = readonlyKeys || [];
|
|
116
|
+
this.ctx = ctx;
|
|
117
|
+
this.files = files;
|
|
118
|
+
this.schema = schema;
|
|
119
|
+
this.entityFamily =
|
|
120
|
+
childCache ||
|
|
121
|
+
new EntityCache({
|
|
122
|
+
initial: [this],
|
|
123
|
+
});
|
|
124
|
+
this.patchCreator = patchCreator;
|
|
125
|
+
this.metadataFamily = metadataFamily;
|
|
126
|
+
this.events = events;
|
|
127
|
+
this.parent = parent;
|
|
128
|
+
|
|
129
|
+
// TODO: should any but the root entity be listening to these?
|
|
130
|
+
if (!this.parent) {
|
|
131
|
+
events.add.attach(this.onAdd);
|
|
132
|
+
events.replace.attach(this.onReplace);
|
|
133
|
+
events.resetAll.attach(this.onResetAll);
|
|
134
|
+
}
|
|
140
135
|
}
|
|
141
136
|
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
137
|
+
private onAdd = (_store: any, data: EntityStoreEventData) => {
|
|
138
|
+
if (data.oid === this.oid) {
|
|
139
|
+
this.addConfirmedData(data);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
private onReplace = (_store: any, data: EntityStoreEventData) => {
|
|
143
|
+
if (data.oid === this.oid) {
|
|
144
|
+
this.replaceAllData(data);
|
|
145
145
|
}
|
|
146
|
+
};
|
|
147
|
+
private onResetAll = () => {
|
|
148
|
+
this.resetAllData();
|
|
149
|
+
};
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
private get metadata() {
|
|
152
|
+
return this.metadataFamily.get(this.oid);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The view of this Entity, not including nested
|
|
157
|
+
* entities (that's the snapshot - see #getSnapshot())
|
|
158
|
+
*
|
|
159
|
+
* Nested entities are represented by refs.
|
|
160
|
+
*/
|
|
161
|
+
private get viewData() {
|
|
162
|
+
if (this._viewData === undefined) {
|
|
163
|
+
this._viewData = this.metadata.computeView();
|
|
164
|
+
this.validate();
|
|
165
|
+
}
|
|
166
|
+
return this._viewData;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** convenience getter for viewData.view */
|
|
170
|
+
private get rawView() {
|
|
171
|
+
return this.viewData.view;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* An Entity's View includes the rendering of its underlying data,
|
|
176
|
+
* connecting of children where refs were, and validation
|
|
177
|
+
* and pruning according to schema.
|
|
178
|
+
*/
|
|
179
|
+
private get view() {
|
|
180
|
+
if (this.cachedView !== undefined) {
|
|
181
|
+
return this.cachedView;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.viewData.deleted) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// can't use invalid data - but this should be bubbled up to
|
|
188
|
+
// a prune point
|
|
189
|
+
const rawView = this.rawView;
|
|
190
|
+
|
|
191
|
+
const viewIsWrongType =
|
|
192
|
+
(!rawView && !isNullable(this.schema)) ||
|
|
193
|
+
(this.schema.type === 'array' && !Array.isArray(rawView)) ||
|
|
194
|
+
((this.schema.type === 'object' || this.schema.type === 'map') &&
|
|
195
|
+
!isObject(rawView));
|
|
196
|
+
|
|
197
|
+
if (viewIsWrongType) {
|
|
198
|
+
// this will cover lists and maps, too.
|
|
199
|
+
if (hasDefault(this.schema)) {
|
|
200
|
+
return getDefault(this.schema);
|
|
153
201
|
}
|
|
154
|
-
|
|
202
|
+
// force null - invalid - will require parent prune
|
|
203
|
+
return null as any;
|
|
155
204
|
}
|
|
156
205
|
|
|
157
|
-
|
|
206
|
+
this.cachedView = this.isList ? [] : {};
|
|
207
|
+
assignOid(this.cachedView, this.oid);
|
|
208
|
+
|
|
209
|
+
if (Array.isArray(rawView)) {
|
|
210
|
+
const schema = getChildFieldSchema(this.schema, 0);
|
|
211
|
+
if (!schema) {
|
|
212
|
+
/**
|
|
213
|
+
* PRUNE - this is a prune point. we can't continue
|
|
214
|
+
* to render this data, so we'll just return [].
|
|
215
|
+
* This skips the loop.
|
|
216
|
+
*/
|
|
217
|
+
this.ctx.log(
|
|
218
|
+
'error',
|
|
219
|
+
'No child field schema for list entity.',
|
|
220
|
+
this.oid,
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
for (let i = 0; i < rawView.length; i++) {
|
|
224
|
+
const child = this.get(i);
|
|
225
|
+
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
226
|
+
this.ctx.log(
|
|
227
|
+
'error',
|
|
228
|
+
'Child missing in non-nullable field',
|
|
229
|
+
this.oid,
|
|
230
|
+
'index:',
|
|
231
|
+
i,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// this item will be pruned.
|
|
235
|
+
} else {
|
|
236
|
+
this.cachedView.push(child);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} else if (isObject(rawView)) {
|
|
241
|
+
// iterate over known properties in object-type entities;
|
|
242
|
+
// for maps, we just iterate over the keys.
|
|
243
|
+
const keys =
|
|
244
|
+
this.schema.type === 'object'
|
|
245
|
+
? Object.keys(this.schema.properties)
|
|
246
|
+
: Object.keys(rawView);
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
249
|
+
if (!schema) {
|
|
250
|
+
/**
|
|
251
|
+
* PRUNE - this is a prune point. we can't continue
|
|
252
|
+
* to render this data. If this is a map, it will be
|
|
253
|
+
* pruned empty. Otherwise, prune moves upward.
|
|
254
|
+
*
|
|
255
|
+
* This exits the loop.
|
|
256
|
+
*/
|
|
257
|
+
this.ctx.log(
|
|
258
|
+
'error',
|
|
259
|
+
'No child field schema for object entity at key',
|
|
260
|
+
key,
|
|
261
|
+
);
|
|
262
|
+
if (this.schema.type === 'map') {
|
|
263
|
+
// it's valid to prune here if it's a map
|
|
264
|
+
this.cachedView = {};
|
|
265
|
+
} else {
|
|
266
|
+
// otherwise prune moves upward
|
|
267
|
+
this.cachedView = null;
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
const child = this.get(key as any);
|
|
272
|
+
if (this.childIsNull(child) && !isNullable(schema)) {
|
|
273
|
+
this.ctx.log(
|
|
274
|
+
'error',
|
|
275
|
+
'Child entity is missing for non-nullable field',
|
|
276
|
+
this.oid,
|
|
277
|
+
'key:',
|
|
278
|
+
key,
|
|
279
|
+
);
|
|
280
|
+
/**
|
|
281
|
+
* PRUNE - this is a prune point. we can't continue
|
|
282
|
+
* to render this data. If this is a map, we can ignore
|
|
283
|
+
* this value. Otherwise we must prune upward.
|
|
284
|
+
* This exits the loop.
|
|
285
|
+
*/
|
|
286
|
+
if (this.schema.type !== 'map') {
|
|
287
|
+
this.cachedView = null;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this.cachedView[key] = child;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return this.cachedView;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private childIsNull = (child: any) => {
|
|
300
|
+
if (child instanceof Entity) {
|
|
301
|
+
const childView = child.view;
|
|
302
|
+
return childView === null || childView === undefined;
|
|
303
|
+
}
|
|
304
|
+
return child === null || child === undefined;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
get uid() {
|
|
308
|
+
return this.oid;
|
|
158
309
|
}
|
|
159
310
|
|
|
160
311
|
get deleted() {
|
|
161
|
-
return this.
|
|
312
|
+
return this.viewData.deleted || this.view === null;
|
|
162
313
|
}
|
|
163
314
|
|
|
164
|
-
|
|
165
|
-
return this.
|
|
315
|
+
get invalid() {
|
|
316
|
+
return !!this.validate();
|
|
166
317
|
}
|
|
167
318
|
|
|
168
319
|
get isList() {
|
|
169
|
-
|
|
320
|
+
// have to turn TS off here as our two interfaces both implement
|
|
321
|
+
// const values for this boolean.
|
|
322
|
+
return (
|
|
323
|
+
this.schema.type === 'array' || (Array.isArray(this.viewData.view) as any)
|
|
324
|
+
);
|
|
170
325
|
}
|
|
171
326
|
|
|
172
327
|
get updatedAt() {
|
|
173
|
-
return this.
|
|
328
|
+
return this.viewData.updatedAt;
|
|
174
329
|
}
|
|
175
330
|
|
|
176
331
|
get deepUpdatedAt() {
|
|
177
332
|
if (this.cachedDeepUpdatedAt) return this.cachedDeepUpdatedAt;
|
|
178
333
|
// iterate over all children and take the latest timestamp
|
|
179
|
-
let latest: number | null = this.
|
|
334
|
+
let latest: number | null = this.updatedAt;
|
|
180
335
|
if (this.isList) {
|
|
181
|
-
this.forEach((child) => {
|
|
182
|
-
if (
|
|
336
|
+
this.forEach((child: any) => {
|
|
337
|
+
if (child instanceof Entity) {
|
|
183
338
|
const childTimestamp = child.deepUpdatedAt;
|
|
184
339
|
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
185
340
|
latest = childTimestamp;
|
|
@@ -188,7 +343,7 @@ export class Entity<
|
|
|
188
343
|
});
|
|
189
344
|
} else {
|
|
190
345
|
this.values().forEach((child) => {
|
|
191
|
-
if (
|
|
346
|
+
if (child instanceof Entity) {
|
|
192
347
|
const childTimestamp = child.deepUpdatedAt;
|
|
193
348
|
if (childTimestamp && (!latest || childTimestamp > latest)) {
|
|
194
349
|
latest = childTimestamp;
|
|
@@ -200,170 +355,253 @@ export class Entity<
|
|
|
200
355
|
return latest;
|
|
201
356
|
}
|
|
202
357
|
|
|
203
|
-
|
|
204
|
-
|
|
358
|
+
/**
|
|
359
|
+
* @internal - this is relevant to Verdant's system, not users.
|
|
360
|
+
*
|
|
361
|
+
* Indicates whether this document is from an outdated version
|
|
362
|
+
* of the schema - which means it cannot be used until it is upgraded.
|
|
363
|
+
*/
|
|
364
|
+
get isOutdatedVersion(): boolean {
|
|
365
|
+
if (this.parent) return this.parent.isOutdatedVersion;
|
|
366
|
+
return this.viewData.fromOlderVersion;
|
|
205
367
|
}
|
|
206
368
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}) {
|
|
226
|
-
this.oid = oid;
|
|
227
|
-
const { collection } = decomposeOid(oid);
|
|
228
|
-
this.collection = collection;
|
|
229
|
-
this.store = store;
|
|
230
|
-
this.fieldSchema = fieldSchema;
|
|
231
|
-
this.fieldPath = fieldPath;
|
|
232
|
-
this.readonlyKeys = readonlyKeys;
|
|
233
|
-
this.cache = cache;
|
|
234
|
-
this.parent = parent && this.cache.weakRef(parent);
|
|
235
|
-
const { view, deleted, lastTimestamp } = this.cache.computeView(oid);
|
|
236
|
-
this._current = view;
|
|
237
|
-
this._deleted = deleted;
|
|
238
|
-
this._updatedAt = lastTimestamp ? lastTimestamp : null;
|
|
239
|
-
this.cachedDeepUpdatedAt = null;
|
|
240
|
-
this.events = new EventSubscriber<EntityEvents>(() => {
|
|
241
|
-
if (!this.hasSubscribers) {
|
|
242
|
-
onAllUnsubscribed?.();
|
|
243
|
-
}
|
|
244
|
-
});
|
|
369
|
+
/**
|
|
370
|
+
* Pruning - when entities have invalid children, we 'prune' that
|
|
371
|
+
* data up to the nearest prunable point - a nullable field,
|
|
372
|
+
* or a list.
|
|
373
|
+
*/
|
|
374
|
+
protected validate = memoByKeys(
|
|
375
|
+
() => {
|
|
376
|
+
this.validationError =
|
|
377
|
+
validateEntityField({
|
|
378
|
+
field: this.schema,
|
|
379
|
+
value: this.rawView,
|
|
380
|
+
fieldPath: this.fieldPath,
|
|
381
|
+
depth: 1,
|
|
382
|
+
}) ?? undefined;
|
|
383
|
+
return this.validationError;
|
|
384
|
+
},
|
|
385
|
+
() => [this.viewData],
|
|
386
|
+
);
|
|
245
387
|
|
|
246
|
-
|
|
247
|
-
|
|
388
|
+
private viewWithMappedChildren = (
|
|
389
|
+
mapper: (child: Entity | EntityFile) => any,
|
|
390
|
+
) => {
|
|
391
|
+
const view = this.view;
|
|
392
|
+
if (!view) {
|
|
393
|
+
return null;
|
|
248
394
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this.cachedDeepUpdatedAt = null;
|
|
260
|
-
|
|
261
|
-
if (this._deleted) {
|
|
262
|
-
this.events.emit('delete', info);
|
|
395
|
+
if (Array.isArray(view)) {
|
|
396
|
+
const mapped = view.map((value) => {
|
|
397
|
+
if (value instanceof Entity || value instanceof EntityFile) {
|
|
398
|
+
return mapper(value);
|
|
399
|
+
} else {
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
assignOid(mapped, this.oid);
|
|
404
|
+
return mapped;
|
|
263
405
|
} else {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
406
|
+
const mapped = Object.entries(view).reduce((acc, [key, value]) => {
|
|
407
|
+
if (value instanceof Entity || value instanceof EntityFile) {
|
|
408
|
+
acc[key as any] = mapper(value);
|
|
409
|
+
} else {
|
|
410
|
+
acc[key as any] = value;
|
|
411
|
+
}
|
|
412
|
+
return acc;
|
|
413
|
+
}, {} as any);
|
|
414
|
+
assignOid(mapped, this.oid);
|
|
415
|
+
return mapped;
|
|
270
416
|
}
|
|
271
417
|
};
|
|
272
418
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
this.
|
|
279
|
-
this.events.emit('changeDeep', source, info);
|
|
280
|
-
const parent = this.parent?.deref();
|
|
281
|
-
if (parent) {
|
|
282
|
-
parent[DEEP_CHANGE](source, info);
|
|
283
|
-
}
|
|
419
|
+
/**
|
|
420
|
+
* A current snapshot of this Entity's data, including nested
|
|
421
|
+
* Entities.
|
|
422
|
+
*/
|
|
423
|
+
getSnapshot = (): any => {
|
|
424
|
+
return this.viewWithMappedChildren((child) => child.getSnapshot());
|
|
284
425
|
};
|
|
285
426
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return this.fieldSchema.values;
|
|
293
|
-
} else if (this.fieldSchema.type === 'any') {
|
|
294
|
-
return this.fieldSchema;
|
|
427
|
+
// change management methods (internal use only)
|
|
428
|
+
private addPendingOperations = (operations: Operation[]) => {
|
|
429
|
+
this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
|
|
430
|
+
const changes = this.metadataFamily.addPendingData(operations);
|
|
431
|
+
for (const change of changes) {
|
|
432
|
+
this.change(change);
|
|
295
433
|
}
|
|
296
|
-
throw new Error('Invalid field schema');
|
|
297
434
|
};
|
|
298
435
|
|
|
299
|
-
|
|
300
|
-
this.
|
|
436
|
+
private addConfirmedData = (data: EntityStoreEventData) => {
|
|
437
|
+
this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
|
|
438
|
+
const changes = this.metadataFamily.addConfirmedData(data);
|
|
439
|
+
for (const change of changes) {
|
|
440
|
+
this.change(change);
|
|
441
|
+
}
|
|
301
442
|
};
|
|
302
443
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return unsubscribe;
|
|
444
|
+
private replaceAllData = (data: EntityStoreEventData) => {
|
|
445
|
+
this.ctx.log('debug', 'Entity: replacing all data', this.oid);
|
|
446
|
+
const changes = this.metadataFamily.replaceAllData(data);
|
|
447
|
+
for (const change of changes) {
|
|
448
|
+
this.change(change);
|
|
449
|
+
}
|
|
310
450
|
};
|
|
311
451
|
|
|
312
|
-
|
|
313
|
-
this.
|
|
452
|
+
private resetAllData = () => {
|
|
453
|
+
this.ctx.log('debug', 'Entity: resetting all data', this.oid);
|
|
454
|
+
this.cachedDeepUpdatedAt = null;
|
|
455
|
+
this.cachedView = undefined;
|
|
456
|
+
this._viewData = undefined;
|
|
457
|
+
const changes = this.metadataFamily.replaceAllData({});
|
|
458
|
+
for (const change of changes) {
|
|
459
|
+
this.change(change);
|
|
460
|
+
}
|
|
314
461
|
};
|
|
315
462
|
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
463
|
+
private change = (ev: EntityChange) => {
|
|
464
|
+
if (ev.oid === this.oid) {
|
|
465
|
+
// reset cached view
|
|
466
|
+
this._viewData = undefined;
|
|
467
|
+
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
|
+
if (!this.parent) {
|
|
476
|
+
// newly deleted - emit event
|
|
477
|
+
if (this.deleted && !this.wasDeletedLastChange) {
|
|
478
|
+
this.ctx.log('debug', 'Entity deleted', this.oid);
|
|
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
|
+
}
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
// forward it to the correct family member. if none exists
|
|
490
|
+
// in cache, no one will hear it anyways.
|
|
491
|
+
const other = this.entityFamily.getCached(ev.oid);
|
|
492
|
+
if (other && other instanceof Entity) {
|
|
493
|
+
other.change(ev);
|
|
494
|
+
}
|
|
319
495
|
}
|
|
320
|
-
|
|
496
|
+
};
|
|
497
|
+
protected deepChange = (target: Entity, ev: EntityChange) => {
|
|
498
|
+
// reset cached deep updated at timestamp; either this
|
|
499
|
+
// entity or children have changed
|
|
500
|
+
this.cachedDeepUpdatedAt = null;
|
|
501
|
+
// reset this flag to recompute snapshot data - children
|
|
502
|
+
// or self has changed. new pruning needs to happen.
|
|
503
|
+
this.cachedView = undefined;
|
|
504
|
+
this.ctx.log(
|
|
505
|
+
'debug',
|
|
506
|
+
'Deep change detected at',
|
|
507
|
+
this.oid,
|
|
508
|
+
'reset cached view',
|
|
509
|
+
);
|
|
510
|
+
this.ctx.log('debug', 'Emitting deep change event', this.oid);
|
|
511
|
+
this.emit('changeDeep', target, ev);
|
|
512
|
+
this.parent?.deepChange(target, ev);
|
|
321
513
|
};
|
|
322
514
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return this.cache.getEntity({
|
|
515
|
+
private getChild = (key: any, oid: ObjectIdentifier) => {
|
|
516
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
517
|
+
if (!schema) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return this.entityFamily.get({
|
|
332
523
|
oid,
|
|
333
|
-
|
|
524
|
+
schema,
|
|
525
|
+
entityFamily: this.entityFamily,
|
|
526
|
+
metadataFamily: this.metadataFamily,
|
|
334
527
|
parent: this,
|
|
335
|
-
|
|
528
|
+
ctx: this.ctx,
|
|
529
|
+
files: this.files,
|
|
530
|
+
fieldPath: [...this.fieldPath, key],
|
|
531
|
+
patchCreator: this.patchCreator,
|
|
532
|
+
events: this.events,
|
|
336
533
|
});
|
|
337
534
|
};
|
|
338
535
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
536
|
+
// generic entity methods
|
|
537
|
+
/**
|
|
538
|
+
* Gets a value from this Entity. If the value
|
|
539
|
+
* is an object, it will be wrapped in another
|
|
540
|
+
* Entity.
|
|
541
|
+
*/
|
|
542
|
+
get = <Key extends keyof KeyValue>(key: Key): KeyValue[Key] => {
|
|
543
|
+
assertNotSymbol(key);
|
|
544
|
+
|
|
545
|
+
const view = this.rawView;
|
|
546
|
+
if (!view) {
|
|
349
547
|
throw new Error(
|
|
350
|
-
`
|
|
548
|
+
`Cannot access data at key ${key} on deleted entity ${this.oid}`,
|
|
351
549
|
);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
550
|
+
}
|
|
551
|
+
const child = view[key as any];
|
|
552
|
+
const schema = getChildFieldSchema(this.schema, key);
|
|
553
|
+
if (!schema) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`No schema for key ${String(key)} in ${JSON.stringify(this.schema)}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (isRef(child)) {
|
|
559
|
+
if (isFileRef(child)) {
|
|
560
|
+
if (schema.type !== 'file') {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Expected file schema for key ${String(key)}, got ${schema.type}`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const file = this.files.get(child.id, {
|
|
566
|
+
downloadRemote: !!schema.downloadRemote,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// FIXME: this seems bad and inconsistent
|
|
355
570
|
file.subscribe('change', () => {
|
|
356
|
-
this
|
|
357
|
-
isLocal: false,
|
|
358
|
-
});
|
|
571
|
+
this.deepChange(this, { isLocal: false, oid: this.oid });
|
|
359
572
|
});
|
|
360
|
-
|
|
573
|
+
|
|
574
|
+
return file as KeyValue[Key];
|
|
575
|
+
} else {
|
|
576
|
+
return this.getChild(key, child.id) as KeyValue[Key];
|
|
361
577
|
}
|
|
578
|
+
} else {
|
|
579
|
+
// prune invalid primitive fields
|
|
580
|
+
if (
|
|
581
|
+
validateEntityField({
|
|
582
|
+
field: schema,
|
|
583
|
+
value: child,
|
|
584
|
+
fieldPath: [...this.fieldPath, key],
|
|
585
|
+
depth: 1,
|
|
586
|
+
requireDefaults: true,
|
|
587
|
+
})
|
|
588
|
+
) {
|
|
589
|
+
if (hasDefault(schema)) {
|
|
590
|
+
return getDefault(schema);
|
|
591
|
+
}
|
|
592
|
+
if (isNullable(schema)) {
|
|
593
|
+
return null as any;
|
|
594
|
+
}
|
|
595
|
+
return undefined as any;
|
|
596
|
+
}
|
|
597
|
+
return child as KeyValue[Key];
|
|
362
598
|
}
|
|
363
|
-
return value;
|
|
364
599
|
};
|
|
365
600
|
|
|
366
|
-
|
|
601
|
+
private processInputValue = (value: any, key: any) => {
|
|
602
|
+
if (this.readonlyKeys.includes(key as string)) {
|
|
603
|
+
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
604
|
+
}
|
|
367
605
|
// disassociate incoming OIDs on values and generally break object
|
|
368
606
|
// references. cloning doesn't work on files so those are
|
|
369
607
|
// filtered out.
|
|
@@ -378,350 +616,264 @@ export class Entity<
|
|
|
378
616
|
// referenced in multiple entities, which could mean introduction
|
|
379
617
|
// of foreign OIDs, or one object being assigned different OIDs
|
|
380
618
|
// with unexpected results.
|
|
381
|
-
if (!(value
|
|
619
|
+
if (!isFile(value)) {
|
|
382
620
|
value = cloneDeep(value, false);
|
|
383
621
|
}
|
|
384
|
-
const fieldSchema = this.
|
|
622
|
+
const fieldSchema = getChildFieldSchema(this.schema, key);
|
|
385
623
|
if (fieldSchema) {
|
|
386
624
|
traverseCollectionFieldsAndApplyDefaults(value, fieldSchema);
|
|
625
|
+
const validationError = validateEntityField({
|
|
626
|
+
field: fieldSchema,
|
|
627
|
+
value,
|
|
628
|
+
fieldPath: [...this.fieldPath, key],
|
|
629
|
+
});
|
|
630
|
+
if (validationError) {
|
|
631
|
+
// TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
|
|
632
|
+
// but also we don't really want invalid data supplied.
|
|
633
|
+
throw new Error(validationError.message);
|
|
634
|
+
}
|
|
387
635
|
}
|
|
388
|
-
|
|
389
|
-
...this.fieldPath,
|
|
390
|
-
key,
|
|
391
|
-
]);
|
|
392
|
-
if (validationError) {
|
|
393
|
-
// TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
|
|
394
|
-
// but also we don't really want invalid data supplied.
|
|
395
|
-
throw new Error(validationError);
|
|
396
|
-
}
|
|
397
|
-
return processValueFiles(value, this.store.addFile);
|
|
636
|
+
return processValueFiles(value, this.files.add);
|
|
398
637
|
};
|
|
399
638
|
|
|
400
|
-
|
|
401
|
-
if (this.
|
|
402
|
-
|
|
639
|
+
private getDeleteMode = (key: any) => {
|
|
640
|
+
if (this.readonlyKeys.includes(key)) {
|
|
641
|
+
return false;
|
|
403
642
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
getAll = (): KeyValue => {
|
|
410
|
-
if (this.value === undefined || this.value === null) {
|
|
411
|
-
throw new Error('Cannot access deleted entity');
|
|
643
|
+
// any is always deletable, and map values
|
|
644
|
+
if (this.schema.type === 'any' || this.schema.type === 'map') {
|
|
645
|
+
return 'delete';
|
|
412
646
|
}
|
|
413
647
|
|
|
414
|
-
if (this.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
) as any;
|
|
421
|
-
} else {
|
|
422
|
-
result = {} as any;
|
|
423
|
-
for (const key in this.value) {
|
|
424
|
-
result[key as any] = this.get(key as any);
|
|
648
|
+
if (this.schema.type === 'object') {
|
|
649
|
+
const property = this.schema.properties[key];
|
|
650
|
+
if (!property) {
|
|
651
|
+
// huh, the property doesn't exist. it's ok to
|
|
652
|
+
// remove I suppose.
|
|
653
|
+
return 'delete';
|
|
425
654
|
}
|
|
655
|
+
if (property.type === 'any') return 'delete';
|
|
656
|
+
// map can't be nullable. should it be?
|
|
657
|
+
if (property.type === 'map') return false;
|
|
658
|
+
if (property.nullable) return 'null';
|
|
426
659
|
}
|
|
427
|
-
|
|
428
|
-
return
|
|
660
|
+
// no other types are deletable
|
|
661
|
+
return false;
|
|
429
662
|
};
|
|
430
663
|
|
|
431
|
-
private getFileSnapshot(item: FileRef) {
|
|
432
|
-
const file = this.store.getFile(item.id);
|
|
433
|
-
if (file.url) {
|
|
434
|
-
return { id: item.id, url: file.url };
|
|
435
|
-
} else if (file.loading || file.failed) {
|
|
436
|
-
return { id: item.id, url: undefined };
|
|
437
|
-
} else {
|
|
438
|
-
return { id: item.id, url: null };
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
664
|
/**
|
|
443
|
-
* Returns
|
|
444
|
-
*
|
|
665
|
+
* Returns the referent value of an item in the list, used for
|
|
666
|
+
* operations which act on items. if the item is an object,
|
|
667
|
+
* it will attempt to create an OID reference to it. If it
|
|
668
|
+
* is a primitive, it will return the primitive.
|
|
445
669
|
*/
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
return
|
|
670
|
+
private getItemRefValue = (item: any) => {
|
|
671
|
+
if (item instanceof Entity) {
|
|
672
|
+
return createRef(item.oid);
|
|
449
673
|
}
|
|
450
|
-
if (
|
|
451
|
-
return
|
|
674
|
+
if (item instanceof EntityFile) {
|
|
675
|
+
return createFileRef(item.id);
|
|
452
676
|
}
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
} else if (isFileRef(item)) {
|
|
462
|
-
return this.getFileSnapshot(item);
|
|
463
|
-
}
|
|
464
|
-
return item;
|
|
465
|
-
}) as Snapshot;
|
|
466
|
-
} else {
|
|
467
|
-
snapshot = { ...this.value };
|
|
468
|
-
for (const [key, value] of Object.entries(snapshot)) {
|
|
469
|
-
if (isObjectRef(value)) {
|
|
470
|
-
snapshot[key] = this.getSubObject(value.id, key)?.getSnapshot();
|
|
471
|
-
} else if (isFileRef(value)) {
|
|
472
|
-
snapshot[key] = this.getFileSnapshot(value);
|
|
473
|
-
}
|
|
677
|
+
if (typeof item === 'object') {
|
|
678
|
+
const itemOid = maybeGetOid(item);
|
|
679
|
+
if (!itemOid || !this.entityFamily.has(itemOid)) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Cannot move object ${JSON.stringify(
|
|
682
|
+
item,
|
|
683
|
+
)} which does not exist in this list`,
|
|
684
|
+
);
|
|
474
685
|
}
|
|
686
|
+
return createRef(itemOid);
|
|
687
|
+
} else {
|
|
688
|
+
return item;
|
|
475
689
|
}
|
|
476
|
-
|
|
477
|
-
assignOid(snapshot, this.oid);
|
|
478
|
-
this.cachedSnapshot = snapshot;
|
|
479
|
-
return snapshot;
|
|
480
690
|
};
|
|
481
691
|
|
|
482
|
-
/**
|
|
483
|
-
* Object methods
|
|
484
|
-
*/
|
|
485
|
-
keys = () => {
|
|
486
|
-
return Object.keys(this.value || {});
|
|
487
|
-
};
|
|
488
|
-
entries = () => {
|
|
489
|
-
return Object.entries(this.getAll());
|
|
490
|
-
};
|
|
491
|
-
values = () => {
|
|
492
|
-
return Object.values(this.getAll());
|
|
493
|
-
};
|
|
494
692
|
set = <Key extends keyof Init>(key: Key, value: Init[Key]) => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.addPatches(
|
|
499
|
-
this.store.patchCreator.createSet(
|
|
693
|
+
assertNotSymbol(key);
|
|
694
|
+
this.addPendingOperations(
|
|
695
|
+
this.patchCreator.createSet(
|
|
500
696
|
this.oid,
|
|
501
|
-
key
|
|
697
|
+
key,
|
|
502
698
|
this.processInputValue(value, key),
|
|
503
699
|
),
|
|
504
700
|
);
|
|
505
701
|
};
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Returns a destructured version of this Entity, where child
|
|
705
|
+
* Entities are accessible at their respective keys.
|
|
706
|
+
*/
|
|
707
|
+
getAll = (): KeyValue => {
|
|
708
|
+
return this.view;
|
|
709
|
+
};
|
|
710
|
+
|
|
506
711
|
delete = (key: any) => {
|
|
507
|
-
if (
|
|
508
|
-
|
|
509
|
-
|
|
712
|
+
if (this.isList) {
|
|
713
|
+
assertNumber(key);
|
|
714
|
+
this.addPendingOperations(
|
|
715
|
+
this.patchCreator.createListDelete(this.oid, key),
|
|
510
716
|
);
|
|
511
717
|
} else {
|
|
512
|
-
// the key must be deletable - i.e. optional in the schema
|
|
718
|
+
// the key must be deletable - i.e. optional in the schema.
|
|
513
719
|
const deleteMode = this.getDeleteMode(key);
|
|
514
720
|
if (!deleteMode) {
|
|
515
721
|
throw new Error(
|
|
516
|
-
`Cannot delete key ${key} - the property is not marked as optional in the schema
|
|
722
|
+
`Cannot delete key ${key.toString()} - the property is not marked as optional in the schema.`,
|
|
517
723
|
);
|
|
518
724
|
}
|
|
519
725
|
if (deleteMode === 'delete') {
|
|
520
|
-
this.
|
|
726
|
+
this.addPendingOperations(
|
|
727
|
+
this.patchCreator.createRemove(this.oid, key),
|
|
728
|
+
);
|
|
521
729
|
} else {
|
|
522
|
-
this.
|
|
730
|
+
this.addPendingOperations(
|
|
731
|
+
this.patchCreator.createSet(this.oid, key, null),
|
|
732
|
+
);
|
|
523
733
|
}
|
|
524
734
|
}
|
|
525
735
|
};
|
|
526
|
-
private getDeleteMode = (key: any) => {
|
|
527
|
-
if (this.readonlyKeys.includes(key)) {
|
|
528
|
-
return false;
|
|
529
|
-
}
|
|
530
|
-
// 'any' is always deletable, and map values can be removed completely
|
|
531
|
-
if (this.fieldSchema.type === 'any' || this.fieldSchema.type === 'map') {
|
|
532
|
-
return 'delete';
|
|
533
|
-
}
|
|
534
736
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// no other parent objects support deleting
|
|
550
|
-
return false;
|
|
737
|
+
// object entity methods
|
|
738
|
+
keys = (): string[] => {
|
|
739
|
+
if (!this.view) return [];
|
|
740
|
+
return Object.keys(this.view);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
entries = (): [string, Exclude<KeyValue[keyof KeyValue], undefined>][] => {
|
|
744
|
+
if (!this.view) return [];
|
|
745
|
+
return Object.entries(this.view);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
values = (): Exclude<KeyValue[keyof KeyValue], undefined>[] => {
|
|
749
|
+
if (!this.view) return [];
|
|
750
|
+
return Object.values(this.view);
|
|
551
751
|
};
|
|
552
|
-
/** @deprecated - renamed to delete */
|
|
553
|
-
remove = this.delete.bind(this);
|
|
554
752
|
|
|
555
753
|
update = (
|
|
556
|
-
|
|
754
|
+
data: DeepPartial<Init>,
|
|
557
755
|
{
|
|
558
|
-
replaceSubObjects = false,
|
|
559
756
|
merge = true,
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
*/
|
|
565
|
-
replaceSubObjects?: boolean;
|
|
566
|
-
/**
|
|
567
|
-
* If false, omitted keys will erase their respective fields.
|
|
568
|
-
*/
|
|
569
|
-
merge?: boolean;
|
|
570
|
-
} = {
|
|
571
|
-
replaceSubObjects: false,
|
|
572
|
-
merge: true,
|
|
573
|
-
},
|
|
574
|
-
) => {
|
|
575
|
-
if (
|
|
576
|
-
!merge &&
|
|
577
|
-
this.fieldSchema.type !== 'any' &&
|
|
578
|
-
this.fieldSchema.type !== 'map'
|
|
579
|
-
) {
|
|
757
|
+
replaceSubObjects = false,
|
|
758
|
+
}: { replaceSubObjects?: boolean; merge?: boolean } = {},
|
|
759
|
+
): void => {
|
|
760
|
+
if (!merge && this.schema.type !== 'any' && this.schema.type !== 'map') {
|
|
580
761
|
throw new Error(
|
|
581
762
|
'Cannot use .update without merge if the field has a strict schema type. merge: false is only available on "any" or "map" types.',
|
|
582
763
|
);
|
|
583
764
|
}
|
|
584
|
-
|
|
765
|
+
const changes: any = {};
|
|
766
|
+
assignOid(changes, this.oid);
|
|
767
|
+
for (const [key, field] of Object.entries(data)) {
|
|
585
768
|
if (this.readonlyKeys.includes(key as any)) {
|
|
586
769
|
throw new Error(`Cannot set readonly key ${key.toString()}`);
|
|
587
770
|
}
|
|
588
|
-
const fieldSchema = this.
|
|
771
|
+
const fieldSchema = getChildFieldSchema(this.schema, key);
|
|
589
772
|
if (fieldSchema) {
|
|
590
773
|
traverseCollectionFieldsAndApplyDefaults(field, fieldSchema);
|
|
591
774
|
}
|
|
775
|
+
changes[key] = this.processInputValue(field, key);
|
|
592
776
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
{
|
|
599
|
-
mergeUnknownObjects: !replaceSubObjects,
|
|
600
|
-
defaultUndefined: merge,
|
|
601
|
-
},
|
|
602
|
-
),
|
|
777
|
+
this.addPendingOperations(
|
|
778
|
+
this.patchCreator.createDiff(this.getSnapshot(), changes, {
|
|
779
|
+
mergeUnknownObjects: !replaceSubObjects,
|
|
780
|
+
defaultUndefined: merge,
|
|
781
|
+
}),
|
|
603
782
|
);
|
|
604
783
|
};
|
|
605
784
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Returns the referent value of an item in the list, used for
|
|
612
|
-
* operations which act on items. if the item is an object,
|
|
613
|
-
* it will attempt to create an OID reference to it. If it
|
|
614
|
-
* is a primitive, it will return the primitive.
|
|
615
|
-
*/
|
|
616
|
-
private getItemRefValue = (item: ListItemValue<KeyValue>) => {
|
|
617
|
-
if (typeof item === 'object') {
|
|
618
|
-
const itemOid = maybeGetOid(item);
|
|
619
|
-
if (!itemOid || !this.cache.hasOid(itemOid)) {
|
|
620
|
-
throw new Error(
|
|
621
|
-
`Cannot move object ${JSON.stringify(
|
|
622
|
-
item,
|
|
623
|
-
)} which does not exist in this list`,
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
|
-
return itemOid;
|
|
627
|
-
} else {
|
|
628
|
-
return item;
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
get length() {
|
|
633
|
-
return this.value.length;
|
|
785
|
+
// array entity methods
|
|
786
|
+
get length(): number {
|
|
787
|
+
return this.view.length;
|
|
634
788
|
}
|
|
635
789
|
|
|
636
|
-
push = (value: ListItemInit<Init>) => {
|
|
637
|
-
this.
|
|
638
|
-
this.
|
|
790
|
+
push = (value: ListItemInit<Init>): void => {
|
|
791
|
+
this.addPendingOperations(
|
|
792
|
+
this.patchCreator.createListPush(
|
|
639
793
|
this.oid,
|
|
640
|
-
this.processInputValue(value, this.
|
|
794
|
+
this.processInputValue(value, this.view.length),
|
|
641
795
|
),
|
|
642
796
|
);
|
|
643
797
|
};
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
798
|
+
|
|
799
|
+
insert = (index: number, value: ListItemInit<Init>): void => {
|
|
800
|
+
this.addPendingOperations(
|
|
801
|
+
this.patchCreator.createListInsert(
|
|
647
802
|
this.oid,
|
|
648
803
|
index,
|
|
649
804
|
this.processInputValue(value, index),
|
|
650
805
|
),
|
|
651
806
|
);
|
|
652
807
|
};
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
808
|
+
|
|
809
|
+
move = (from: number, to: number): void => {
|
|
810
|
+
this.addPendingOperations(
|
|
811
|
+
this.patchCreator.createListMoveByIndex(this.oid, from, to),
|
|
656
812
|
);
|
|
657
813
|
};
|
|
658
|
-
|
|
814
|
+
|
|
815
|
+
moveItem = (item: ListItemValue<KeyValue>, to: number): void => {
|
|
659
816
|
const itemRef = this.getItemRefValue(item);
|
|
660
|
-
if (
|
|
661
|
-
this.
|
|
662
|
-
this.
|
|
817
|
+
if (isRef(itemRef)) {
|
|
818
|
+
this.addPendingOperations(
|
|
819
|
+
this.patchCreator.createListMoveByRef(this.oid, itemRef, to),
|
|
663
820
|
);
|
|
664
821
|
} else {
|
|
665
|
-
const index = this.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
822
|
+
const index = this.view.indexOf(item);
|
|
823
|
+
if (index === -1) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
`Cannot move item ${JSON.stringify(
|
|
826
|
+
item,
|
|
827
|
+
)} which does not exist in this list`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
this.move(index, to);
|
|
669
831
|
}
|
|
670
832
|
};
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
833
|
+
|
|
834
|
+
add = (value: ListItemValue<KeyValue>): void => {
|
|
835
|
+
this.addPendingOperations(
|
|
836
|
+
this.patchCreator.createListAdd(
|
|
674
837
|
this.oid,
|
|
675
|
-
this.
|
|
838
|
+
this.processInputValue(value, this.view.length),
|
|
676
839
|
),
|
|
677
840
|
);
|
|
678
841
|
};
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
842
|
+
|
|
843
|
+
removeAll = (item: ListItemValue<KeyValue>): void => {
|
|
844
|
+
this.addPendingOperations(
|
|
845
|
+
this.patchCreator.createListRemove(this.oid, this.getItemRefValue(item)),
|
|
846
|
+
);
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
removeFirst = (item: ListItemValue<KeyValue>): void => {
|
|
850
|
+
this.addPendingOperations(
|
|
851
|
+
this.patchCreator.createListRemove(
|
|
682
852
|
this.oid,
|
|
683
853
|
this.getItemRefValue(item),
|
|
684
854
|
'first',
|
|
685
855
|
),
|
|
686
856
|
);
|
|
687
857
|
};
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
858
|
+
|
|
859
|
+
removeLast = (item: ListItemValue<KeyValue>): void => {
|
|
860
|
+
this.addPendingOperations(
|
|
861
|
+
this.patchCreator.createListRemove(
|
|
691
862
|
this.oid,
|
|
692
863
|
this.getItemRefValue(item),
|
|
693
864
|
'last',
|
|
694
865
|
),
|
|
695
866
|
);
|
|
696
867
|
};
|
|
697
|
-
add = (item: ListItemValue<KeyValue>) => {
|
|
698
|
-
this.addPatches(
|
|
699
|
-
this.store.patchCreator.createListAdd(
|
|
700
|
-
this.oid,
|
|
701
|
-
this.processInputValue(item, this.value.length),
|
|
702
|
-
),
|
|
703
|
-
);
|
|
704
|
-
};
|
|
705
|
-
has = (item: ListItemValue<KeyValue>) => {
|
|
706
|
-
if (typeof item === 'object') {
|
|
707
|
-
return this.value.some((val: unknown) => {
|
|
708
|
-
if (isObjectRef(val)) return val.id === maybeGetOid(item);
|
|
709
|
-
// Sets of files don't work right now, there's no way to compare them
|
|
710
|
-
// effectively.
|
|
711
|
-
if (isFileRef(val)) return false;
|
|
712
|
-
return false;
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
return this.value.includes(item);
|
|
716
|
-
};
|
|
717
868
|
|
|
718
869
|
// list implements an iterator which maps items to wrapped
|
|
719
870
|
// versions
|
|
720
871
|
[Symbol.iterator]() {
|
|
721
872
|
let index = 0;
|
|
873
|
+
let length = this.view?.length;
|
|
722
874
|
return {
|
|
723
875
|
next: () => {
|
|
724
|
-
if (index <
|
|
876
|
+
if (index < length) {
|
|
725
877
|
return {
|
|
726
878
|
value: this.get(index++) as ListItemValue<KeyValue>,
|
|
727
879
|
done: false,
|
|
@@ -735,140 +887,68 @@ export class Entity<
|
|
|
735
887
|
};
|
|
736
888
|
}
|
|
737
889
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
return this.value.map(this.wrapValue);
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
map = <U>(callback: (value: ListItemValue<KeyValue>, index: number) => U) => {
|
|
746
|
-
return this.getAsWrapped().map(callback);
|
|
890
|
+
map = <U>(
|
|
891
|
+
callback: (value: ListItemValue<KeyValue>, index: number) => U,
|
|
892
|
+
): U[] => {
|
|
893
|
+
return this.view.map(callback);
|
|
747
894
|
};
|
|
748
895
|
|
|
749
896
|
filter = (
|
|
750
897
|
callback: (value: ListItemValue<KeyValue>, index: number) => boolean,
|
|
751
|
-
) => {
|
|
752
|
-
return this.
|
|
753
|
-
|
|
754
|
-
|
|
898
|
+
): ListItemValue<KeyValue>[] => {
|
|
899
|
+
return this.view.filter(callback);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
has = (value: ListItemValue<KeyValue>): boolean => {
|
|
903
|
+
if (!this.isList) {
|
|
904
|
+
throw new Error('has() is only available on list entities');
|
|
905
|
+
}
|
|
906
|
+
const itemRef = this.getItemRefValue(value);
|
|
907
|
+
if (isRef(itemRef)) {
|
|
908
|
+
return this.view.some((item: any) => {
|
|
909
|
+
if (isRef(item)) {
|
|
910
|
+
return compareRefs(item, itemRef);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
} else {
|
|
914
|
+
return this.view.includes(value);
|
|
915
|
+
}
|
|
755
916
|
};
|
|
756
917
|
|
|
757
918
|
forEach = (
|
|
758
919
|
callback: (value: ListItemValue<KeyValue>, index: number) => void,
|
|
759
|
-
) => {
|
|
760
|
-
this.
|
|
920
|
+
): void => {
|
|
921
|
+
this.view.forEach(callback);
|
|
761
922
|
};
|
|
762
923
|
|
|
763
|
-
some = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
|
|
764
|
-
return this.
|
|
924
|
+
some = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
|
|
925
|
+
return this.view.some(predicate);
|
|
765
926
|
};
|
|
766
927
|
|
|
767
|
-
every = (predicate: (value: ListItemValue<KeyValue>) => boolean) => {
|
|
768
|
-
return this.
|
|
928
|
+
every = (predicate: (value: ListItemValue<KeyValue>) => boolean): boolean => {
|
|
929
|
+
return this.view.every(predicate);
|
|
769
930
|
};
|
|
770
931
|
|
|
771
|
-
find = (
|
|
772
|
-
|
|
932
|
+
find = (
|
|
933
|
+
predicate: (value: ListItemValue<KeyValue>) => boolean,
|
|
934
|
+
): ListItemValue<KeyValue> | undefined => {
|
|
935
|
+
return this.view.find(predicate);
|
|
773
936
|
};
|
|
774
937
|
|
|
775
|
-
includes =
|
|
776
|
-
return this.has(item);
|
|
777
|
-
};
|
|
778
|
-
}
|
|
938
|
+
includes = this.has;
|
|
779
939
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
dispose: () => void;
|
|
786
|
-
subscribe<EventName extends keyof EntityEvents>(
|
|
787
|
-
event: EventName,
|
|
788
|
-
callback: EntityEvents[EventName],
|
|
789
|
-
): () => void;
|
|
790
|
-
get<Key extends keyof Value>(key: Key): Value[Key];
|
|
791
|
-
getAll(): Value;
|
|
792
|
-
getSnapshot(): Snapshot;
|
|
793
|
-
readonly deleted: boolean;
|
|
794
|
-
readonly hasSubscribers: boolean;
|
|
940
|
+
// TODO: make these escape hatches unnecessary
|
|
941
|
+
__getViewData__ = (oid: ObjectIdentifier, type: 'confirmed' | 'pending') => {
|
|
942
|
+
return this.metadataFamily.get(oid).computeView(type === 'confirmed');
|
|
943
|
+
};
|
|
944
|
+
__getFamilyOids__ = () => this.metadataFamily.getAllOids();
|
|
795
945
|
}
|
|
796
946
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
? Array<DeepPartial<U>>
|
|
800
|
-
: T[P] extends ReadonlyArray<infer U>
|
|
801
|
-
? ReadonlyArray<DeepPartial<U>>
|
|
802
|
-
: DeepPartial<T[P]>;
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
export interface ObjectEntity<
|
|
806
|
-
Init,
|
|
807
|
-
Value extends BaseEntityValue,
|
|
808
|
-
Snapshot = DataFromInit<Init>,
|
|
809
|
-
> extends BaseEntity<Init, Value, Snapshot> {
|
|
810
|
-
keys(): string[];
|
|
811
|
-
entries(): [string, Exclude<Value[keyof Value], undefined>][];
|
|
812
|
-
values(): Exclude<Value[keyof Value], undefined>[];
|
|
813
|
-
set<Key extends keyof Init>(key: Key, value: Init[Key]): void;
|
|
814
|
-
delete(key: DeletableKeys<Value>): void;
|
|
815
|
-
update(
|
|
816
|
-
value: DeepPartial<Init>,
|
|
817
|
-
options?: { replaceSubObjects?: boolean; merge?: boolean },
|
|
818
|
-
): void;
|
|
819
|
-
readonly isList: false;
|
|
947
|
+
function assertNotSymbol<T>(key: T): asserts key is Exclude<T, symbol> {
|
|
948
|
+
if (typeof key === 'symbol') throw new Error("Symbol keys aren't supported");
|
|
820
949
|
}
|
|
821
950
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
Snapshot = DataFromInit<Init>,
|
|
826
|
-
> extends Iterable<ListItemValue<Value>>,
|
|
827
|
-
BaseEntity<Init, Value, Snapshot> {
|
|
828
|
-
readonly isList: true;
|
|
829
|
-
readonly length: number;
|
|
830
|
-
push(value: ListItemInit<Init>): void;
|
|
831
|
-
insert(index: number, value: ListItemInit<Init>): void;
|
|
832
|
-
move(from: number, to: number): void;
|
|
833
|
-
moveItem(item: ListItemValue<Value>, to: number): void;
|
|
834
|
-
/**
|
|
835
|
-
* A Set operation which adds a value if an equivalent value is not already present.
|
|
836
|
-
* Object values are never the same.
|
|
837
|
-
*/
|
|
838
|
-
add(value: ListItemValue<Value>): void;
|
|
839
|
-
removeAll(item: ListItemValue<Value>): void;
|
|
840
|
-
removeFirst(item: ListItemValue<Value>): void;
|
|
841
|
-
removeLast(item: ListItemValue<Value>): void;
|
|
842
|
-
map<U>(callback: (value: ListItemValue<Value>, index: number) => U): U[];
|
|
843
|
-
filter(
|
|
844
|
-
callback: (value: ListItemValue<Value>, index: number) => boolean,
|
|
845
|
-
): ListItemValue<Value>[];
|
|
846
|
-
delete(index: number): void;
|
|
847
|
-
has(value: ListItemValue<Value>): boolean;
|
|
848
|
-
forEach(callback: (value: ListItemValue<Value>, index: number) => void): void;
|
|
849
|
-
some(predicate: (value: ListItemValue<Value>) => boolean): boolean;
|
|
850
|
-
every(predicate: (value: ListItemValue<Value>) => boolean): boolean;
|
|
851
|
-
find(
|
|
852
|
-
predicate: (value: ListItemValue<Value>) => boolean,
|
|
853
|
-
): ListItemValue<Value> | undefined;
|
|
854
|
-
includes(value: ListItemValue<Value>): boolean;
|
|
951
|
+
function assertNumber(key: unknown): asserts key is number {
|
|
952
|
+
if (typeof key !== 'number')
|
|
953
|
+
throw new Error('Only number keys are supported in list entities');
|
|
855
954
|
}
|
|
856
|
-
|
|
857
|
-
export type AnyEntity<
|
|
858
|
-
Init,
|
|
859
|
-
KeyValue extends BaseEntityValue,
|
|
860
|
-
Snapshot extends any,
|
|
861
|
-
> =
|
|
862
|
-
| ListEntity<Init, KeyValue, Snapshot>
|
|
863
|
-
| ObjectEntity<Init, KeyValue, Snapshot>;
|
|
864
|
-
|
|
865
|
-
type ListItemValue<KeyValue> = KeyValue extends Array<infer T> ? T : never;
|
|
866
|
-
type ListItemInit<Init> = Init extends Array<infer T> ? T : never;
|
|
867
|
-
|
|
868
|
-
export type EntityDestructured<T extends AnyEntity<any, any, any> | null> =
|
|
869
|
-
| (T extends ListEntity<any, infer KeyValue, any>
|
|
870
|
-
? KeyValue
|
|
871
|
-
: T extends ObjectEntity<any, infer KeyValue, any>
|
|
872
|
-
? KeyValue
|
|
873
|
-
: never)
|
|
874
|
-
| (T extends null ? null : never);
|