@verdant-web/store 3.8.4 → 3.9.0-next.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 +8 -8
- package/dist/bundle/index.js.map +4 -4
- package/dist/esm/IDBService.d.ts +1 -1
- package/dist/esm/IDBService.js +7 -2
- package/dist/esm/IDBService.js.map +1 -1
- package/dist/esm/__tests__/entities.test.js +21 -16
- package/dist/esm/__tests__/entities.test.js.map +1 -1
- package/dist/esm/__tests__/fixtures/testStorage.d.ts +4 -0
- package/dist/esm/__tests__/fixtures/testStorage.js +3 -0
- package/dist/esm/__tests__/fixtures/testStorage.js.map +1 -1
- package/dist/esm/authorization.d.ts +4 -0
- package/dist/esm/authorization.js +6 -0
- package/dist/esm/authorization.js.map +1 -0
- package/dist/esm/client/Client.d.ts +23 -1
- package/dist/esm/client/Client.js +22 -2
- package/dist/esm/client/Client.js.map +1 -1
- package/dist/esm/{DocumentManager.d.ts → entities/DocumentManager.d.ts} +12 -6
- package/dist/esm/entities/DocumentManager.js +77 -0
- package/dist/esm/entities/DocumentManager.js.map +1 -0
- package/dist/esm/entities/Entity.d.ts +8 -0
- package/dist/esm/entities/Entity.js +23 -3
- package/dist/esm/entities/Entity.js.map +1 -1
- package/dist/esm/entities/EntityMetadata.d.ts +1 -0
- package/dist/esm/entities/EntityMetadata.js +18 -3
- package/dist/esm/entities/EntityMetadata.js.map +1 -1
- package/dist/esm/entities/EntityStore.d.ts +6 -4
- package/dist/esm/entities/EntityStore.js +21 -10
- package/dist/esm/entities/EntityStore.js.map +1 -1
- package/dist/esm/entities/types.d.ts +1 -0
- package/dist/esm/files/EntityFile.d.ts +1 -1
- package/dist/esm/files/EntityFile.js +7 -1
- package/dist/esm/files/EntityFile.js.map +1 -1
- package/dist/esm/files/FileManager.d.ts +11 -2
- package/dist/esm/files/FileManager.js +45 -8
- package/dist/esm/files/FileManager.js.map +1 -1
- package/dist/esm/files/FileStorage.d.ts +6 -0
- package/dist/esm/files/FileStorage.js +6 -1
- package/dist/esm/files/FileStorage.js.map +1 -1
- package/dist/esm/files/utils.d.ts +1 -2
- package/dist/esm/files/utils.js +11 -5
- package/dist/esm/files/utils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/metadata/LocalReplicaStore.d.ts +1 -0
- package/dist/esm/metadata/LocalReplicaStore.js +1 -0
- package/dist/esm/metadata/LocalReplicaStore.js.map +1 -1
- package/dist/esm/metadata/MessageCreator.js +4 -16
- package/dist/esm/metadata/MessageCreator.js.map +1 -1
- package/dist/esm/metadata/Metadata.d.ts +8 -0
- package/dist/esm/metadata/Metadata.js +32 -0
- package/dist/esm/metadata/Metadata.js.map +1 -1
- package/dist/esm/metadata/OperationsStore.js +3 -3
- package/dist/esm/metadata/OperationsStore.js.map +1 -1
- package/dist/esm/migration/engine.js +12 -2
- package/dist/esm/migration/engine.js.map +1 -1
- package/dist/esm/queries/CollectionQueries.d.ts +8 -2
- package/dist/esm/queries/CollectionQueries.js +2 -1
- package/dist/esm/queries/CollectionQueries.js.map +1 -1
- package/dist/esm/sync/FileSync.d.ts +1 -0
- package/dist/esm/sync/FileSync.js +5 -2
- package/dist/esm/sync/FileSync.js.map +1 -1
- package/dist/esm/sync/PushPullSync.d.ts +2 -1
- package/dist/esm/sync/PushPullSync.js +10 -6
- package/dist/esm/sync/PushPullSync.js.map +1 -1
- package/dist/esm/sync/ServerSyncEndpointProvider.d.ts +10 -1
- package/dist/esm/sync/ServerSyncEndpointProvider.js +13 -2
- package/dist/esm/sync/ServerSyncEndpointProvider.js.map +1 -1
- package/dist/esm/sync/Sync.d.ts +5 -4
- package/dist/esm/sync/Sync.js +22 -7
- package/dist/esm/sync/Sync.js.map +1 -1
- package/dist/esm/sync/WebSocketSync.d.ts +2 -1
- package/dist/esm/sync/WebSocketSync.js +5 -2
- package/dist/esm/sync/WebSocketSync.js.map +1 -1
- package/package.json +6 -6
- package/src/IDBService.ts +8 -4
- package/src/__tests__/entities.test.ts +29 -5
- package/src/__tests__/fixtures/testStorage.ts +3 -0
- package/src/authorization.ts +6 -0
- package/src/client/Client.ts +28 -6
- package/src/entities/DocumentManager.ts +154 -0
- package/src/entities/Entity.ts +26 -2
- package/src/entities/EntityMetadata.ts +22 -0
- package/src/entities/EntityStore.ts +28 -11
- package/src/entities/types.ts +1 -0
- package/src/files/EntityFile.ts +6 -2
- package/src/files/FileManager.ts +57 -9
- package/src/files/FileStorage.ts +7 -1
- package/src/files/utils.ts +17 -8
- package/src/index.ts +1 -0
- package/src/metadata/LocalReplicaStore.ts +2 -0
- package/src/metadata/MessageCreator.ts +4 -15
- package/src/metadata/Metadata.ts +37 -0
- package/src/metadata/OperationsStore.ts +3 -3
- package/src/migration/engine.ts +14 -2
- package/src/queries/CollectionQueries.ts +23 -4
- package/src/sync/FileSync.ts +6 -7
- package/src/sync/PushPullSync.ts +7 -2
- package/src/sync/ServerSyncEndpointProvider.ts +22 -2
- package/src/sync/Sync.ts +27 -6
- package/src/sync/WebSocketSync.ts +6 -2
- package/dist/esm/DocumentManager.js +0 -46
- package/dist/esm/DocumentManager.js.map +0 -1
- package/src/DocumentManager.ts +0 -97
package/src/IDBService.ts
CHANGED
|
@@ -81,7 +81,7 @@ export class IDBService extends Disposable {
|
|
|
81
81
|
iterate = async <T>(
|
|
82
82
|
storeName: string,
|
|
83
83
|
getRequest: (store: IDBObjectStore) => IDBRequest | IDBRequest[],
|
|
84
|
-
iterator: (value: T, store: IDBObjectStore) => void,
|
|
84
|
+
iterator: (value: T, store: IDBObjectStore) => boolean | void,
|
|
85
85
|
opts?: {
|
|
86
86
|
mode?: 'readonly' | 'readwrite';
|
|
87
87
|
transaction?: IDBTransaction;
|
|
@@ -117,10 +117,14 @@ export class IDBService extends Disposable {
|
|
|
117
117
|
}
|
|
118
118
|
return new Promise<void>((resolve, reject) => {
|
|
119
119
|
request.onsuccess = () => {
|
|
120
|
-
const cursor = request.result;
|
|
120
|
+
const cursor = request.result as IDBCursorWithValue | null;
|
|
121
121
|
if (cursor) {
|
|
122
|
-
iterator(cursor.value, store);
|
|
123
|
-
|
|
122
|
+
const stop = iterator(cursor.value, store);
|
|
123
|
+
if (stop) {
|
|
124
|
+
resolve();
|
|
125
|
+
} else {
|
|
126
|
+
cursor.continue();
|
|
127
|
+
}
|
|
124
128
|
} else {
|
|
125
129
|
resolve();
|
|
126
130
|
}
|
|
@@ -492,8 +492,7 @@ describe('entities', () => {
|
|
|
492
492
|
],
|
|
493
493
|
});
|
|
494
494
|
|
|
495
|
-
const
|
|
496
|
-
const item2 = await storage.todos.put(snapshot);
|
|
495
|
+
const item2 = await storage.todos.clone(item1);
|
|
497
496
|
|
|
498
497
|
expect(item2.get('tags').length).toBe(1);
|
|
499
498
|
expect(item2.get('attachments').length).toBe(1);
|
|
@@ -501,6 +500,7 @@ describe('entities', () => {
|
|
|
501
500
|
item2.get('attachments').get(0).set('name', 'attachment 2');
|
|
502
501
|
|
|
503
502
|
expect(item1.get('attachments').get(0).get('name')).toBe('attachment 1');
|
|
503
|
+
expect(item1.uid).not.toBe(item2.uid);
|
|
504
504
|
});
|
|
505
505
|
|
|
506
506
|
it('should not allow modifying the primary key', async () => {
|
|
@@ -634,13 +634,37 @@ describe('entities', () => {
|
|
|
634
634
|
|
|
635
635
|
it('should error on invalid values passed to initialization', async () => {
|
|
636
636
|
const storage = await createTestStorage();
|
|
637
|
-
expect(() => {
|
|
638
|
-
storage.todos.put({
|
|
637
|
+
expect(async () => {
|
|
638
|
+
await storage.todos.put({
|
|
639
639
|
content: { invalid: 'value' },
|
|
640
640
|
});
|
|
641
|
+
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
642
|
+
`[Error: Validation error: Expected string for field content, got [object Object]]`,
|
|
643
|
+
);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should only allow valid values for file fields', async () => {
|
|
647
|
+
const storage = await createTestStorage();
|
|
648
|
+
const weird = await storage.weirds.put({});
|
|
649
|
+
|
|
650
|
+
expect(() => {
|
|
651
|
+
weird.set('file', { invalid: 'value' });
|
|
641
652
|
}).toThrowErrorMatchingInlineSnapshot(
|
|
642
|
-
`[Error: Validation error: Expected
|
|
653
|
+
`[Error: Validation error: Expected file or null for field file, got [object Object]]`,
|
|
643
654
|
);
|
|
655
|
+
|
|
656
|
+
// valid options
|
|
657
|
+
weird.set(
|
|
658
|
+
'file',
|
|
659
|
+
new window.File(['d(⌐□_□)b'], 'test.txt', { type: 'text/plain' }),
|
|
660
|
+
);
|
|
661
|
+
weird.set('file', {
|
|
662
|
+
id: 'abc',
|
|
663
|
+
type: 'text/plain',
|
|
664
|
+
name: 'foo.txt',
|
|
665
|
+
url: 'http://example.com/foo.txt',
|
|
666
|
+
remote: true,
|
|
667
|
+
});
|
|
644
668
|
});
|
|
645
669
|
|
|
646
670
|
it('should allow subscribing to one field', async () => {
|
package/src/client/Client.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Operation,
|
|
7
7
|
} from '@verdant-web/common';
|
|
8
8
|
import { Context } from '../context.js';
|
|
9
|
-
import { DocumentManager } from '../DocumentManager.js';
|
|
9
|
+
import { DocumentManager } from '../entities/DocumentManager.js';
|
|
10
10
|
import { EntityStore } from '../entities/EntityStore.js';
|
|
11
11
|
import { FileManager, FileManagerConfig } from '../files/FileManager.js';
|
|
12
12
|
import { ReturnedFileData } from '../files/FileStorage.js';
|
|
@@ -112,11 +112,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
112
112
|
this._queryCache = new QueryCache({
|
|
113
113
|
context,
|
|
114
114
|
});
|
|
115
|
-
this._documentManager = new DocumentManager(
|
|
116
|
-
this.meta,
|
|
117
|
-
this.schema,
|
|
118
|
-
this._entities,
|
|
119
|
-
);
|
|
115
|
+
this._documentManager = new DocumentManager(this.schema, this._entities);
|
|
120
116
|
|
|
121
117
|
const notifyFutureSeen = debounce(() => {
|
|
122
118
|
this.emit('futureSeen');
|
|
@@ -231,6 +227,8 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
231
227
|
? await navigator.storage.estimate()
|
|
232
228
|
: undefined;
|
|
233
229
|
|
|
230
|
+
const files = await this._fileManager.stats();
|
|
231
|
+
|
|
234
232
|
// determine data:metadata ratio for total size of all collections vs metadata
|
|
235
233
|
const totalCollectionsSize = Object.values(collections).reduce(
|
|
236
234
|
(acc, { size }) => acc + size,
|
|
@@ -246,6 +244,7 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
246
244
|
totalMetaSize,
|
|
247
245
|
totalCollectionsSize,
|
|
248
246
|
metaToDataRatio,
|
|
247
|
+
files,
|
|
249
248
|
quotaUsage:
|
|
250
249
|
storage?.usage && storage?.quota
|
|
251
250
|
? storage.usage / storage.quota
|
|
@@ -433,6 +432,26 @@ export class Client<Presence = any, Profile = any> extends EventSubscriber<{
|
|
|
433
432
|
const exportData = await this.export();
|
|
434
433
|
await this.import(exportData);
|
|
435
434
|
};
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Immediately runs the file deletion process. This is useful
|
|
438
|
+
* for testing, mostly. Or if your client is long-lived, since
|
|
439
|
+
* normally this cleanup only runs on startup.
|
|
440
|
+
*
|
|
441
|
+
* Note this still follows the file deletion heuristic configured
|
|
442
|
+
* on the client. So if you clean up files 3 days after delete,
|
|
443
|
+
* invoking this manually will not skip that 3 day waiting period.
|
|
444
|
+
*/
|
|
445
|
+
__cleanupFilesImmediately = () => {
|
|
446
|
+
return this._fileManager.tryCleanupDeletedFiles();
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Manually triggers storage rebasing. Follows normal
|
|
451
|
+
* rebasing rules. Rebases already happen automatically
|
|
452
|
+
* during normal operation, so you probably don't need this.
|
|
453
|
+
*/
|
|
454
|
+
__manualRebase = () => this.meta.manualRebase();
|
|
436
455
|
}
|
|
437
456
|
|
|
438
457
|
export interface ClientStats {
|
|
@@ -441,6 +460,9 @@ export interface ClientStats {
|
|
|
441
460
|
baselinesSize: { count: number; size: number };
|
|
442
461
|
operationsSize: { count: number; size: number };
|
|
443
462
|
};
|
|
463
|
+
files: {
|
|
464
|
+
size: { count: number; size: number };
|
|
465
|
+
};
|
|
444
466
|
storage: StorageEstimate | undefined;
|
|
445
467
|
totalMetaSize: number;
|
|
446
468
|
totalCollectionsSize: number;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addFieldDefaults,
|
|
3
|
+
constrainEntity,
|
|
4
|
+
assert,
|
|
5
|
+
createOid,
|
|
6
|
+
SchemaCollection,
|
|
7
|
+
StorageCollectionSchema,
|
|
8
|
+
StorageDocument,
|
|
9
|
+
StorageSchema,
|
|
10
|
+
isRootOid,
|
|
11
|
+
AuthorizationKey,
|
|
12
|
+
} from '@verdant-web/common';
|
|
13
|
+
import { EntityCreateOptions, EntityStore } from '../entities/EntityStore.js';
|
|
14
|
+
import { Metadata } from '../metadata/Metadata.js';
|
|
15
|
+
import { Sync } from '../sync/Sync.js';
|
|
16
|
+
import { Context } from '../context.js';
|
|
17
|
+
import { ObjectEntity } from '../index.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Exposes functionality for creating documents,
|
|
21
|
+
* the only mutation which is available as an entry
|
|
22
|
+
* point in the storage system.
|
|
23
|
+
*/
|
|
24
|
+
export class DocumentManager<Schema extends StorageSchema<any>> {
|
|
25
|
+
constructor(
|
|
26
|
+
private schema: Schema,
|
|
27
|
+
private entities: EntityStore,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
private getOid = (collection: string, init: any) => {
|
|
31
|
+
const primaryKeyName = this.schema.collections[collection]
|
|
32
|
+
.primaryKey as Exclude<
|
|
33
|
+
keyof StorageDocument<SchemaCollection<Schema, any>>,
|
|
34
|
+
symbol
|
|
35
|
+
>;
|
|
36
|
+
const primaryKey = init[primaryKeyName];
|
|
37
|
+
assert(
|
|
38
|
+
primaryKey,
|
|
39
|
+
`Document must have a primary key: ${primaryKeyName.toString()} (got: ${JSON.stringify(
|
|
40
|
+
init,
|
|
41
|
+
)})`,
|
|
42
|
+
);
|
|
43
|
+
return createOid(collection, primaryKey);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
private addDefaults = (collectionName: string, init: any) => {
|
|
47
|
+
const collection = this.schema.collections[
|
|
48
|
+
collectionName
|
|
49
|
+
] as StorageCollectionSchema;
|
|
50
|
+
return addFieldDefaults(collection, init);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
private validate = (collectionName: string, init: any) => {
|
|
54
|
+
const collection = this.schema.collections[
|
|
55
|
+
collectionName
|
|
56
|
+
] as StorageCollectionSchema;
|
|
57
|
+
return constrainEntity(collection.fields, init);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
create = async (
|
|
61
|
+
collection: string,
|
|
62
|
+
init: any,
|
|
63
|
+
options: {
|
|
64
|
+
undoable?: boolean;
|
|
65
|
+
access?: AuthorizationKey;
|
|
66
|
+
silenceAccessControlWithPrimaryKeyWarning?: boolean;
|
|
67
|
+
} = {},
|
|
68
|
+
) => {
|
|
69
|
+
const widenedOptions = options as EntityCreateOptions;
|
|
70
|
+
const defaulted = this.addDefaults(collection, init);
|
|
71
|
+
const validated = this.validate(collection, defaulted);
|
|
72
|
+
const oid = this.getOid(collection, validated);
|
|
73
|
+
|
|
74
|
+
if (options.access) {
|
|
75
|
+
const collectionSchema = this.schema.collections[collection];
|
|
76
|
+
if (
|
|
77
|
+
options.access !== 'shared' &&
|
|
78
|
+
init[collectionSchema.primaryKey] &&
|
|
79
|
+
!options.silenceAccessControlWithPrimaryKeyWarning
|
|
80
|
+
) {
|
|
81
|
+
// using a custom primary key with access control is not supported.
|
|
82
|
+
// resulting docs could collide with existing docs with different permissions,
|
|
83
|
+
// leading to confusing results. this logs a warning.
|
|
84
|
+
console.warn(
|
|
85
|
+
`Using a custom primary key with access control is not supported. This may result in corrupted documents. Read more about why: https://verdant.dev/docs/sync/access#a-warning-about-custom-primaryKey`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
widenedOptions.access = options.access;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// documents are always objects at the root
|
|
92
|
+
return this.entities.create(validated, oid, widenedOptions) as any;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
delete = async (
|
|
96
|
+
collection: string,
|
|
97
|
+
primaryKey: string,
|
|
98
|
+
options: { undoable?: boolean } = {},
|
|
99
|
+
) => {
|
|
100
|
+
const oid = createOid(collection, primaryKey);
|
|
101
|
+
return this.entities.delete(oid, options);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
deleteAll = async (
|
|
105
|
+
ids: [string, string][],
|
|
106
|
+
options: { undoable?: boolean } = {},
|
|
107
|
+
) => {
|
|
108
|
+
return this.entities.deleteAll(
|
|
109
|
+
ids.map(([collection, primaryKey]) => createOid(collection, primaryKey)),
|
|
110
|
+
options,
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
deleteAllFromCollection = async (
|
|
115
|
+
collection: string,
|
|
116
|
+
ids: string[],
|
|
117
|
+
options: { undoable?: boolean } = {},
|
|
118
|
+
) => {
|
|
119
|
+
return this.entities.deleteAll(
|
|
120
|
+
ids.map((primaryKey) => createOid(collection, primaryKey)),
|
|
121
|
+
options,
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
clone = async (
|
|
126
|
+
collection: string,
|
|
127
|
+
entity: ObjectEntity<any, any>,
|
|
128
|
+
options: {
|
|
129
|
+
undoable?: boolean;
|
|
130
|
+
access?: AuthorizationKey;
|
|
131
|
+
primaryKey?: string;
|
|
132
|
+
} = {},
|
|
133
|
+
) => {
|
|
134
|
+
if (!isRootOid(entity.uid)) {
|
|
135
|
+
throw new Error('Cannot clone non-root documents');
|
|
136
|
+
}
|
|
137
|
+
// take the entity snapshot
|
|
138
|
+
const snapshot = entity.getSnapshot();
|
|
139
|
+
// remove the primary key
|
|
140
|
+
const collectionSchema = this.schema.collections[collection];
|
|
141
|
+
delete snapshot[collectionSchema.primaryKey];
|
|
142
|
+
// if collection schema's primary key doesn't have a default value,
|
|
143
|
+
// a user-supplied value is required
|
|
144
|
+
if (!collectionSchema.fields[collectionSchema.primaryKey].default) {
|
|
145
|
+
if (!options.primaryKey) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Error cloning document from collection ${collection}: collection does not have a default on primary key. You must supply a value to options.primaryKey for the clone.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
snapshot[collectionSchema.primaryKey] = options.primaryKey;
|
|
151
|
+
}
|
|
152
|
+
return this.create(collection, snapshot, options);
|
|
153
|
+
};
|
|
154
|
+
}
|
package/src/entities/Entity.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getDefault,
|
|
18
18
|
hasDefault,
|
|
19
19
|
isFileRef,
|
|
20
|
+
isFile,
|
|
20
21
|
isNullable,
|
|
21
22
|
isObject,
|
|
22
23
|
isObjectRef,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
} from '@verdant-web/common';
|
|
30
31
|
import { Context } from '../context.js';
|
|
31
32
|
import { FileManager } from '../files/FileManager.js';
|
|
32
|
-
import {
|
|
33
|
+
import { processValueFiles } from '../files/utils.js';
|
|
33
34
|
import { EntityFile } from '../index.js';
|
|
34
35
|
import { EntityCache } from './EntityCache.js';
|
|
35
36
|
import { EntityFamilyMetadata, EntityMetadataView } from './EntityMetadata.js';
|
|
@@ -382,6 +383,19 @@ export class Entity<
|
|
|
382
383
|
return this.ctx.namespace;
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
/**
|
|
387
|
+
* The authz string signifying the permissions this entity has.
|
|
388
|
+
* On the client (where we are) it's only ever possible to see
|
|
389
|
+
* an entity with either full access or access for the current
|
|
390
|
+
* user.
|
|
391
|
+
*/
|
|
392
|
+
get access() {
|
|
393
|
+
return this.viewData.authz;
|
|
394
|
+
}
|
|
395
|
+
get isAuthorized() {
|
|
396
|
+
return !!this.access;
|
|
397
|
+
}
|
|
398
|
+
|
|
385
399
|
/**
|
|
386
400
|
* Pruning - when entities have invalid children, we 'prune' that
|
|
387
401
|
* data up to the nearest prunable point - a nullable field,
|
|
@@ -443,6 +457,14 @@ export class Entity<
|
|
|
443
457
|
// change management methods (internal use only)
|
|
444
458
|
private addPendingOperations = (operations: Operation[]) => {
|
|
445
459
|
this.ctx.log('debug', 'Entity: adding pending operations', this.oid);
|
|
460
|
+
|
|
461
|
+
// apply authz to all operations
|
|
462
|
+
if (this.access) {
|
|
463
|
+
for (const op of operations) {
|
|
464
|
+
op.authz = this.access;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
446
468
|
const changes = this.metadataFamily.addPendingData(operations);
|
|
447
469
|
for (const change of changes) {
|
|
448
470
|
this.change(change);
|
|
@@ -708,7 +730,9 @@ export class Entity<
|
|
|
708
730
|
if (validationError) {
|
|
709
731
|
// TODO: is it a good idea to throw an error here? a runtime error won't be that helpful,
|
|
710
732
|
// but also we don't really want invalid data supplied.
|
|
711
|
-
throw new Error(validationError.message
|
|
733
|
+
throw new Error(`Validation error: ${validationError.message}`, {
|
|
734
|
+
cause: validationError,
|
|
735
|
+
});
|
|
712
736
|
}
|
|
713
737
|
}
|
|
714
738
|
return processValueFiles(value, this.files.add);
|
|
@@ -20,6 +20,7 @@ export type EntityMetadataView = {
|
|
|
20
20
|
empty: boolean;
|
|
21
21
|
updatedAt: number;
|
|
22
22
|
latestTimestamp: string | null;
|
|
23
|
+
authz?: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export class EntityMetadata {
|
|
@@ -63,6 +64,11 @@ export class EntityMetadata {
|
|
|
63
64
|
computeView = (omitPending = false): EntityMetadataView => {
|
|
64
65
|
const base = cloneDeep(this.baseline?.snapshot ?? undefined);
|
|
65
66
|
const baselineTimestamp = this.baseline?.timestamp ?? null;
|
|
67
|
+
|
|
68
|
+
// start with the baseline authz, if any. further init ops
|
|
69
|
+
// may overwrite this.
|
|
70
|
+
let authz = this.baseline?.authz;
|
|
71
|
+
|
|
66
72
|
const confirmedResult = this.applyOperations(
|
|
67
73
|
// apply ops to baseline
|
|
68
74
|
base,
|
|
@@ -79,6 +85,10 @@ export class EntityMetadata {
|
|
|
79
85
|
if (confirmedResult.futureSeen) {
|
|
80
86
|
this.ctx.globalEvents.emit('futureSeen', confirmedResult.futureSeen);
|
|
81
87
|
}
|
|
88
|
+
if (confirmedResult.authz) {
|
|
89
|
+
authz = confirmedResult.authz;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
const pendingResult = omitPending
|
|
83
93
|
? confirmedResult
|
|
84
94
|
: this.applyOperations(
|
|
@@ -92,6 +102,10 @@ export class EntityMetadata {
|
|
|
92
102
|
// logically in the future
|
|
93
103
|
null,
|
|
94
104
|
);
|
|
105
|
+
if (pendingResult.authz) {
|
|
106
|
+
authz = pendingResult.authz;
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
// before letting this data out into the wild, we need
|
|
96
110
|
// to associate its oid
|
|
97
111
|
if (pendingResult.view) {
|
|
@@ -138,6 +152,7 @@ export class EntityMetadata {
|
|
|
138
152
|
fromOlderVersion,
|
|
139
153
|
updatedAt,
|
|
140
154
|
latestTimestamp: updatedAtTimestamp,
|
|
155
|
+
authz,
|
|
141
156
|
};
|
|
142
157
|
};
|
|
143
158
|
|
|
@@ -207,8 +222,11 @@ export class EntityMetadata {
|
|
|
207
222
|
latestTimestamp: string | null;
|
|
208
223
|
deleted: boolean;
|
|
209
224
|
futureSeen: string | undefined;
|
|
225
|
+
authz?: string;
|
|
210
226
|
} => {
|
|
211
227
|
let futureSeen: string | undefined = undefined;
|
|
228
|
+
let authz: string | undefined = undefined;
|
|
229
|
+
|
|
212
230
|
const now = this.ctx.getNow();
|
|
213
231
|
for (const op of operations) {
|
|
214
232
|
// ignore ops before our after cutoff
|
|
@@ -229,6 +247,9 @@ export class EntityMetadata {
|
|
|
229
247
|
base = applyPatch(base, op.data);
|
|
230
248
|
if (op.data.op === 'initialize') {
|
|
231
249
|
deleted = false;
|
|
250
|
+
if (op.authz) {
|
|
251
|
+
authz = op.authz;
|
|
252
|
+
}
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
|
|
@@ -242,6 +263,7 @@ export class EntityMetadata {
|
|
|
242
263
|
latestTimestamp: latestTimestamp ?? null,
|
|
243
264
|
deleted,
|
|
244
265
|
futureSeen,
|
|
266
|
+
authz,
|
|
245
267
|
};
|
|
246
268
|
};
|
|
247
269
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
groupPatchesByRootOid,
|
|
14
14
|
isRootOid,
|
|
15
15
|
removeOidsFromAllSubObjects,
|
|
16
|
+
AuthorizationKey,
|
|
16
17
|
} from '@verdant-web/common';
|
|
17
18
|
import { Context } from '../context.js';
|
|
18
19
|
import { Metadata } from '../metadata/Metadata.js';
|
|
@@ -49,6 +50,11 @@ export interface IncomingData {
|
|
|
49
50
|
isLocal?: boolean;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
export interface EntityCreateOptions {
|
|
54
|
+
undoable?: boolean;
|
|
55
|
+
access?: AuthorizationKey;
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
export class EntityStore extends Disposable {
|
|
53
59
|
private ctx;
|
|
54
60
|
private meta;
|
|
@@ -320,7 +326,7 @@ export class EntityStore extends Disposable {
|
|
|
320
326
|
create = async (
|
|
321
327
|
initial: any,
|
|
322
328
|
oid: ObjectIdentifier,
|
|
323
|
-
{ undoable = true }:
|
|
329
|
+
{ undoable = true, access }: EntityCreateOptions = {},
|
|
324
330
|
) => {
|
|
325
331
|
this.ctx.log('debug', 'Creating new entity', oid);
|
|
326
332
|
const { collection } = decomposeOid(oid);
|
|
@@ -340,17 +346,23 @@ export class EntityStore extends Disposable {
|
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
const operations = this.meta.patchCreator.createInitialize(processed, oid);
|
|
349
|
+
if (access) {
|
|
350
|
+
operations.forEach((op) => {
|
|
351
|
+
op.authz = access;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
343
354
|
await this.batcher.commitOperations(operations, {
|
|
344
355
|
undoable: !!undoable,
|
|
345
356
|
source: entity,
|
|
346
357
|
});
|
|
347
358
|
|
|
348
|
-
//
|
|
359
|
+
// TODONE: what happens if you create an entity with an OID that already
|
|
349
360
|
// exists?
|
|
361
|
+
// A: it will overwrite the existing entity
|
|
350
362
|
|
|
351
363
|
// we still need to synchronously add the initial operations to the Entity
|
|
352
364
|
// even though they are flowing through the system
|
|
353
|
-
//
|
|
365
|
+
// FIXME: this could be better aligned to avoid grouping here
|
|
354
366
|
const operationsGroupedByOid = groupPatchesByOid(operations);
|
|
355
367
|
this.events.add.invoke(this, {
|
|
356
368
|
operations: operationsGroupedByOid,
|
|
@@ -372,12 +384,7 @@ export class EntityStore extends Disposable {
|
|
|
372
384
|
'Only root documents may be deleted via client methods',
|
|
373
385
|
);
|
|
374
386
|
|
|
375
|
-
const
|
|
376
|
-
oids.flatMap(async (oid) => {
|
|
377
|
-
const entity = await this.hydrate(oid);
|
|
378
|
-
return entity?.__getFamilyOids__() ?? [];
|
|
379
|
-
}),
|
|
380
|
-
);
|
|
387
|
+
const entities = await Promise.all(oids.map((oid) => this.hydrate(oid)));
|
|
381
388
|
|
|
382
389
|
// remove the entities from cache
|
|
383
390
|
oids.forEach((oid) => {
|
|
@@ -385,8 +392,18 @@ export class EntityStore extends Disposable {
|
|
|
385
392
|
this.ctx.log('debug', 'Deleted document from cache', oid);
|
|
386
393
|
});
|
|
387
394
|
|
|
388
|
-
|
|
389
|
-
const
|
|
395
|
+
const operations: Operation[] = [];
|
|
396
|
+
for (const entity of entities) {
|
|
397
|
+
if (entity) {
|
|
398
|
+
const oids = entity.__getFamilyOids__();
|
|
399
|
+
const deletes = this.meta.patchCreator.createDeleteAll(oids);
|
|
400
|
+
for (const op of deletes) {
|
|
401
|
+
op.authz = entity.access;
|
|
402
|
+
}
|
|
403
|
+
operations.push(...deletes);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
390
407
|
await this.batcher.commitOperations(operations, {
|
|
391
408
|
undoable: options?.undoable === undefined ? true : options.undoable,
|
|
392
409
|
});
|
package/src/entities/types.ts
CHANGED
package/src/files/EntityFile.ts
CHANGED
|
@@ -101,10 +101,14 @@ export class EntityFile extends EventSubscriber<EntityFileEvents> {
|
|
|
101
101
|
this.dispose();
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
getSnapshot():
|
|
104
|
+
getSnapshot(): FileData {
|
|
105
|
+
if (this._fileData) return this._fileData;
|
|
105
106
|
return {
|
|
106
107
|
id: this.id,
|
|
107
|
-
url: this.
|
|
108
|
+
url: this._objectUrl ?? undefined,
|
|
109
|
+
name: this.name ?? 'unknown-file',
|
|
110
|
+
remote: false,
|
|
111
|
+
type: this.type ?? '',
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
}
|