@verdant-web/store 4.1.6 → 4.3.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 -7
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/UndoHistory.d.ts +2 -0
- package/dist/esm/UndoHistory.js +6 -0
- package/dist/esm/UndoHistory.js.map +1 -1
- package/dist/esm/__tests__/entities.test.js +143 -5
- package/dist/esm/__tests__/entities.test.js.map +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +1 -0
- package/dist/esm/__tests__/fixtures/testStorage.js +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/client/Client.js +1 -0
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/client/ClientDescriptor.js +4 -1
- package/dist/esm/client/ClientDescriptor.js.map +1 -1
- package/dist/esm/context/context.d.ts +6 -0
- package/dist/esm/entities/Entity.d.ts +22 -2
- package/dist/esm/entities/Entity.js +127 -5
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/EntityCache.d.ts +1 -1
- package/dist/esm/entities/EntityMetadata.d.ts +2 -2
- package/dist/esm/entities/EntityMetadata.js.map +1 -1
- package/dist/esm/entities/EntityStore.d.ts +4 -4
- package/dist/esm/entities/EntityStore.js +4 -9
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/OperationBatcher.d.ts +1 -1
- package/dist/esm/entities/OperationBatcher.js +2 -0
- package/dist/esm/entities/OperationBatcher.js.map +1 -1
- package/dist/esm/index.d.ts +16 -19
- package/dist/esm/index.js +10 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/persistence/migration/engine.d.ts +1 -1
- package/dist/esm/persistence/migration/engine.js +7 -0
- package/dist/esm/persistence/migration/engine.js.map +1 -1
- package/package.json +2 -2
- package/src/UndoHistory.ts +7 -0
- package/src/__tests__/entities.test.ts +160 -5
- package/src/__tests__/fixtures/testStorage.ts +1 -1
- package/src/client/Client.ts +1 -0
- package/src/client/ClientDescriptor.ts +8 -0
- package/src/context/context.ts +7 -0
- package/src/entities/Entity.ts +151 -5
- package/src/entities/EntityMetadata.ts +5 -4
- package/src/entities/EntityStore.ts +10 -11
- package/src/entities/OperationBatcher.ts +16 -2
- package/src/index.ts +39 -39
- package/src/persistence/migration/engine.ts +9 -2
package/dist/esm/index.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { ClientDescriptor, } from './client/ClientDescriptor.js';
|
|
2
1
|
import { Client } from './client/Client.js';
|
|
3
|
-
|
|
4
|
-
export { Client };
|
|
2
|
+
import { ClientDescriptor, } from './client/ClientDescriptor.js';
|
|
3
|
+
export { Client, ClientDescriptor };
|
|
5
4
|
// backward compat
|
|
6
|
-
export {
|
|
7
|
-
export { Client as Storage };
|
|
8
|
-
export { Entity } from './entities/Entity.js';
|
|
9
|
-
export { ServerSync } from './sync/Sync.js';
|
|
10
|
-
export { EntityFile } from './files/EntityFile.js';
|
|
11
|
-
export { schema, createMigration } from '@verdant-web/common';
|
|
12
|
-
export * from './utils/id.js';
|
|
13
|
-
export { UndoHistory } from './UndoHistory.js';
|
|
5
|
+
export { createMigration, schema } from '@verdant-web/common';
|
|
14
6
|
export * from './authorization.js';
|
|
15
|
-
export
|
|
7
|
+
export { Entity, getEntityClient } from './entities/Entity.js';
|
|
8
|
+
export { EntityFile } from './files/EntityFile.js';
|
|
16
9
|
export { IdbPersistence } from './persistence/idb/idbPersistence.js';
|
|
10
|
+
export * from './sync/cliSync.js';
|
|
11
|
+
export { ServerSync } from './sync/Sync.js';
|
|
12
|
+
export { UndoHistory } from './UndoHistory.js';
|
|
13
|
+
export * from './utils/id.js';
|
|
14
|
+
export { Client as Storage, ClientDescriptor as StorageDescriptor };
|
|
17
15
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EACN,gBAAgB,GAEhB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;AACpC,kBAAkB;AAClB,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAyB9D,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAU/D,OAAO,EAAE,UAAU,EAA2B,MAAM,uBAAuB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAKrE,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,gBAAgB,IAAI,iBAAiB,EAAE,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Migration, MigrationEngine } from '@verdant-web/common';
|
|
2
|
-
import { OpenDocumentDbContext } from './types.js';
|
|
3
2
|
import { PersistenceNamespace } from '../interfaces.js';
|
|
3
|
+
import { OpenDocumentDbContext } from './types.js';
|
|
4
4
|
export declare function getMigrationEngine({ migration, context, ns, }: {
|
|
5
5
|
log?: (...args: any[]) => void;
|
|
6
6
|
migration: Migration;
|
|
@@ -108,7 +108,14 @@ export async function getMigrationEngine({ migration, context, ns, }) {
|
|
|
108
108
|
removeOidPropertiesFromAllSubObjects(newValue);
|
|
109
109
|
assignOidsToAllSubObjects(newValue);
|
|
110
110
|
const patches = diffToPatches(original, newValue, () => context.time.zeroWithVersion(migration.version), undefined, [], {
|
|
111
|
+
// incoming unknown objects are assumed to be the same
|
|
112
|
+
// as any pre-existing object.
|
|
111
113
|
mergeUnknownObjects: true,
|
|
114
|
+
// if a field is undefined in the new value, it should be
|
|
115
|
+
// erased. this is the only way to allow users to remove
|
|
116
|
+
// entries in maps during migrations. it is a little
|
|
117
|
+
// dangerous for other types, though.
|
|
118
|
+
defaultUndefined: false,
|
|
112
119
|
authz,
|
|
113
120
|
});
|
|
114
121
|
if (patches.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../../src/persistence/migration/engine.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../../src/persistence/migration/engine.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verdant-web/store",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"access": "public",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"jszip": "^3.10.1",
|
|
35
35
|
"jwt-decode": "^3.1.2",
|
|
36
36
|
"weak-event": "^2.0.5",
|
|
37
|
-
"@verdant-web/common": "2.
|
|
37
|
+
"@verdant-web/common": "2.9.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "20.10.5",
|
package/src/UndoHistory.ts
CHANGED
|
@@ -13,6 +13,13 @@ export class UndoHistory extends EventSubscriber<{ change: () => void }> {
|
|
|
13
13
|
return this._undone.length > 0;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
get undoLength() {
|
|
17
|
+
return this._undoable.length;
|
|
18
|
+
}
|
|
19
|
+
get redoLength() {
|
|
20
|
+
return this._undone.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
undo = async () => {
|
|
17
24
|
const next = this._undoable.pop();
|
|
18
25
|
if (next) {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
assert,
|
|
3
|
+
createRef,
|
|
4
|
+
EventSubscriber,
|
|
5
|
+
NaiveTimestampProvider,
|
|
6
|
+
PatchCreator,
|
|
7
|
+
schema,
|
|
8
|
+
} from '@verdant-web/common';
|
|
9
|
+
import { describe, expect, it, MockedFunction, vi, vitest } from 'vitest';
|
|
10
|
+
import { WeakEvent } from 'weak-event';
|
|
11
|
+
import { Time } from '../context/Time.js';
|
|
12
|
+
import { EntityFamilyMetadata } from '../entities/EntityMetadata.js';
|
|
13
|
+
import { Entity, EntityFile } from '../index.js';
|
|
3
14
|
import { createTestStorage } from './fixtures/testStorage.js';
|
|
4
|
-
import { EntityFile } from '../index.js';
|
|
5
15
|
|
|
6
16
|
async function waitForStoragePropagation(mock: MockedFunction<any>) {
|
|
7
17
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -639,7 +649,7 @@ describe('entities', () => {
|
|
|
639
649
|
content: { invalid: 'value' },
|
|
640
650
|
});
|
|
641
651
|
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
642
|
-
`[Error: Validation error: Expected string for field content, got
|
|
652
|
+
`[Error: Validation error: Expected string for field content, got {"invalid":"value"}]`,
|
|
643
653
|
);
|
|
644
654
|
});
|
|
645
655
|
|
|
@@ -650,7 +660,7 @@ describe('entities', () => {
|
|
|
650
660
|
expect(() => {
|
|
651
661
|
weird.set('file', { invalid: 'value' });
|
|
652
662
|
}).toThrowErrorMatchingInlineSnapshot(
|
|
653
|
-
`[Error: Validation error: Expected file or null for field file, got
|
|
663
|
+
`[Error: Validation error: Expected file or null for field file, got {"invalid":"value"}]`,
|
|
654
664
|
);
|
|
655
665
|
|
|
656
666
|
// valid options
|
|
@@ -705,4 +715,149 @@ describe('entities', () => {
|
|
|
705
715
|
|
|
706
716
|
expect(item.deleted).toBe(true);
|
|
707
717
|
});
|
|
718
|
+
|
|
719
|
+
it('should apply contextual changes to a pruned entity in a way consistent with the pruned view of the data', async () => {
|
|
720
|
+
// manually constructing an entity for this test is easiest,
|
|
721
|
+
// kind of hard to force invalid data otherwise
|
|
722
|
+
const onPendingOperations = vi.fn();
|
|
723
|
+
const time = new Time(new NaiveTimestampProvider(), 1);
|
|
724
|
+
// too much junk in here, have to manually pick and choose
|
|
725
|
+
let subId = 0;
|
|
726
|
+
const testCtx = {
|
|
727
|
+
globalEvents: new EventSubscriber(),
|
|
728
|
+
time,
|
|
729
|
+
log: vi.fn(),
|
|
730
|
+
patchCreator: new PatchCreator(
|
|
731
|
+
() => time.now,
|
|
732
|
+
() => `${subId++}`,
|
|
733
|
+
),
|
|
734
|
+
files: {
|
|
735
|
+
add: vi.fn(),
|
|
736
|
+
},
|
|
737
|
+
} as any;
|
|
738
|
+
const metadataFamily = new EntityFamilyMetadata({
|
|
739
|
+
ctx: testCtx,
|
|
740
|
+
onPendingOperations,
|
|
741
|
+
rootOid: 'foos/a',
|
|
742
|
+
});
|
|
743
|
+
const entity = new Entity({
|
|
744
|
+
oid: 'foos/a',
|
|
745
|
+
schema: {
|
|
746
|
+
type: 'object',
|
|
747
|
+
properties: {
|
|
748
|
+
id: schema.fields.id(),
|
|
749
|
+
content: schema.fields.string(),
|
|
750
|
+
items: schema.fields.array({
|
|
751
|
+
items: schema.fields.object({
|
|
752
|
+
properties: {
|
|
753
|
+
content: schema.fields.string(),
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
}),
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
ctx: testCtx,
|
|
760
|
+
deleteSelf: vi.fn(),
|
|
761
|
+
files: {
|
|
762
|
+
add: vi.fn(),
|
|
763
|
+
} as any,
|
|
764
|
+
metadataFamily,
|
|
765
|
+
storeEvents: {
|
|
766
|
+
add: new WeakEvent(),
|
|
767
|
+
replace: new WeakEvent(),
|
|
768
|
+
resetAll: new WeakEvent(),
|
|
769
|
+
},
|
|
770
|
+
readonlyKeys: ['id'],
|
|
771
|
+
});
|
|
772
|
+
metadataFamily.addConfirmedData({
|
|
773
|
+
baselines: [
|
|
774
|
+
{
|
|
775
|
+
oid: 'foos/a:1',
|
|
776
|
+
snapshot: { content: 'item 1' },
|
|
777
|
+
timestamp: time.now,
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
oid: 'foos/a:2',
|
|
781
|
+
snapshot: { content: 'item 2' },
|
|
782
|
+
timestamp: time.now,
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
oid: 'foos/a:3',
|
|
786
|
+
snapshot: {}, // INVALID!
|
|
787
|
+
timestamp: time.now,
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
oid: 'foos/a:4',
|
|
791
|
+
snapshot: { content: 'item 4' },
|
|
792
|
+
timestamp: time.now,
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
oid: 'foos/a:5',
|
|
796
|
+
snapshot: [
|
|
797
|
+
createRef('foos/a:1'),
|
|
798
|
+
createRef('foos/a:2'),
|
|
799
|
+
createRef('foos/a:3'),
|
|
800
|
+
createRef('foos/a:4'),
|
|
801
|
+
],
|
|
802
|
+
timestamp: time.now,
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
oid: 'foos/a',
|
|
806
|
+
snapshot: {
|
|
807
|
+
id: 'a',
|
|
808
|
+
content: 'the main foo',
|
|
809
|
+
items: createRef('foos/a:5'),
|
|
810
|
+
},
|
|
811
|
+
timestamp: time.now,
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
expect(entity.deepInvalid).toBe(true);
|
|
817
|
+
|
|
818
|
+
// check all that worked, lol. and that it's
|
|
819
|
+
// pruned item 3
|
|
820
|
+
expect(entity.getSnapshot()).toEqual({
|
|
821
|
+
id: 'a',
|
|
822
|
+
content: 'the main foo',
|
|
823
|
+
items: [
|
|
824
|
+
{ content: 'item 1' },
|
|
825
|
+
{ content: 'item 2' },
|
|
826
|
+
{ content: 'item 4' },
|
|
827
|
+
],
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// also check that unpruned snapshot is correct
|
|
831
|
+
expect(entity.getUnprunedSnapshot()).toEqual({
|
|
832
|
+
id: 'a',
|
|
833
|
+
content: 'the main foo',
|
|
834
|
+
items: [
|
|
835
|
+
{ content: 'item 1' },
|
|
836
|
+
{ content: 'item 2' },
|
|
837
|
+
{},
|
|
838
|
+
{ content: 'item 4' },
|
|
839
|
+
],
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
// now, let's set the content of index 2.
|
|
843
|
+
// if this works as expected, we should replace
|
|
844
|
+
// item 4 (even though technically it's at index 3)
|
|
845
|
+
// because we are respecting the user's intention.
|
|
846
|
+
const items = entity.get('items');
|
|
847
|
+
items.set(2, { content: 'new item' });
|
|
848
|
+
|
|
849
|
+
expect(entity.getSnapshot()).toEqual({
|
|
850
|
+
id: 'a',
|
|
851
|
+
content: 'the main foo',
|
|
852
|
+
items: [
|
|
853
|
+
{ content: 'item 1' },
|
|
854
|
+
{ content: 'item 2' },
|
|
855
|
+
{ content: 'new item' },
|
|
856
|
+
],
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// now, the entity has been 'fixed' and is valid again
|
|
860
|
+
expect(entity.getSnapshot()).toEqual(entity.getUnprunedSnapshot());
|
|
861
|
+
expect(entity.deepInvalid).toBe(false);
|
|
862
|
+
});
|
|
708
863
|
});
|
package/src/client/Client.ts
CHANGED
|
@@ -85,6 +85,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
85
85
|
|
|
86
86
|
constructor(private context: Context) {
|
|
87
87
|
super();
|
|
88
|
+
context.getClient = () => this;
|
|
88
89
|
this.collectionNames = Object.keys(context.schema.collections);
|
|
89
90
|
this._sync =
|
|
90
91
|
this.context.config.sync && !context.schema.wip
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Migration,
|
|
5
5
|
PatchCreator,
|
|
6
6
|
StorageSchema,
|
|
7
|
+
VerdantError,
|
|
7
8
|
noop,
|
|
8
9
|
} from '@verdant-web/common';
|
|
9
10
|
import { FileConfig, InitialContext, QueryConfig } from '../context/context.js';
|
|
@@ -167,6 +168,13 @@ export class ClientDescriptor<
|
|
|
167
168
|
environment,
|
|
168
169
|
persistenceShutdownHandler: new ShutdownHandler(init.log),
|
|
169
170
|
pauseRebasing: false,
|
|
171
|
+
getClient() {
|
|
172
|
+
throw new VerdantError(
|
|
173
|
+
VerdantError.Code.Unexpected,
|
|
174
|
+
undefined,
|
|
175
|
+
'Client not yet initialized. This is a Verdant bug, please report it.',
|
|
176
|
+
);
|
|
177
|
+
},
|
|
170
178
|
};
|
|
171
179
|
ctx.log('info', 'Initializing client', {
|
|
172
180
|
namespace: ctx.namespace,
|
package/src/context/context.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
StorageSchema,
|
|
11
11
|
} from '@verdant-web/common';
|
|
12
12
|
import { UndoHistory } from '../UndoHistory.js';
|
|
13
|
+
import type { Client } from '../client/Client.js';
|
|
13
14
|
import { PersistenceFiles } from '../persistence/PersistenceFiles.js';
|
|
14
15
|
import type { PersistenceMetadata } from '../persistence/PersistenceMetadata.js';
|
|
15
16
|
import type { PersistenceDocuments } from '../persistence/PersistenceQueries.js';
|
|
@@ -110,6 +111,12 @@ export interface Context {
|
|
|
110
111
|
};
|
|
111
112
|
|
|
112
113
|
persistence: PersistenceImplementation;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Must be defined by the Client once it exists. Attempts to use this before
|
|
117
|
+
* it's ready will rightfully throw an error.
|
|
118
|
+
*/
|
|
119
|
+
getClient: () => Client;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
export interface FileConfig {
|
package/src/entities/Entity.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
isFileRef,
|
|
18
18
|
isNullable,
|
|
19
19
|
isObject,
|
|
20
|
+
isObjectRef,
|
|
20
21
|
isRef,
|
|
21
22
|
maybeGetOid,
|
|
22
23
|
memoByKeys,
|
|
@@ -26,12 +27,13 @@ import {
|
|
|
26
27
|
import { Context } from '../context/context.js';
|
|
27
28
|
import { FileManager } from '../files/FileManager.js';
|
|
28
29
|
import { processValueFiles } from '../files/utils.js';
|
|
29
|
-
import { EntityFile } from '../index.js';
|
|
30
|
+
import { ClientWithCollections, EntityFile } from '../index.js';
|
|
30
31
|
import { EntityCache } from './EntityCache.js';
|
|
31
32
|
import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
|
|
32
33
|
import { EntityStoreEventData, EntityStoreEvents } from './EntityStore.js';
|
|
33
34
|
import { entityFieldSubscriber } from './entityFieldSubscriber.js';
|
|
34
35
|
import {
|
|
36
|
+
AnyEntity,
|
|
35
37
|
BaseEntityValue,
|
|
36
38
|
DataFromInit,
|
|
37
39
|
EntityChange,
|
|
@@ -56,6 +58,8 @@ export interface EntityInit {
|
|
|
56
58
|
deleteSelf: () => void;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
const PRIVATE_ENTITY_CONTEXT_KEY = Symbol('private entity context key');
|
|
62
|
+
|
|
59
63
|
export class Entity<
|
|
60
64
|
Init = any,
|
|
61
65
|
KeyValue extends BaseEntityValue = any,
|
|
@@ -79,6 +83,10 @@ export class Entity<
|
|
|
79
83
|
private files;
|
|
80
84
|
private storeEvents;
|
|
81
85
|
|
|
86
|
+
get [PRIVATE_ENTITY_CONTEXT_KEY]() {
|
|
87
|
+
return this.ctx;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
// an internal representation of this Entity.
|
|
83
91
|
// if present, this is the cached, known value. If null,
|
|
84
92
|
// the entity is deleted. If undefined, we need to recompute
|
|
@@ -290,7 +298,12 @@ export class Entity<
|
|
|
290
298
|
break;
|
|
291
299
|
}
|
|
292
300
|
} else {
|
|
293
|
-
|
|
301
|
+
// special case - rewrite undefined fields to null
|
|
302
|
+
if (isNullable(schema) && child === undefined) {
|
|
303
|
+
this.cachedView[key] = null;
|
|
304
|
+
} else {
|
|
305
|
+
this.cachedView[key] = child;
|
|
306
|
+
}
|
|
294
307
|
}
|
|
295
308
|
}
|
|
296
309
|
}
|
|
@@ -318,6 +331,33 @@ export class Entity<
|
|
|
318
331
|
return !!this.validate();
|
|
319
332
|
}
|
|
320
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Returns true if this or any child is invalid (pruned)
|
|
336
|
+
*/
|
|
337
|
+
get deepInvalid(): boolean {
|
|
338
|
+
if (this.invalid) return true;
|
|
339
|
+
if (Array.isArray(this.rawView)) {
|
|
340
|
+
for (let i = 0; i < this.rawView.length; i++) {
|
|
341
|
+
if (isObjectRef(this.rawView[i])) {
|
|
342
|
+
const child = this.getChild(i, this.rawView[i].id);
|
|
343
|
+
if (child.deepInvalid) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (isObject(this.rawView)) {
|
|
349
|
+
for (const key in this.rawView) {
|
|
350
|
+
if (isObjectRef(this.rawView[key])) {
|
|
351
|
+
const child = this.getChild(key, this.rawView[key].id);
|
|
352
|
+
if (child.deepInvalid) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
321
361
|
get isList() {
|
|
322
362
|
// have to turn TS off here as our two interfaces both implement
|
|
323
363
|
// const values for this boolean.
|
|
@@ -411,7 +451,7 @@ export class Entity<
|
|
|
411
451
|
field: this.schema,
|
|
412
452
|
value: this.rawView,
|
|
413
453
|
fieldPath: this.fieldPath,
|
|
414
|
-
|
|
454
|
+
expectRefs: true,
|
|
415
455
|
}) ?? undefined;
|
|
416
456
|
return this.validationError;
|
|
417
457
|
},
|
|
@@ -449,6 +489,37 @@ export class Entity<
|
|
|
449
489
|
}
|
|
450
490
|
};
|
|
451
491
|
|
|
492
|
+
private rawViewWithMappedChildren = (
|
|
493
|
+
mapper: (child: Entity | EntityFile) => any,
|
|
494
|
+
) => {
|
|
495
|
+
const view = this.rawView;
|
|
496
|
+
if (!view) {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(view)) {
|
|
500
|
+
const mapped = view.map((value, i) => {
|
|
501
|
+
if (isRef(value)) {
|
|
502
|
+
return mapper(this.getChild(i, value.id));
|
|
503
|
+
} else {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
assignOid(mapped, this.oid);
|
|
508
|
+
return mapped;
|
|
509
|
+
} else {
|
|
510
|
+
const mapped = Object.entries(view).reduce((acc, [key, value]) => {
|
|
511
|
+
if (isRef(value)) {
|
|
512
|
+
acc[key as any] = mapper(this.getChild(key, value.id));
|
|
513
|
+
} else {
|
|
514
|
+
acc[key as any] = value;
|
|
515
|
+
}
|
|
516
|
+
return acc;
|
|
517
|
+
}, {} as any);
|
|
518
|
+
assignOid(mapped, this.oid);
|
|
519
|
+
return mapped;
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
452
523
|
/**
|
|
453
524
|
* A current snapshot of this Entity's data, including nested
|
|
454
525
|
* Entities.
|
|
@@ -457,10 +528,62 @@ export class Entity<
|
|
|
457
528
|
return this.viewWithMappedChildren((child) => child.getSnapshot());
|
|
458
529
|
};
|
|
459
530
|
|
|
531
|
+
/**
|
|
532
|
+
* A snapshot of this Entity with unpruned (invalid) data. This will
|
|
533
|
+
* not conform to the entity schema and should be used carefully.
|
|
534
|
+
*
|
|
535
|
+
* Can be used to inspect or recover invalid, pruned data not
|
|
536
|
+
* otherwise accessible.
|
|
537
|
+
*/
|
|
538
|
+
getUnprunedSnapshot = (): any => {
|
|
539
|
+
return this.rawViewWithMappedChildren((child) => {
|
|
540
|
+
if (child instanceof EntityFile) return child.getSnapshot();
|
|
541
|
+
return child.getUnprunedSnapshot();
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
|
|
460
545
|
// change management methods (internal use only)
|
|
461
546
|
private addPendingOperations = (operations: Operation[]) => {
|
|
462
|
-
this.ctx.log(
|
|
547
|
+
this.ctx.log(
|
|
548
|
+
'debug',
|
|
549
|
+
'Entity: adding pending operations',
|
|
550
|
+
this.oid,
|
|
551
|
+
operations,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// special case -- if this entity is pruned, any changes we apply to it
|
|
555
|
+
// will be in relation to 'exposed' pruned data, not the 'real world'
|
|
556
|
+
// data that's backing it. That means those changes will produce unexpected
|
|
557
|
+
// or further invalid results. To avoid this, we basically stamp in the
|
|
558
|
+
// pruned version of this entity before proceeding.
|
|
559
|
+
//
|
|
560
|
+
// as an example of a failure mode without this check, consider a list:
|
|
561
|
+
// [1, 2, <pruned>, 4, 5]
|
|
562
|
+
// the user sees: [1, 2, 4, 5]
|
|
563
|
+
// when they try to replace the item at index 2 with "0" (they see "4"), they
|
|
564
|
+
// actually replace the invisible pruned item, resulting in [1, 2, 0, 4, 5]
|
|
565
|
+
// being the result when they expected [1, 2, 0, 5].
|
|
566
|
+
//
|
|
567
|
+
// To "stamp" the data before applying user changes, we diff the snapshot
|
|
568
|
+
// (which is the pruned version) with the current state of the entity.
|
|
569
|
+
if (this.deepInvalid) {
|
|
570
|
+
this.ctx.log(
|
|
571
|
+
'warn',
|
|
572
|
+
'Changes are being applied to a pruned entity. This means that the pruned version is being treated as the new baseline and any pruned invalid data is lost.',
|
|
573
|
+
this.oid,
|
|
574
|
+
);
|
|
575
|
+
this.canonizePrunedVersion();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.applyPendingOperations(operations);
|
|
579
|
+
};
|
|
463
580
|
|
|
581
|
+
// naming is fuzzy here, but this method was split out from
|
|
582
|
+
// addPendingOperations since that method also conditionally canonizes
|
|
583
|
+
// the pruned snapshot, and I wanted to keep the actual insertion of
|
|
584
|
+
// the ops in one place, so leaving it as part of addPendingOperations
|
|
585
|
+
// would introduce infinite recursion when canonizing.
|
|
586
|
+
private applyPendingOperations = (operations: Operation[]) => {
|
|
464
587
|
// apply authz to all operations
|
|
465
588
|
if (this.access) {
|
|
466
589
|
for (const op of operations) {
|
|
@@ -474,6 +597,21 @@ export class Entity<
|
|
|
474
597
|
}
|
|
475
598
|
};
|
|
476
599
|
|
|
600
|
+
private canonizePrunedVersion = () => {
|
|
601
|
+
const snapshot = this.getSnapshot();
|
|
602
|
+
const unprunedSnapshot = this.getUnprunedSnapshot();
|
|
603
|
+
const operations = this.patchCreator.createDiff(
|
|
604
|
+
unprunedSnapshot,
|
|
605
|
+
snapshot,
|
|
606
|
+
{
|
|
607
|
+
authz: this.access,
|
|
608
|
+
merge: false,
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
this.applyPendingOperations(operations);
|
|
613
|
+
};
|
|
614
|
+
|
|
477
615
|
private addConfirmedData = (data: EntityStoreEventData) => {
|
|
478
616
|
this.ctx.log('debug', 'Entity: adding confirmed data', this.oid);
|
|
479
617
|
const changes = this.metadataFamily.addConfirmedData(data);
|
|
@@ -920,7 +1058,7 @@ export class Entity<
|
|
|
920
1058
|
this.addPendingOperations(
|
|
921
1059
|
this.patchCreator.createDiff(this.getSnapshot(), changes, {
|
|
922
1060
|
mergeUnknownObjects: !replaceSubObjects,
|
|
923
|
-
|
|
1061
|
+
merge,
|
|
924
1062
|
}),
|
|
925
1063
|
);
|
|
926
1064
|
};
|
|
@@ -1126,3 +1264,11 @@ function assertNumber(key: unknown): asserts key is number {
|
|
|
1126
1264
|
if (typeof key !== 'number')
|
|
1127
1265
|
throw new Error('Only number keys are supported in list entities');
|
|
1128
1266
|
}
|
|
1267
|
+
|
|
1268
|
+
export function getEntityClient(
|
|
1269
|
+
entity: AnyEntity<any, any, any>,
|
|
1270
|
+
): ClientWithCollections {
|
|
1271
|
+
return (entity as Entity)[
|
|
1272
|
+
PRIVATE_ENTITY_CONTEXT_KEY
|
|
1273
|
+
].getClient() as ClientWithCollections;
|
|
1274
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AuthorizationKey,
|
|
2
3
|
DocumentBaseline,
|
|
3
4
|
ObjectIdentifier,
|
|
4
5
|
Operation,
|
|
@@ -20,7 +21,7 @@ export type EntityMetadataView = {
|
|
|
20
21
|
empty: boolean;
|
|
21
22
|
updatedAt: number;
|
|
22
23
|
latestTimestamp: string | null;
|
|
23
|
-
authz?:
|
|
24
|
+
authz?: AuthorizationKey;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export class EntityMetadata {
|
|
@@ -101,7 +102,7 @@ export class EntityMetadata {
|
|
|
101
102
|
// we don't use after for pending ops, they're all
|
|
102
103
|
// logically in the future
|
|
103
104
|
null,
|
|
104
|
-
|
|
105
|
+
);
|
|
105
106
|
if (pendingResult.authz) {
|
|
106
107
|
authz = pendingResult.authz;
|
|
107
108
|
}
|
|
@@ -221,10 +222,10 @@ export class EntityMetadata {
|
|
|
221
222
|
latestTimestamp: string | null;
|
|
222
223
|
deleted: boolean;
|
|
223
224
|
futureSeen: string | undefined;
|
|
224
|
-
authz?:
|
|
225
|
+
authz?: AuthorizationKey;
|
|
225
226
|
} => {
|
|
226
227
|
let futureSeen: string | undefined = undefined;
|
|
227
|
-
let authz:
|
|
228
|
+
let authz: AuthorizationKey | undefined = undefined;
|
|
228
229
|
|
|
229
230
|
const now = this.ctx.time.now;
|
|
230
231
|
for (const op of operations) {
|