@lossless.org/client 0.1.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/.smartconfig.json +37 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.losslessorgclient.d.ts +32 -0
- package/dist_ts/classes.losslessorgclient.js +103 -0
- package/dist_ts/core/classes.error.d.ts +16 -0
- package/dist_ts/core/classes.error.js +29 -0
- package/dist_ts/core/classes.operation.d.ts +17 -0
- package/dist_ts/core/classes.operation.js +70 -0
- package/dist_ts/core/interfaces.d.ts +18 -0
- package/dist_ts/core/interfaces.js +2 -0
- package/dist_ts/index.d.ts +4 -0
- package/dist_ts/index.js +3 -0
- package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
- package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
- package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
- package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
- package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
- package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
- package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
- package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
- package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
- package/dist_ts/nosqldb/classes.cached-document.js +112 -0
- package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
- package/dist_ts/nosqldb/classes.collection.js +1568 -0
- package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
- package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
- package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
- package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
- package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
- package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
- package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
- package/dist_ts/nosqldb/classes.connection.js +23 -0
- package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
- package/dist_ts/nosqldb/classes.convenience.js +5 -0
- package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
- package/dist_ts/nosqldb/classes.cursor.js +82 -0
- package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
- package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
- package/dist_ts/nosqldb/classes.db.d.ts +98 -0
- package/dist_ts/nosqldb/classes.db.js +434 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
- package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
- package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
- package/dist_ts/nosqldb/classes.doc.js +2976 -0
- package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
- package/dist_ts/nosqldb/classes.easystore.js +199 -0
- package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
- package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
- package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
- package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
- package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
- package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
- package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
- package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
- package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
- package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
- package/dist_ts/nosqldb/classes.persistence.js +34 -0
- package/dist_ts/nosqldb/classes.session.d.ts +28 -0
- package/dist_ts/nosqldb/classes.session.js +246 -0
- package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
- package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
- package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
- package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
- package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
- package/dist_ts/nosqldb/classes.watcher.js +98 -0
- package/dist_ts/nosqldb/index.d.ts +27 -0
- package/dist_ts/nosqldb/index.js +18 -0
- package/dist_ts/nosqldb/logging.d.ts +2 -0
- package/dist_ts/nosqldb/logging.js +3 -0
- package/dist_ts/nosqldb/plugins.d.ts +17 -0
- package/dist_ts/nosqldb/plugins.js +20 -0
- package/dist_ts/nosqldb/shim.d.ts +1 -0
- package/dist_ts/nosqldb/shim.js +8 -0
- package/dist_ts/nosqldb/testsupport.d.ts +3 -0
- package/dist_ts/nosqldb/testsupport.js +3 -0
- package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
- package/dist_ts/objectstorage/classes.bucket.js +939 -0
- package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
- package/dist_ts/objectstorage/classes.connection.js +27 -0
- package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
- package/dist_ts/objectstorage/classes.directory.js +335 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
- package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
- package/dist_ts/objectstorage/classes.file.d.ts +91 -0
- package/dist_ts/objectstorage/classes.file.js +255 -0
- package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
- package/dist_ts/objectstorage/classes.listcursor.js +67 -0
- package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
- package/dist_ts/objectstorage/classes.metadata.js +113 -0
- package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
- package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
- package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
- package/dist_ts/objectstorage/classes.trash.js +22 -0
- package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
- package/dist_ts/objectstorage/classes.watcher.js +249 -0
- package/dist_ts/objectstorage/helpers.d.ts +25 -0
- package/dist_ts/objectstorage/helpers.js +215 -0
- package/dist_ts/objectstorage/index.d.ts +13 -0
- package/dist_ts/objectstorage/index.js +13 -0
- package/dist_ts/objectstorage/interfaces.d.ts +276 -0
- package/dist_ts/objectstorage/interfaces.js +2 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
- package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
- package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
- package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
- package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
- package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
- package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
- package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
- package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
- package/dist_ts/objectstorage/plugins.d.ts +17 -0
- package/dist_ts/objectstorage/plugins.js +23 -0
- package/dist_ts/plugins.d.ts +4 -0
- package/dist_ts/plugins.js +5 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
- package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
- package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
- package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
- package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
- package/dist_ts/sqldb/classes.sqltable.js +70 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
- package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
- package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
- package/dist_ts/sqldb/clickhouse/index.js +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
- package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
- package/dist_ts/sqldb/index.d.ts +6 -0
- package/dist_ts/sqldb/index.js +6 -0
- package/dist_ts/sqldb/interfaces.d.ts +53 -0
- package/dist_ts/sqldb/interfaces.js +19 -0
- package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
- package/dist_ts/sqldb/internal.sqlpool.js +214 -0
- package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
- package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
- package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
- package/dist_ts/sqldb/plugins.mariadb.js +5 -0
- package/docs/source/smartbucket/license.md +19 -0
- package/docs/source/smartclickhouse/license.md +19 -0
- package/license.md +21 -0
- package/package.json +101 -0
- package/readme.md +157 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.losslessorgclient.ts +104 -0
- package/ts/core/classes.error.ts +31 -0
- package/ts/core/classes.operation.ts +60 -0
- package/ts/core/interfaces.ts +24 -0
- package/ts/index.ts +4 -0
- package/ts/nosqldb/classes.atomicdelete.ts +81 -0
- package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
- package/ts/nosqldb/classes.atomicupdate.ts +99 -0
- package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
- package/ts/nosqldb/classes.bsonassertion.ts +25 -0
- package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
- package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
- package/ts/nosqldb/classes.cached-document.ts +67 -0
- package/ts/nosqldb/classes.collection.ts +2399 -0
- package/ts/nosqldb/classes.collectionfactory.ts +87 -0
- package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
- package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
- package/ts/nosqldb/classes.collectiontopology.ts +645 -0
- package/ts/nosqldb/classes.connection.ts +31 -0
- package/ts/nosqldb/classes.convenience.ts +5 -0
- package/ts/nosqldb/classes.cursor.ts +88 -0
- package/ts/nosqldb/classes.cursoroperation.ts +38 -0
- package/ts/nosqldb/classes.db.ts +637 -0
- package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
- package/ts/nosqldb/classes.doc.ts +4928 -0
- package/ts/nosqldb/classes.easystore.ts +154 -0
- package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
- package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
- package/ts/nosqldb/classes.mongoadministration.ts +867 -0
- package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
- package/ts/nosqldb/classes.operationbudget.ts +75 -0
- package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
- package/ts/nosqldb/classes.persistence.ts +62 -0
- package/ts/nosqldb/classes.session.ts +389 -0
- package/ts/nosqldb/classes.storedinspection.ts +151 -0
- package/ts/nosqldb/classes.testdatabase.ts +272 -0
- package/ts/nosqldb/classes.watcher.ts +96 -0
- package/ts/nosqldb/index.ts +51 -0
- package/ts/nosqldb/logging.ts +3 -0
- package/ts/nosqldb/plugins.ts +37 -0
- package/ts/nosqldb/shim.ts +6 -0
- package/ts/nosqldb/testsupport.ts +13 -0
- package/ts/objectstorage/classes.bucket.ts +1114 -0
- package/ts/objectstorage/classes.connection.ts +36 -0
- package/ts/objectstorage/classes.directory.ts +425 -0
- package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
- package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
- package/ts/objectstorage/classes.file.ts +303 -0
- package/ts/objectstorage/classes.listcursor.ts +89 -0
- package/ts/objectstorage/classes.metadata.ts +134 -0
- package/ts/objectstorage/classes.smartbucket.ts +445 -0
- package/ts/objectstorage/classes.trash.ts +30 -0
- package/ts/objectstorage/classes.watcher.ts +289 -0
- package/ts/objectstorage/helpers.ts +254 -0
- package/ts/objectstorage/index.ts +13 -0
- package/ts/objectstorage/interfaces.ts +389 -0
- package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
- package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
- package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
- package/ts/objectstorage/internal.multipart.planner.ts +90 -0
- package/ts/objectstorage/internal.multipart.stream.ts +606 -0
- package/ts/objectstorage/internal.multipart.types.ts +106 -0
- package/ts/objectstorage/plugins.ts +35 -0
- package/ts/plugins.ts +4 -0
- package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
- package/ts/sqldb/classes.sqlconnection.ts +350 -0
- package/ts/sqldb/classes.sqltable.ts +70 -0
- package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
- package/ts/sqldb/clickhouse/index.ts +16 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
- package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
- package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
- package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
- package/ts/sqldb/index.ts +6 -0
- package/ts/sqldb/interfaces.ts +61 -0
- package/ts/sqldb/internal.sqlpool.ts +179 -0
- package/ts/sqldb/plugins.clickhouse.ts +4 -0
- package/ts/sqldb/plugins.mariadb.ts +4 -0
|
@@ -0,0 +1,1974 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { getInertObjectIdBytes } from './classes.bsonobjectid.js';
|
|
3
|
+
import { assertBsonWithoutMutation } from './classes.bsonassertion.js';
|
|
4
|
+
import { getOrdinaryPersistencePolicy } from './classes.ordinarypersistence.js';
|
|
5
|
+
|
|
6
|
+
import type { SmartdataCollection } from './classes.collection.js';
|
|
7
|
+
import { registerCollectionReconnectHandler } from './classes.collectionlifecycle.js';
|
|
8
|
+
import type { SmartdataSession } from './classes.session.js';
|
|
9
|
+
import { acquireSmartdataSession } from './classes.session.js';
|
|
10
|
+
import {
|
|
11
|
+
convertFilterForMongoDb,
|
|
12
|
+
type MongoFilter,
|
|
13
|
+
type SmartDataDbDoc,
|
|
14
|
+
type TSmartdataIdentityValueType,
|
|
15
|
+
} from './classes.doc.js';
|
|
16
|
+
|
|
17
|
+
const exactPersistencePolicySymbol = Symbol.for(
|
|
18
|
+
'@push.rocks/smartdata.exactPersistencePolicy',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const storageMetadataKeys = ['_id', '_smartdataRevision'] as const;
|
|
22
|
+
const reservedKeys = [
|
|
23
|
+
...storageMetadataKeys,
|
|
24
|
+
'_createdAt',
|
|
25
|
+
'_updatedAt',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
type TReservedKey = (typeof reservedKeys)[number];
|
|
29
|
+
type TStorageMetadataKey = (typeof storageMetadataKeys)[number];
|
|
30
|
+
|
|
31
|
+
export type TExactPersistedDocument<TPersisted extends object> = Omit<
|
|
32
|
+
TPersisted,
|
|
33
|
+
TReservedKey
|
|
34
|
+
> & {
|
|
35
|
+
[TKey in TReservedKey]?: never;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TStoredDocument<TPersisted extends object> = Omit<
|
|
39
|
+
TPersisted,
|
|
40
|
+
TReservedKey
|
|
41
|
+
> & {
|
|
42
|
+
_id: plugins.mongodb.ObjectId;
|
|
43
|
+
_smartdataRevision?: string;
|
|
44
|
+
_createdAt?: never;
|
|
45
|
+
_updatedAt?: never;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type TExactDistinctValue<TValue> = TValue extends ReadonlyArray<infer TEntry>
|
|
49
|
+
? TEntry
|
|
50
|
+
: TValue;
|
|
51
|
+
|
|
52
|
+
type TExactDocumentPathLeaf =
|
|
53
|
+
| string
|
|
54
|
+
| number
|
|
55
|
+
| boolean
|
|
56
|
+
| bigint
|
|
57
|
+
| symbol
|
|
58
|
+
| null
|
|
59
|
+
| undefined
|
|
60
|
+
| Date
|
|
61
|
+
| RegExp
|
|
62
|
+
| Uint8Array
|
|
63
|
+
| ((...args: any[]) => unknown)
|
|
64
|
+
| { readonly _bsontype: string };
|
|
65
|
+
|
|
66
|
+
type TExactDocumentPathTarget<TValue> =
|
|
67
|
+
NonNullable<TValue> extends ReadonlyArray<infer TEntry>
|
|
68
|
+
? NonNullable<TEntry>
|
|
69
|
+
: NonNullable<TValue>;
|
|
70
|
+
|
|
71
|
+
type TExactDocumentPathInternal<
|
|
72
|
+
TDocument,
|
|
73
|
+
TDepth extends readonly unknown[],
|
|
74
|
+
> = TDepth['length'] extends 8
|
|
75
|
+
? never
|
|
76
|
+
: TDocument extends TExactDocumentPathLeaf
|
|
77
|
+
? never
|
|
78
|
+
: TDocument extends object
|
|
79
|
+
? {
|
|
80
|
+
[TKey in keyof TDocument & string]:
|
|
81
|
+
TExactDocumentPathTarget<TDocument[TKey]> extends TExactDocumentPathLeaf
|
|
82
|
+
? TKey
|
|
83
|
+
: TExactDocumentPathTarget<TDocument[TKey]> extends object
|
|
84
|
+
? TKey | `${TKey}.${TExactDocumentPathInternal<
|
|
85
|
+
TExactDocumentPathTarget<TDocument[TKey]>,
|
|
86
|
+
[...TDepth, unknown]
|
|
87
|
+
>}`
|
|
88
|
+
: TKey;
|
|
89
|
+
}[keyof TDocument & string]
|
|
90
|
+
: never;
|
|
91
|
+
|
|
92
|
+
type TExactDocumentPathContainer<TValue> =
|
|
93
|
+
NonNullable<TValue> extends ReadonlyArray<infer TEntry>
|
|
94
|
+
? NonNullable<TEntry>
|
|
95
|
+
: NonNullable<TValue>;
|
|
96
|
+
|
|
97
|
+
export type TExactDocumentPath<TDocument extends object> =
|
|
98
|
+
TExactDocumentPathInternal<TDocument, []>;
|
|
99
|
+
|
|
100
|
+
type TExactDocumentPathValueForContainer<
|
|
101
|
+
TContainer,
|
|
102
|
+
TPath extends string,
|
|
103
|
+
> = TContainer extends unknown
|
|
104
|
+
? TPath extends `${infer THead}.${infer TTail}`
|
|
105
|
+
? THead extends keyof TContainer
|
|
106
|
+
? TExactDocumentPathValue<
|
|
107
|
+
TContainer[THead],
|
|
108
|
+
TTail
|
|
109
|
+
>
|
|
110
|
+
: never
|
|
111
|
+
: TPath extends keyof TContainer
|
|
112
|
+
? Exclude<TContainer[TPath], undefined>
|
|
113
|
+
: never
|
|
114
|
+
: never;
|
|
115
|
+
|
|
116
|
+
export type TExactDocumentPathValue<
|
|
117
|
+
TDocument,
|
|
118
|
+
TPath extends string,
|
|
119
|
+
> = TExactDocumentPathValueForContainer<
|
|
120
|
+
TExactDocumentPathContainer<TDocument>,
|
|
121
|
+
TPath
|
|
122
|
+
>;
|
|
123
|
+
|
|
124
|
+
export const isMongoObjectId = (
|
|
125
|
+
valueArg: unknown,
|
|
126
|
+
): valueArg is plugins.mongodb.ObjectId =>
|
|
127
|
+
valueArg instanceof plugins.mongodb.ObjectId;
|
|
128
|
+
|
|
129
|
+
export type TExactInsertResult<TPersisted extends object> =
|
|
130
|
+
| {
|
|
131
|
+
status: 'inserted' | 'already_committed';
|
|
132
|
+
document: TStoredDocument<TPersisted>;
|
|
133
|
+
}
|
|
134
|
+
| {
|
|
135
|
+
status: 'conflict';
|
|
136
|
+
document: TStoredDocument<TPersisted>;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type TExactTransitionResult<TPersisted extends object> =
|
|
140
|
+
| {
|
|
141
|
+
status: 'transitioned';
|
|
142
|
+
document: TStoredDocument<TPersisted>;
|
|
143
|
+
}
|
|
144
|
+
| {
|
|
145
|
+
status: 'concurrent_change';
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type TExactDeleteResult =
|
|
149
|
+
| {
|
|
150
|
+
status: 'deleted';
|
|
151
|
+
deletedCount: 1;
|
|
152
|
+
}
|
|
153
|
+
| {
|
|
154
|
+
status: 'concurrent_change';
|
|
155
|
+
deletedCount: 0;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type TExactPersistenceErrorCode =
|
|
159
|
+
| 'ambiguous_write'
|
|
160
|
+
| 'invalid_configuration'
|
|
161
|
+
| 'invalid_document'
|
|
162
|
+
| 'noncanonical_document'
|
|
163
|
+
| 'reserved_field'
|
|
164
|
+
| 'unique_conflict'
|
|
165
|
+
| 'unsupported_operation';
|
|
166
|
+
|
|
167
|
+
export class SmartdataExactPersistenceError extends Error {
|
|
168
|
+
public readonly code: TExactPersistenceErrorCode;
|
|
169
|
+
|
|
170
|
+
constructor(
|
|
171
|
+
codeArg: TExactPersistenceErrorCode,
|
|
172
|
+
messageArg: string,
|
|
173
|
+
optionsArg?: ErrorOptions,
|
|
174
|
+
) {
|
|
175
|
+
super(messageArg, optionsArg);
|
|
176
|
+
this.name = 'SmartdataExactPersistenceError';
|
|
177
|
+
this.code = codeArg;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type TExactDocumentAssertion<TPersisted extends object> = (
|
|
182
|
+
documentArg: unknown,
|
|
183
|
+
) => asserts documentArg is TPersisted;
|
|
184
|
+
|
|
185
|
+
type TExactAssertedDocument<
|
|
186
|
+
TAssertion extends TExactDocumentAssertion<object>,
|
|
187
|
+
> = TAssertion extends TExactDocumentAssertion<infer TPersisted>
|
|
188
|
+
? TPersisted
|
|
189
|
+
: never;
|
|
190
|
+
|
|
191
|
+
export type TExactIdentityKey<TPersisted extends object> = {
|
|
192
|
+
[TKey in Exclude<keyof TPersisted, TReservedKey> & string]-?:
|
|
193
|
+
[TPersisted[TKey]] extends [string] ? TKey : never;
|
|
194
|
+
}[Exclude<keyof TPersisted, TReservedKey> & string];
|
|
195
|
+
|
|
196
|
+
export interface IExactPersistenceOptions<TPersisted extends object> {
|
|
197
|
+
reconcileBy?: TExactIdentityKey<TPersisted>;
|
|
198
|
+
assertDocument: TExactDocumentAssertion<TPersisted>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type TPersistedOf<
|
|
202
|
+
TModel extends SmartDataDbDoc<any, any, any>,
|
|
203
|
+
> = TModel extends SmartDataDbDoc<any, infer TPersisted, any>
|
|
204
|
+
? TPersisted
|
|
205
|
+
: never;
|
|
206
|
+
|
|
207
|
+
export type TExact<
|
|
208
|
+
TModel extends SmartDataDbDoc<any, any, any>,
|
|
209
|
+
> = SmartdataExactCollection<TModel, TPersistedOf<TModel>>;
|
|
210
|
+
|
|
211
|
+
export interface IExactSessionOptions {
|
|
212
|
+
session?: SmartdataSession;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface IExactReadOptions extends IExactSessionOptions {
|
|
216
|
+
signal?: AbortSignal;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface IFindStoredOptions<TPersisted extends object> extends IExactReadOptions {
|
|
220
|
+
filter?: MongoFilter<TStoredDocument<TPersisted>>;
|
|
221
|
+
sort?: Partial<
|
|
222
|
+
Record<TExactDocumentPath<TStoredDocument<TPersisted>>, 1 | -1>
|
|
223
|
+
>;
|
|
224
|
+
skip?: number;
|
|
225
|
+
limit?: number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
interface IExactModelConstructor<
|
|
229
|
+
TModel extends SmartDataDbDoc<any, any, any>,
|
|
230
|
+
> {
|
|
231
|
+
new (...args: any[]): TModel;
|
|
232
|
+
collection: SmartdataCollection<any>;
|
|
233
|
+
createInstanceFromMongoDbNativeDoc<T>(documentArg: any): T;
|
|
234
|
+
name: string;
|
|
235
|
+
prototype: TModel;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const exactCollectionCache = new WeakMap<
|
|
239
|
+
object,
|
|
240
|
+
WeakMap<object, SmartdataExactCollection<any, any>>
|
|
241
|
+
>();
|
|
242
|
+
|
|
243
|
+
const bytesEqual = (leftArg: Uint8Array, rightArg: Uint8Array): boolean => {
|
|
244
|
+
if (leftArg.byteLength !== rightArg.byteLength) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
for (let index = 0; index < leftArg.byteLength; index++) {
|
|
248
|
+
if (leftArg[index] !== rightArg[index]) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const waitWithSignal = async <T>(
|
|
256
|
+
promiseArg: Promise<T>,
|
|
257
|
+
signalArg?: AbortSignal,
|
|
258
|
+
): Promise<T> => {
|
|
259
|
+
if (!signalArg) return promiseArg;
|
|
260
|
+
signalArg.throwIfAborted();
|
|
261
|
+
return new Promise<T>((resolve, reject) => {
|
|
262
|
+
let settled = false;
|
|
263
|
+
const beginSettlement = (): boolean => {
|
|
264
|
+
if (settled) return false;
|
|
265
|
+
settled = true;
|
|
266
|
+
signalArg.removeEventListener('abort', onAbort);
|
|
267
|
+
return true;
|
|
268
|
+
};
|
|
269
|
+
const onAbort = () => {
|
|
270
|
+
if (beginSettlement()) reject(signalArg.reason);
|
|
271
|
+
};
|
|
272
|
+
signalArg.addEventListener('abort', onAbort, { once: true });
|
|
273
|
+
if (signalArg.aborted) onAbort();
|
|
274
|
+
void promiseArg.then(
|
|
275
|
+
(valueArg) => {
|
|
276
|
+
if (beginSettlement()) resolve(valueArg);
|
|
277
|
+
},
|
|
278
|
+
(errorArg) => {
|
|
279
|
+
if (beginSettlement()) reject(errorArg);
|
|
280
|
+
},
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const hasOwn = (documentArg: object, keyArg: PropertyKey): boolean =>
|
|
286
|
+
Object.prototype.hasOwnProperty.call(documentArg, keyArg);
|
|
287
|
+
|
|
288
|
+
const isPlainObject = (
|
|
289
|
+
valueArg: unknown,
|
|
290
|
+
): valueArg is Record<string, unknown> => {
|
|
291
|
+
if (typeof valueArg !== 'object' || valueArg === null) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
295
|
+
return prototype === Object.prototype || prototype === null;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const hasActiveInheritedToBsonEncoder = (
|
|
299
|
+
prototypeArg: object | null,
|
|
300
|
+
): boolean => {
|
|
301
|
+
let prototype = prototypeArg;
|
|
302
|
+
while (prototype) {
|
|
303
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'toBSON');
|
|
304
|
+
if (
|
|
305
|
+
descriptor &&
|
|
306
|
+
(
|
|
307
|
+
!('value' in descriptor) ||
|
|
308
|
+
descriptor.value !== undefined
|
|
309
|
+
)
|
|
310
|
+
) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const maxExactSnapshotArrayLength = 100_000;
|
|
319
|
+
const maxExactSnapshotByteLength = 16 * 1024 * 1024;
|
|
320
|
+
const minSignedInt32 = -(2 ** 31);
|
|
321
|
+
const maxSignedInt32 = 2 ** 31 - 1;
|
|
322
|
+
|
|
323
|
+
const isSignedInt32 = (valueArg: unknown): valueArg is number =>
|
|
324
|
+
typeof valueArg === 'number' &&
|
|
325
|
+
Number.isInteger(valueArg) &&
|
|
326
|
+
!Object.is(valueArg, -0) &&
|
|
327
|
+
valueArg >= minSignedInt32 &&
|
|
328
|
+
valueArg <= maxSignedInt32;
|
|
329
|
+
|
|
330
|
+
const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype);
|
|
331
|
+
const typedArrayBufferGetter = Object.getOwnPropertyDescriptor(
|
|
332
|
+
typedArrayPrototype,
|
|
333
|
+
'buffer',
|
|
334
|
+
)?.get;
|
|
335
|
+
const typedArrayByteOffsetGetter = Object.getOwnPropertyDescriptor(
|
|
336
|
+
typedArrayPrototype,
|
|
337
|
+
'byteOffset',
|
|
338
|
+
)?.get;
|
|
339
|
+
const typedArrayByteLengthGetter = Object.getOwnPropertyDescriptor(
|
|
340
|
+
typedArrayPrototype,
|
|
341
|
+
'byteLength',
|
|
342
|
+
)?.get;
|
|
343
|
+
const regExpSourceGetter = Object.getOwnPropertyDescriptor(
|
|
344
|
+
RegExp.prototype,
|
|
345
|
+
'source',
|
|
346
|
+
)?.get;
|
|
347
|
+
const regExpFlagGetters = [
|
|
348
|
+
['d', 'hasIndices'],
|
|
349
|
+
['g', 'global'],
|
|
350
|
+
['i', 'ignoreCase'],
|
|
351
|
+
['m', 'multiline'],
|
|
352
|
+
['s', 'dotAll'],
|
|
353
|
+
['u', 'unicode'],
|
|
354
|
+
['v', 'unicodeSets'],
|
|
355
|
+
['y', 'sticky'],
|
|
356
|
+
].map(([flag, property]) => ({
|
|
357
|
+
flag,
|
|
358
|
+
getter: Object.getOwnPropertyDescriptor(
|
|
359
|
+
RegExp.prototype,
|
|
360
|
+
property,
|
|
361
|
+
)?.get,
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
const exactSnapshotError = (
|
|
365
|
+
labelArg: string,
|
|
366
|
+
detailArg: string,
|
|
367
|
+
): SmartdataExactPersistenceError =>
|
|
368
|
+
new SmartdataExactPersistenceError(
|
|
369
|
+
'invalid_document',
|
|
370
|
+
`${labelArg} ${detailArg}`,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const constructExactBsonScalar = (
|
|
374
|
+
constructorArg: () => object,
|
|
375
|
+
labelArg: string,
|
|
376
|
+
): object => {
|
|
377
|
+
try {
|
|
378
|
+
return constructorArg();
|
|
379
|
+
} catch (errorArg) {
|
|
380
|
+
if (errorArg instanceof SmartdataExactPersistenceError) {
|
|
381
|
+
throw errorArg;
|
|
382
|
+
}
|
|
383
|
+
throw new SmartdataExactPersistenceError(
|
|
384
|
+
'invalid_document',
|
|
385
|
+
`${labelArg} contains malformed BSON scalar data.`,
|
|
386
|
+
{
|
|
387
|
+
cause: errorArg,
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const getOwnDataValue = (
|
|
394
|
+
valueArg: object,
|
|
395
|
+
keyArg: string,
|
|
396
|
+
labelArg: string,
|
|
397
|
+
): unknown => {
|
|
398
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, keyArg);
|
|
399
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
400
|
+
throw exactSnapshotError(
|
|
401
|
+
labelArg,
|
|
402
|
+
`contains a non-inert "${keyArg}" property.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return descriptor.value;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const getExactSnapshotBytes = (
|
|
409
|
+
valueArg: unknown,
|
|
410
|
+
labelArg: string,
|
|
411
|
+
): Uint8Array => {
|
|
412
|
+
if (
|
|
413
|
+
typeof valueArg !== 'object' ||
|
|
414
|
+
valueArg === null ||
|
|
415
|
+
plugins.nodeUtil.types.isProxy(valueArg) ||
|
|
416
|
+
!ArrayBuffer.isView(valueArg) ||
|
|
417
|
+
!typedArrayBufferGetter ||
|
|
418
|
+
!typedArrayByteOffsetGetter ||
|
|
419
|
+
!typedArrayByteLengthGetter
|
|
420
|
+
) {
|
|
421
|
+
throw exactSnapshotError(labelArg, 'contains invalid byte data.');
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const buffer = Reflect.apply(typedArrayBufferGetter, valueArg, []);
|
|
425
|
+
const byteOffset = Reflect.apply(typedArrayByteOffsetGetter, valueArg, []);
|
|
426
|
+
const byteLength = Reflect.apply(typedArrayByteLengthGetter, valueArg, []);
|
|
427
|
+
if (
|
|
428
|
+
!Number.isSafeInteger(byteOffset) ||
|
|
429
|
+
!Number.isSafeInteger(byteLength) ||
|
|
430
|
+
byteLength < 0 ||
|
|
431
|
+
byteLength > maxExactSnapshotByteLength
|
|
432
|
+
) {
|
|
433
|
+
throw exactSnapshotError(labelArg, 'contains oversized byte data.');
|
|
434
|
+
}
|
|
435
|
+
return Uint8Array.from(
|
|
436
|
+
new Uint8Array(buffer, byteOffset, byteLength),
|
|
437
|
+
);
|
|
438
|
+
} catch (errorArg) {
|
|
439
|
+
if (errorArg instanceof SmartdataExactPersistenceError) {
|
|
440
|
+
throw errorArg;
|
|
441
|
+
}
|
|
442
|
+
throw exactSnapshotError(labelArg, 'contains invalid byte data.');
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const markInertBsonSnapshot = <TValue extends object>(
|
|
447
|
+
valueArg: TValue,
|
|
448
|
+
): TValue => {
|
|
449
|
+
Object.defineProperty(valueArg, 'toBSON', {
|
|
450
|
+
value: undefined,
|
|
451
|
+
enumerable: false,
|
|
452
|
+
});
|
|
453
|
+
return valueArg;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const snapshotInertExactValue = (
|
|
457
|
+
valueArg: unknown,
|
|
458
|
+
labelArg: string,
|
|
459
|
+
seenArg = new WeakSet<object>(),
|
|
460
|
+
depthArg = 0,
|
|
461
|
+
): unknown => {
|
|
462
|
+
if (typeof valueArg === 'function' || typeof valueArg === 'symbol') {
|
|
463
|
+
throw exactSnapshotError(
|
|
464
|
+
labelArg,
|
|
465
|
+
'contains a value that cannot be inert BSON data.',
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (typeof valueArg !== 'object' || valueArg === null) {
|
|
469
|
+
return valueArg;
|
|
470
|
+
}
|
|
471
|
+
if (plugins.nodeUtil.types.isProxy(valueArg)) {
|
|
472
|
+
throw exactSnapshotError(labelArg, 'may not contain Proxy objects.');
|
|
473
|
+
}
|
|
474
|
+
if (seenArg.has(valueArg)) {
|
|
475
|
+
throw exactSnapshotError(labelArg, 'may not contain circular data.');
|
|
476
|
+
}
|
|
477
|
+
if (depthArg >= 100) {
|
|
478
|
+
throw exactSnapshotError(
|
|
479
|
+
labelArg,
|
|
480
|
+
'exceeds the maximum supported nesting depth.',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
seenArg.add(valueArg);
|
|
484
|
+
try {
|
|
485
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
486
|
+
if (Array.isArray(valueArg)) {
|
|
487
|
+
if (
|
|
488
|
+
prototype !== Array.prototype ||
|
|
489
|
+
valueArg.length > maxExactSnapshotArrayLength
|
|
490
|
+
) {
|
|
491
|
+
throw exactSnapshotError(
|
|
492
|
+
labelArg,
|
|
493
|
+
'must be a bounded ordinary array.',
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (hasActiveInheritedToBsonEncoder(prototype)) {
|
|
497
|
+
throw exactSnapshotError(
|
|
498
|
+
labelArg,
|
|
499
|
+
'may not inherit a custom BSON encoder.',
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
503
|
+
const toBsonDescriptor = Object.getOwnPropertyDescriptor(
|
|
504
|
+
valueArg,
|
|
505
|
+
'toBSON',
|
|
506
|
+
);
|
|
507
|
+
if (
|
|
508
|
+
toBsonDescriptor &&
|
|
509
|
+
(
|
|
510
|
+
!('value' in toBsonDescriptor) ||
|
|
511
|
+
toBsonDescriptor.value !== undefined ||
|
|
512
|
+
toBsonDescriptor.enumerable === true
|
|
513
|
+
)
|
|
514
|
+
) {
|
|
515
|
+
throw exactSnapshotError(
|
|
516
|
+
labelArg,
|
|
517
|
+
'arrays may contain only an inert non-enumerable toBSON marker.',
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const indexedKeys = ownKeys.filter(
|
|
521
|
+
(keyArg) => keyArg !== 'length' && keyArg !== 'toBSON',
|
|
522
|
+
);
|
|
523
|
+
if (
|
|
524
|
+
indexedKeys.length !== valueArg.length ||
|
|
525
|
+
indexedKeys.some((keyArg) => {
|
|
526
|
+
if (
|
|
527
|
+
typeof keyArg !== 'string' ||
|
|
528
|
+
!/^(0|[1-9]\d*)$/.test(keyArg)
|
|
529
|
+
) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
const index = Number(keyArg);
|
|
533
|
+
return !Number.isSafeInteger(index) || index >= valueArg.length;
|
|
534
|
+
})
|
|
535
|
+
) {
|
|
536
|
+
throw exactSnapshotError(
|
|
537
|
+
labelArg,
|
|
538
|
+
'arrays must be dense and contain only indexed data properties.',
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const snapshot: unknown[] = [];
|
|
542
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
543
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
544
|
+
valueArg,
|
|
545
|
+
String(index),
|
|
546
|
+
);
|
|
547
|
+
if (
|
|
548
|
+
!descriptor ||
|
|
549
|
+
!('value' in descriptor) ||
|
|
550
|
+
descriptor.enumerable !== true
|
|
551
|
+
) {
|
|
552
|
+
throw exactSnapshotError(
|
|
553
|
+
labelArg,
|
|
554
|
+
'arrays may contain only inert enumerable data properties.',
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
snapshot.push(
|
|
558
|
+
snapshotInertExactValue(
|
|
559
|
+
descriptor.value,
|
|
560
|
+
`${labelArg}.${index}`,
|
|
561
|
+
seenArg,
|
|
562
|
+
depthArg + 1,
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
return snapshot;
|
|
567
|
+
}
|
|
568
|
+
if (isPlainObject(valueArg)) {
|
|
569
|
+
const snapshot = Object.create(null) as Record<string, unknown>;
|
|
570
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
571
|
+
if (
|
|
572
|
+
typeof key !== 'string' ||
|
|
573
|
+
key === 'toBSON' ||
|
|
574
|
+
key === '_bsontype'
|
|
575
|
+
) {
|
|
576
|
+
throw exactSnapshotError(
|
|
577
|
+
labelArg,
|
|
578
|
+
'may contain only ordinary string-keyed data.',
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
582
|
+
if (
|
|
583
|
+
!descriptor ||
|
|
584
|
+
!('value' in descriptor) ||
|
|
585
|
+
descriptor.enumerable !== true
|
|
586
|
+
) {
|
|
587
|
+
throw exactSnapshotError(
|
|
588
|
+
labelArg,
|
|
589
|
+
'may contain only inert enumerable data properties.',
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
snapshot[key] = snapshotInertExactValue(
|
|
593
|
+
descriptor.value,
|
|
594
|
+
`${labelArg}.${key}`,
|
|
595
|
+
seenArg,
|
|
596
|
+
depthArg + 1,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return snapshot;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (hasActiveInheritedToBsonEncoder(prototype)) {
|
|
603
|
+
throw exactSnapshotError(
|
|
604
|
+
labelArg,
|
|
605
|
+
'may not inherit a custom BSON encoder.',
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (prototype === Date.prototype) {
|
|
610
|
+
if (Reflect.ownKeys(valueArg).length !== 0) {
|
|
611
|
+
throw exactSnapshotError(labelArg, 'contains a customized Date.');
|
|
612
|
+
}
|
|
613
|
+
return markInertBsonSnapshot(
|
|
614
|
+
new Date(Reflect.apply(Date.prototype.getTime, valueArg, [])),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (prototype === RegExp.prototype) {
|
|
618
|
+
const ownKeys = Reflect.ownKeys(valueArg);
|
|
619
|
+
if (
|
|
620
|
+
ownKeys.length !== 1 ||
|
|
621
|
+
ownKeys[0] !== 'lastIndex' ||
|
|
622
|
+
!regExpSourceGetter
|
|
623
|
+
) {
|
|
624
|
+
throw exactSnapshotError(
|
|
625
|
+
labelArg,
|
|
626
|
+
'contains a customized regular expression.',
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const source = Reflect.apply(regExpSourceGetter, valueArg, []);
|
|
630
|
+
const flags = regExpFlagGetters
|
|
631
|
+
.filter(({ getter }) => getter && Reflect.apply(getter, valueArg, []))
|
|
632
|
+
.map(({ flag }) => flag)
|
|
633
|
+
.join('');
|
|
634
|
+
return markInertBsonSnapshot(new RegExp(source, flags));
|
|
635
|
+
}
|
|
636
|
+
if (prototype === Uint8Array.prototype) {
|
|
637
|
+
return markInertBsonSnapshot(
|
|
638
|
+
getExactSnapshotBytes(valueArg, labelArg),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const scalarToBsonDescriptor = Object.getOwnPropertyDescriptor(
|
|
643
|
+
valueArg,
|
|
644
|
+
'toBSON',
|
|
645
|
+
);
|
|
646
|
+
if (
|
|
647
|
+
scalarToBsonDescriptor &&
|
|
648
|
+
(
|
|
649
|
+
!('value' in scalarToBsonDescriptor) ||
|
|
650
|
+
scalarToBsonDescriptor.value !== undefined ||
|
|
651
|
+
scalarToBsonDescriptor.enumerable === true
|
|
652
|
+
)
|
|
653
|
+
) {
|
|
654
|
+
throw exactSnapshotError(
|
|
655
|
+
labelArg,
|
|
656
|
+
'contains a customized BSON scalar encoder.',
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let scalarSnapshot: object;
|
|
661
|
+
if (prototype === plugins.mongodb.ObjectId.prototype) {
|
|
662
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
663
|
+
() => new plugins.mongodb.ObjectId(getInertObjectIdBytes(valueArg, () =>
|
|
664
|
+
exactSnapshotError(labelArg, 'contains a malformed or non-inert ObjectId.'))),
|
|
665
|
+
labelArg,
|
|
666
|
+
);
|
|
667
|
+
} else if (prototype === plugins.mongodb.UUID.prototype) {
|
|
668
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
669
|
+
() => new plugins.mongodb.UUID(
|
|
670
|
+
getExactSnapshotBytes(
|
|
671
|
+
getOwnDataValue(valueArg, 'buffer', labelArg),
|
|
672
|
+
labelArg,
|
|
673
|
+
),
|
|
674
|
+
),
|
|
675
|
+
labelArg,
|
|
676
|
+
);
|
|
677
|
+
} else if (prototype === plugins.mongodb.Binary.prototype) {
|
|
678
|
+
const buffer = getExactSnapshotBytes(
|
|
679
|
+
getOwnDataValue(valueArg, 'buffer', labelArg),
|
|
680
|
+
labelArg,
|
|
681
|
+
);
|
|
682
|
+
const subType = getOwnDataValue(valueArg, 'sub_type', labelArg);
|
|
683
|
+
const position = getOwnDataValue(valueArg, 'position', labelArg);
|
|
684
|
+
if (
|
|
685
|
+
typeof subType !== 'number' ||
|
|
686
|
+
!Number.isSafeInteger(subType) ||
|
|
687
|
+
typeof position !== 'number' ||
|
|
688
|
+
!Number.isSafeInteger(position) ||
|
|
689
|
+
position < 0 ||
|
|
690
|
+
position > buffer.length
|
|
691
|
+
) {
|
|
692
|
+
throw exactSnapshotError(labelArg, 'contains invalid binary data.');
|
|
693
|
+
}
|
|
694
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
695
|
+
() => new plugins.mongodb.Binary(
|
|
696
|
+
buffer.subarray(0, position),
|
|
697
|
+
subType,
|
|
698
|
+
),
|
|
699
|
+
labelArg,
|
|
700
|
+
);
|
|
701
|
+
} else if (prototype === plugins.mongodb.Decimal128.prototype) {
|
|
702
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
703
|
+
() => new plugins.mongodb.Decimal128(
|
|
704
|
+
getExactSnapshotBytes(
|
|
705
|
+
getOwnDataValue(valueArg, 'bytes', labelArg),
|
|
706
|
+
labelArg,
|
|
707
|
+
),
|
|
708
|
+
),
|
|
709
|
+
labelArg,
|
|
710
|
+
);
|
|
711
|
+
} else if (prototype === plugins.mongodb.Int32.prototype) {
|
|
712
|
+
const value = getOwnDataValue(valueArg, 'value', labelArg);
|
|
713
|
+
if (!isSignedInt32(value)) {
|
|
714
|
+
throw exactSnapshotError(labelArg, 'contains an invalid Int32.');
|
|
715
|
+
}
|
|
716
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
717
|
+
() => new plugins.mongodb.Int32(value),
|
|
718
|
+
labelArg,
|
|
719
|
+
);
|
|
720
|
+
} else if (prototype === plugins.mongodb.Double.prototype) {
|
|
721
|
+
const value = getOwnDataValue(valueArg, 'value', labelArg);
|
|
722
|
+
if (typeof value !== 'number') {
|
|
723
|
+
throw exactSnapshotError(labelArg, 'contains an invalid Double.');
|
|
724
|
+
}
|
|
725
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
726
|
+
() => new plugins.mongodb.Double(value),
|
|
727
|
+
labelArg,
|
|
728
|
+
);
|
|
729
|
+
} else if (prototype === plugins.mongodb.Long.prototype) {
|
|
730
|
+
const low = getOwnDataValue(valueArg, 'low', labelArg);
|
|
731
|
+
const high = getOwnDataValue(valueArg, 'high', labelArg);
|
|
732
|
+
const unsigned = getOwnDataValue(valueArg, 'unsigned', labelArg);
|
|
733
|
+
if (
|
|
734
|
+
!isSignedInt32(low) ||
|
|
735
|
+
!isSignedInt32(high) ||
|
|
736
|
+
typeof unsigned !== 'boolean'
|
|
737
|
+
) {
|
|
738
|
+
throw exactSnapshotError(labelArg, 'contains an invalid 64-bit value.');
|
|
739
|
+
}
|
|
740
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
741
|
+
() => new plugins.mongodb.Long(low, high, unsigned),
|
|
742
|
+
labelArg,
|
|
743
|
+
);
|
|
744
|
+
} else if (prototype === plugins.mongodb.Timestamp.prototype) {
|
|
745
|
+
const low = getOwnDataValue(valueArg, 'low', labelArg);
|
|
746
|
+
const high = getOwnDataValue(valueArg, 'high', labelArg);
|
|
747
|
+
const unsigned = getOwnDataValue(valueArg, 'unsigned', labelArg);
|
|
748
|
+
if (!isSignedInt32(low) || !isSignedInt32(high) || unsigned !== true) {
|
|
749
|
+
throw exactSnapshotError(labelArg, 'contains an invalid Timestamp.');
|
|
750
|
+
}
|
|
751
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
752
|
+
() => new plugins.mongodb.Timestamp(
|
|
753
|
+
new plugins.mongodb.Long(low, high, true),
|
|
754
|
+
),
|
|
755
|
+
labelArg,
|
|
756
|
+
);
|
|
757
|
+
} else if (prototype === plugins.mongodb.BSONRegExp.prototype) {
|
|
758
|
+
const pattern = getOwnDataValue(valueArg, 'pattern', labelArg);
|
|
759
|
+
const options = getOwnDataValue(valueArg, 'options', labelArg);
|
|
760
|
+
if (typeof pattern !== 'string' || typeof options !== 'string') {
|
|
761
|
+
throw exactSnapshotError(labelArg, 'contains an invalid BSON regex.');
|
|
762
|
+
}
|
|
763
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
764
|
+
() => new plugins.mongodb.BSONRegExp(pattern, options),
|
|
765
|
+
labelArg,
|
|
766
|
+
);
|
|
767
|
+
} else if (prototype === plugins.mongodb.BSONSymbol.prototype) {
|
|
768
|
+
const value = getOwnDataValue(valueArg, 'value', labelArg);
|
|
769
|
+
if (typeof value !== 'string') {
|
|
770
|
+
throw exactSnapshotError(labelArg, 'contains an invalid BSON symbol.');
|
|
771
|
+
}
|
|
772
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
773
|
+
() => new plugins.mongodb.BSONSymbol(value),
|
|
774
|
+
labelArg,
|
|
775
|
+
);
|
|
776
|
+
} else if (prototype === plugins.mongodb.Code.prototype) {
|
|
777
|
+
const code = getOwnDataValue(valueArg, 'code', labelArg);
|
|
778
|
+
const scope = getOwnDataValue(valueArg, 'scope', labelArg);
|
|
779
|
+
if (typeof code !== 'string') {
|
|
780
|
+
throw exactSnapshotError(labelArg, 'contains invalid BSON code.');
|
|
781
|
+
}
|
|
782
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
783
|
+
() => new plugins.mongodb.Code(
|
|
784
|
+
code,
|
|
785
|
+
scope === null
|
|
786
|
+
? null
|
|
787
|
+
: snapshotInertExactValue(
|
|
788
|
+
scope,
|
|
789
|
+
`${labelArg}.scope`,
|
|
790
|
+
seenArg,
|
|
791
|
+
depthArg + 1,
|
|
792
|
+
) as plugins.mongodb.Document,
|
|
793
|
+
),
|
|
794
|
+
labelArg,
|
|
795
|
+
);
|
|
796
|
+
} else if (prototype === plugins.mongodb.DBRef.prototype) {
|
|
797
|
+
const collection = getOwnDataValue(valueArg, 'collection', labelArg);
|
|
798
|
+
const oid = snapshotInertExactValue(
|
|
799
|
+
getOwnDataValue(valueArg, 'oid', labelArg),
|
|
800
|
+
`${labelArg}.oid`,
|
|
801
|
+
seenArg,
|
|
802
|
+
depthArg + 1,
|
|
803
|
+
);
|
|
804
|
+
const db = getOwnDataValue(valueArg, 'db', labelArg);
|
|
805
|
+
const fields = snapshotInertExactValue(
|
|
806
|
+
getOwnDataValue(valueArg, 'fields', labelArg),
|
|
807
|
+
`${labelArg}.fields`,
|
|
808
|
+
seenArg,
|
|
809
|
+
depthArg + 1,
|
|
810
|
+
);
|
|
811
|
+
if (
|
|
812
|
+
typeof collection !== 'string' ||
|
|
813
|
+
!(oid instanceof plugins.mongodb.ObjectId) ||
|
|
814
|
+
(db !== undefined && typeof db !== 'string') ||
|
|
815
|
+
!isPlainObject(fields)
|
|
816
|
+
) {
|
|
817
|
+
throw exactSnapshotError(labelArg, 'contains an invalid DBRef.');
|
|
818
|
+
}
|
|
819
|
+
scalarSnapshot = constructExactBsonScalar(
|
|
820
|
+
() => new plugins.mongodb.DBRef(
|
|
821
|
+
collection,
|
|
822
|
+
oid,
|
|
823
|
+
db,
|
|
824
|
+
fields,
|
|
825
|
+
),
|
|
826
|
+
labelArg,
|
|
827
|
+
);
|
|
828
|
+
} else if (prototype === plugins.mongodb.MinKey.prototype) {
|
|
829
|
+
scalarSnapshot = new plugins.mongodb.MinKey();
|
|
830
|
+
} else if (prototype === plugins.mongodb.MaxKey.prototype) {
|
|
831
|
+
scalarSnapshot = new plugins.mongodb.MaxKey();
|
|
832
|
+
} else {
|
|
833
|
+
throw exactSnapshotError(
|
|
834
|
+
labelArg,
|
|
835
|
+
'contains an object with a custom BSON encoding boundary.',
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return markInertBsonSnapshot(scalarSnapshot);
|
|
839
|
+
} finally {
|
|
840
|
+
seenArg.delete(valueArg);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const canonicalizeValue = <TValue>(
|
|
845
|
+
valueArg: TValue,
|
|
846
|
+
seenArg = new WeakSet<object>(),
|
|
847
|
+
): TValue => {
|
|
848
|
+
if (typeof valueArg !== 'object' || valueArg === null) {
|
|
849
|
+
return valueArg;
|
|
850
|
+
}
|
|
851
|
+
const isArray = Array.isArray(valueArg);
|
|
852
|
+
if (!isArray && !isPlainObject(valueArg)) {
|
|
853
|
+
return valueArg;
|
|
854
|
+
}
|
|
855
|
+
if (seenArg.has(valueArg)) {
|
|
856
|
+
return valueArg;
|
|
857
|
+
}
|
|
858
|
+
seenArg.add(valueArg);
|
|
859
|
+
if (isArray) {
|
|
860
|
+
const canonicalArray = (valueArg as unknown[]).map((entryArg) =>
|
|
861
|
+
canonicalizeValue(entryArg, seenArg),
|
|
862
|
+
) as TValue;
|
|
863
|
+
seenArg.delete(valueArg);
|
|
864
|
+
return canonicalArray;
|
|
865
|
+
}
|
|
866
|
+
const canonical = Object.create(null) as Record<string, unknown>;
|
|
867
|
+
for (const key of Object.keys(valueArg).sort()) {
|
|
868
|
+
const entry = valueArg[key];
|
|
869
|
+
if (entry !== undefined) {
|
|
870
|
+
canonical[key] = canonicalizeValue(entry, seenArg);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
seenArg.delete(valueArg);
|
|
874
|
+
return canonical as TValue;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const isDuplicateKeyError = (errorArg: unknown): boolean => {
|
|
878
|
+
const mongoError = errorArg as { code?: number; codeName?: string } | undefined;
|
|
879
|
+
return mongoError?.code === 11000 || mongoError?.codeName === 'DuplicateKey';
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const isTransientTransactionError = (
|
|
883
|
+
errorArg: unknown,
|
|
884
|
+
sessionArg?: plugins.mongodb.ClientSession,
|
|
885
|
+
): boolean =>
|
|
886
|
+
Boolean(sessionArg)
|
|
887
|
+
&& typeof (errorArg as { hasErrorLabel?: unknown })?.hasErrorLabel === 'function'
|
|
888
|
+
&& (errorArg as { hasErrorLabel: (labelArg: string) => boolean })
|
|
889
|
+
.hasErrorLabel('TransientTransactionError');
|
|
890
|
+
|
|
891
|
+
const hasActiveTransaction = (
|
|
892
|
+
sessionArg?: plugins.mongodb.ClientSession,
|
|
893
|
+
): boolean => Boolean(sessionArg?.inTransaction());
|
|
894
|
+
|
|
895
|
+
const unsupportedExactSave = async (): Promise<never> => {
|
|
896
|
+
throw new SmartdataExactPersistenceError(
|
|
897
|
+
'unsupported_operation',
|
|
898
|
+
'save() is unavailable for exact-persistence models. Use the model exact API.',
|
|
899
|
+
);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const forbiddenExactDelete = async (): Promise<never> => {
|
|
903
|
+
throw new SmartdataExactPersistenceError(
|
|
904
|
+
'unsupported_operation',
|
|
905
|
+
'delete() is forbidden for exact-persistence models.',
|
|
906
|
+
);
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
export class SmartdataExactCollection<
|
|
910
|
+
TModel extends SmartDataDbDoc<any, any, any>,
|
|
911
|
+
TPersisted extends object,
|
|
912
|
+
> {
|
|
913
|
+
private initializationPromise?: Promise<void>;
|
|
914
|
+
private initializedCollection?: plugins.mongodb.Collection;
|
|
915
|
+
private initializationTarget?: plugins.mongodb.Collection;
|
|
916
|
+
private initializationGeneration = 0;
|
|
917
|
+
private readonly allowedPersistedKeys: ReadonlySet<string>;
|
|
918
|
+
private readonly uniqueIndexes: readonly string[];
|
|
919
|
+
private readonly reconcileBy: string;
|
|
920
|
+
|
|
921
|
+
constructor(
|
|
922
|
+
private readonly modelConstructor: IExactModelConstructor<TModel>,
|
|
923
|
+
private readonly collection: SmartdataCollection<any>,
|
|
924
|
+
private readonly options: IExactPersistenceOptions<TPersisted>,
|
|
925
|
+
) {
|
|
926
|
+
// Field-decorator initializers materialize the model metadata on its
|
|
927
|
+
// prototype. Exact persistence uses that metadata as the ownership boundary.
|
|
928
|
+
new this.modelConstructor();
|
|
929
|
+
const prototype = this.modelConstructor.prototype;
|
|
930
|
+
this.allowedPersistedKeys = new Set(
|
|
931
|
+
[
|
|
932
|
+
...(prototype.globalSaveableProperties || []),
|
|
933
|
+
...(prototype.saveableProperties || []),
|
|
934
|
+
].filter((keyArg) => !reservedKeys.includes(keyArg as TReservedKey)),
|
|
935
|
+
);
|
|
936
|
+
this.uniqueIndexes = [
|
|
937
|
+
...new Set(prototype.uniqueIndexes || []),
|
|
938
|
+
];
|
|
939
|
+
const identityValueTypes = prototype.identityValueTypes as
|
|
940
|
+
| Record<string, TSmartdataIdentityValueType>
|
|
941
|
+
| undefined;
|
|
942
|
+
const numericIdentityField = this.uniqueIndexes.find(
|
|
943
|
+
(fieldArg) => identityValueTypes?.[fieldArg] === 'positiveSafeInteger',
|
|
944
|
+
);
|
|
945
|
+
if (numericIdentityField) {
|
|
946
|
+
throw new SmartdataExactPersistenceError(
|
|
947
|
+
'invalid_configuration',
|
|
948
|
+
`Exact persistence identity field "${numericIdentityField}" must use the default string valueType.`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const explicitReconcileBy = options.reconcileBy as string | undefined;
|
|
953
|
+
if (explicitReconcileBy) {
|
|
954
|
+
if (
|
|
955
|
+
!this.allowedPersistedKeys.has(explicitReconcileBy) ||
|
|
956
|
+
!this.uniqueIndexes.includes(explicitReconcileBy)
|
|
957
|
+
) {
|
|
958
|
+
throw new SmartdataExactPersistenceError(
|
|
959
|
+
'invalid_configuration',
|
|
960
|
+
`Exact persistence reconcileBy field "${explicitReconcileBy}" must be a persisted field decorated with @unI().`,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
this.reconcileBy = explicitReconcileBy;
|
|
964
|
+
} else if (this.uniqueIndexes.length === 1) {
|
|
965
|
+
this.reconcileBy = this.uniqueIndexes[0];
|
|
966
|
+
} else if (this.uniqueIndexes.length === 0) {
|
|
967
|
+
throw new SmartdataExactPersistenceError(
|
|
968
|
+
'invalid_configuration',
|
|
969
|
+
'Exact persistence requires at least one persisted field decorated with @unI().',
|
|
970
|
+
);
|
|
971
|
+
} else {
|
|
972
|
+
throw new SmartdataExactPersistenceError(
|
|
973
|
+
'invalid_configuration',
|
|
974
|
+
`Exact persistence requires reconcileBy when the model has multiple @unI() fields.`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
registerCollectionReconnectHandler(this.collection, () => {
|
|
978
|
+
this.initializationGeneration++;
|
|
979
|
+
this.initializationPromise = undefined;
|
|
980
|
+
this.initializedCollection = undefined;
|
|
981
|
+
this.initializationTarget = undefined;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private async initialize(): Promise<void> {
|
|
986
|
+
await this.collection.init();
|
|
987
|
+
if (this.initializedCollection === this.collection.mongoDbCollection) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const targetCollection = this.collection.mongoDbCollection;
|
|
991
|
+
if (this.initializationTarget !== targetCollection) {
|
|
992
|
+
this.initializationTarget = targetCollection;
|
|
993
|
+
this.initializationPromise = undefined;
|
|
994
|
+
this.initializationGeneration++;
|
|
995
|
+
}
|
|
996
|
+
if (!this.initializationPromise) {
|
|
997
|
+
const generation = this.initializationGeneration;
|
|
998
|
+
const initializationPromise = this.initializeOnce(targetCollection)
|
|
999
|
+
.then(() => {
|
|
1000
|
+
if (
|
|
1001
|
+
this.initializationGeneration === generation
|
|
1002
|
+
&& this.collection.mongoDbCollection === targetCollection
|
|
1003
|
+
) {
|
|
1004
|
+
this.initializedCollection = targetCollection;
|
|
1005
|
+
}
|
|
1006
|
+
})
|
|
1007
|
+
.catch((errorArg) => {
|
|
1008
|
+
if (this.initializationPromise === initializationPromise) {
|
|
1009
|
+
this.initializationPromise = undefined;
|
|
1010
|
+
}
|
|
1011
|
+
throw errorArg;
|
|
1012
|
+
});
|
|
1013
|
+
this.initializationPromise = initializationPromise;
|
|
1014
|
+
}
|
|
1015
|
+
await this.initializationPromise;
|
|
1016
|
+
if (this.initializedCollection !== this.collection.mongoDbCollection) {
|
|
1017
|
+
await this.initialize();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private async initializeOnce(
|
|
1022
|
+
targetCollectionArg: plugins.mongodb.Collection,
|
|
1023
|
+
): Promise<void> {
|
|
1024
|
+
const missingUniqueIndexes = this.uniqueIndexes.filter(
|
|
1025
|
+
(uniqueIndexArg) => !this.collection.uniqueIndexes.includes(uniqueIndexArg),
|
|
1026
|
+
);
|
|
1027
|
+
for (const uniqueIndex of missingUniqueIndexes) {
|
|
1028
|
+
await targetCollectionArg.createIndex(
|
|
1029
|
+
{ [uniqueIndex]: 1 },
|
|
1030
|
+
{ unique: true, name: `${uniqueIndex}_1` },
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const regularIndexes = this.modelConstructor.prototype.regularIndexes || [];
|
|
1035
|
+
const missingRegularIndexes = regularIndexes.filter((regularIndexArg) =>
|
|
1036
|
+
!this.collection.regularIndexes.some(
|
|
1037
|
+
(existingIndexArg) => existingIndexArg.field === regularIndexArg.field,
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
for (const regularIndex of missingRegularIndexes) {
|
|
1041
|
+
await targetCollectionArg.createIndex(
|
|
1042
|
+
{ [regularIndex.field]: 1 },
|
|
1043
|
+
regularIndex.options,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
if (this.collection.mongoDbCollection !== targetCollectionArg) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
for (const uniqueIndex of missingUniqueIndexes) {
|
|
1050
|
+
if (!this.collection.uniqueIndexes.includes(uniqueIndex)) {
|
|
1051
|
+
this.collection.uniqueIndexes.push(uniqueIndex);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
for (const regularIndex of missingRegularIndexes) {
|
|
1055
|
+
if (!this.collection.regularIndexes.some(
|
|
1056
|
+
(existingIndexArg) => existingIndexArg.field === regularIndex.field,
|
|
1057
|
+
)) {
|
|
1058
|
+
this.collection.regularIndexes.push(regularIndex);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
public async prepare(): Promise<void> {
|
|
1064
|
+
await this.initialize();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private async beginOperation(
|
|
1068
|
+
optionsArg: IExactSessionOptions,
|
|
1069
|
+
signalArg?: AbortSignal,
|
|
1070
|
+
): Promise<{
|
|
1071
|
+
session?: plugins.mongodb.ClientSession;
|
|
1072
|
+
release: () => void;
|
|
1073
|
+
}> {
|
|
1074
|
+
signalArg?.throwIfAborted();
|
|
1075
|
+
if (!optionsArg.session) {
|
|
1076
|
+
await waitWithSignal(this.initialize(), signalArg);
|
|
1077
|
+
signalArg?.throwIfAborted();
|
|
1078
|
+
return { release: () => {} };
|
|
1079
|
+
}
|
|
1080
|
+
if (
|
|
1081
|
+
!this.collection.isInitializedForCurrentDatabase()
|
|
1082
|
+
|| this.initializedCollection !== this.collection.mongoDbCollection
|
|
1083
|
+
) {
|
|
1084
|
+
throw new SmartdataExactPersistenceError(
|
|
1085
|
+
'unsupported_operation',
|
|
1086
|
+
'Exact collection must be prepared before it participates in a transaction.',
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
const lease = acquireSmartdataSession(
|
|
1090
|
+
optionsArg.session,
|
|
1091
|
+
this.collection.smartdataDb,
|
|
1092
|
+
);
|
|
1093
|
+
if (signalArg?.aborted) {
|
|
1094
|
+
lease.release();
|
|
1095
|
+
signalArg.throwIfAborted();
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
session: lease.rawSession,
|
|
1099
|
+
release: lease.release,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private async runOperation<TResult>(
|
|
1104
|
+
optionsArg: IExactSessionOptions,
|
|
1105
|
+
operationArg: (
|
|
1106
|
+
sessionArg?: plugins.mongodb.ClientSession,
|
|
1107
|
+
) => Promise<TResult>,
|
|
1108
|
+
signalArg?: AbortSignal,
|
|
1109
|
+
): Promise<TResult> {
|
|
1110
|
+
const operation = await this.beginOperation(optionsArg, signalArg);
|
|
1111
|
+
try {
|
|
1112
|
+
signalArg?.throwIfAborted();
|
|
1113
|
+
return await operationArg(operation.session);
|
|
1114
|
+
} finally {
|
|
1115
|
+
operation.release();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private canonicalizeDocument<TDocument extends object>(
|
|
1120
|
+
documentArg: TDocument,
|
|
1121
|
+
labelArg: string,
|
|
1122
|
+
): TDocument {
|
|
1123
|
+
let canonicalInput: TDocument;
|
|
1124
|
+
let serialized: Uint8Array;
|
|
1125
|
+
let canonical: TDocument;
|
|
1126
|
+
let reserialized: Uint8Array;
|
|
1127
|
+
try {
|
|
1128
|
+
canonicalInput = canonicalizeValue(documentArg);
|
|
1129
|
+
serialized = plugins.mongodb.BSON.serialize(canonicalInput);
|
|
1130
|
+
canonical = canonicalizeValue(
|
|
1131
|
+
plugins.mongodb.BSON.deserialize(serialized) as TDocument,
|
|
1132
|
+
);
|
|
1133
|
+
reserialized = plugins.mongodb.BSON.serialize(canonical);
|
|
1134
|
+
} catch (errorArg) {
|
|
1135
|
+
throw new SmartdataExactPersistenceError(
|
|
1136
|
+
'invalid_document',
|
|
1137
|
+
`${labelArg} is not BSON serializable.`,
|
|
1138
|
+
{ cause: errorArg },
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (!bytesEqual(serialized, reserialized)) {
|
|
1142
|
+
throw new SmartdataExactPersistenceError(
|
|
1143
|
+
'noncanonical_document',
|
|
1144
|
+
`${labelArg} is not byte-stable across a BSON roundtrip.`,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return canonical;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
private assertAllowedTopLevelKeys(
|
|
1151
|
+
documentArg: object,
|
|
1152
|
+
allowedMetadataKeysArg: readonly TStorageMetadataKey[],
|
|
1153
|
+
labelArg: string,
|
|
1154
|
+
): void {
|
|
1155
|
+
const allowedMetadataKeys = new Set<string>(allowedMetadataKeysArg);
|
|
1156
|
+
for (const key of Object.keys(documentArg)) {
|
|
1157
|
+
if (
|
|
1158
|
+
reservedKeys.includes(key as TReservedKey) &&
|
|
1159
|
+
!allowedMetadataKeys.has(key)
|
|
1160
|
+
) {
|
|
1161
|
+
throw new SmartdataExactPersistenceError(
|
|
1162
|
+
'reserved_field',
|
|
1163
|
+
`${labelArg} may not define reserved field "${key}".`,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
if (
|
|
1167
|
+
!this.allowedPersistedKeys.has(key) &&
|
|
1168
|
+
!allowedMetadataKeys.has(key)
|
|
1169
|
+
) {
|
|
1170
|
+
throw new SmartdataExactPersistenceError(
|
|
1171
|
+
'invalid_document',
|
|
1172
|
+
`${labelArg} defines undeclared top-level field "${key}".`,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private canonicalizePersistedDocument(
|
|
1179
|
+
documentArg: object,
|
|
1180
|
+
): TExactPersistedDocument<TPersisted> {
|
|
1181
|
+
this.assertAllowedTopLevelKeys(documentArg, [], 'Persisted document');
|
|
1182
|
+
const canonical = this.canonicalizeDocument(
|
|
1183
|
+
documentArg,
|
|
1184
|
+
'Persisted document',
|
|
1185
|
+
);
|
|
1186
|
+
this.assertWithoutMutation(canonical, 'Persisted document');
|
|
1187
|
+
return canonical as TExactPersistedDocument<TPersisted>;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private canonicalizeStoredDocument(
|
|
1191
|
+
documentArg: object,
|
|
1192
|
+
): TStoredDocument<TPersisted> {
|
|
1193
|
+
this.assertAllowedTopLevelKeys(
|
|
1194
|
+
documentArg,
|
|
1195
|
+
storageMetadataKeys,
|
|
1196
|
+
'Stored document',
|
|
1197
|
+
);
|
|
1198
|
+
const canonical = this.canonicalizeDocument(documentArg, 'Stored document');
|
|
1199
|
+
const revision = (canonical as Record<string, unknown>)._smartdataRevision;
|
|
1200
|
+
if (!isMongoObjectId((canonical as Record<string, unknown>)._id)) {
|
|
1201
|
+
throw new SmartdataExactPersistenceError(
|
|
1202
|
+
'invalid_document',
|
|
1203
|
+
'Stored document _id must be a MongoDB ObjectId.',
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
if (
|
|
1207
|
+
revision !== undefined &&
|
|
1208
|
+
(typeof revision !== 'string' || revision.length === 0)
|
|
1209
|
+
) {
|
|
1210
|
+
throw new SmartdataExactPersistenceError(
|
|
1211
|
+
'invalid_document',
|
|
1212
|
+
'Stored document _smartdataRevision must be a nonempty string when present.',
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
this.getPersistedBody(canonical as TStoredDocument<TPersisted>);
|
|
1216
|
+
return canonical as TStoredDocument<TPersisted>;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private assertWithoutMutation(
|
|
1220
|
+
documentArg: object,
|
|
1221
|
+
labelArg: string,
|
|
1222
|
+
): void {
|
|
1223
|
+
assertBsonWithoutMutation(documentArg, this.options.assertDocument, (reasonArg, causeArg) =>
|
|
1224
|
+
new SmartdataExactPersistenceError('invalid_document', `${labelArg} ${
|
|
1225
|
+
reasonArg === 'assertion' ? 'assertion failed.' :
|
|
1226
|
+
reasonArg === 'bson' ? 'validator produced a non-BSON document.' :
|
|
1227
|
+
'validator must not mutate the document.'
|
|
1228
|
+
}`, causeArg === undefined ? undefined : { cause: causeArg }));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private getPersistedBody(
|
|
1232
|
+
storedDocumentArg: TStoredDocument<TPersisted>,
|
|
1233
|
+
): TExactPersistedDocument<TPersisted> {
|
|
1234
|
+
const body = Object.create(null) as Record<string, unknown>;
|
|
1235
|
+
for (const key of Object.keys(storedDocumentArg)) {
|
|
1236
|
+
if (!reservedKeys.includes(key as TReservedKey)) {
|
|
1237
|
+
body[key] = (storedDocumentArg as Record<string, unknown>)[key];
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return this.canonicalizePersistedDocument(body);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
private persistedDocumentsEqual(
|
|
1244
|
+
leftArg: TExactPersistedDocument<TPersisted>,
|
|
1245
|
+
rightArg: TExactPersistedDocument<TPersisted>,
|
|
1246
|
+
): boolean {
|
|
1247
|
+
return bytesEqual(
|
|
1248
|
+
plugins.mongodb.BSON.serialize(leftArg),
|
|
1249
|
+
plugins.mongodb.BSON.serialize(rightArg),
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private storedDocumentsEqual(
|
|
1254
|
+
leftArg: TStoredDocument<TPersisted>,
|
|
1255
|
+
rightArg: TStoredDocument<TPersisted>,
|
|
1256
|
+
): boolean {
|
|
1257
|
+
if (!leftArg._id.equals(rightArg._id)) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
if (leftArg._smartdataRevision !== rightArg._smartdataRevision) {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
return this.persistedDocumentsEqual(
|
|
1264
|
+
this.getPersistedBody(leftArg),
|
|
1265
|
+
this.getPersistedBody(rightArg),
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
private validateReconcileIdentity(
|
|
1270
|
+
documentArg: TExactPersistedDocument<TPersisted>,
|
|
1271
|
+
): string {
|
|
1272
|
+
const value = (documentArg as Record<string, unknown>)[this.reconcileBy];
|
|
1273
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1274
|
+
throw new SmartdataExactPersistenceError(
|
|
1275
|
+
'invalid_document',
|
|
1276
|
+
`Exact persistence reconcileBy field "${this.reconcileBy}" must be a nonempty string.`,
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return value;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
private async serializeModel(
|
|
1283
|
+
modelArg: TModel,
|
|
1284
|
+
): Promise<TExactPersistedDocument<TPersisted>> {
|
|
1285
|
+
const serialized = (await modelArg.createSavableObject()) as Record<
|
|
1286
|
+
string,
|
|
1287
|
+
unknown
|
|
1288
|
+
>;
|
|
1289
|
+
if (
|
|
1290
|
+
typeof serialized !== 'object' ||
|
|
1291
|
+
serialized === null ||
|
|
1292
|
+
Array.isArray(serialized)
|
|
1293
|
+
) {
|
|
1294
|
+
throw new SmartdataExactPersistenceError(
|
|
1295
|
+
'invalid_document',
|
|
1296
|
+
'createSavableObject() must return a document object.',
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
for (const key of Object.keys(serialized)) {
|
|
1300
|
+
if (
|
|
1301
|
+
!this.allowedPersistedKeys.has(key) &&
|
|
1302
|
+
key !== '_createdAt' &&
|
|
1303
|
+
key !== '_updatedAt'
|
|
1304
|
+
) {
|
|
1305
|
+
const code: TExactPersistenceErrorCode = reservedKeys.includes(
|
|
1306
|
+
key as TReservedKey,
|
|
1307
|
+
)
|
|
1308
|
+
? 'reserved_field'
|
|
1309
|
+
: 'invalid_document';
|
|
1310
|
+
throw new SmartdataExactPersistenceError(
|
|
1311
|
+
code,
|
|
1312
|
+
`Model serialization produced forbidden top-level field "${key}".`,
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
const body = Object.create(null) as Record<string, unknown>;
|
|
1317
|
+
for (const key of this.allowedPersistedKeys) {
|
|
1318
|
+
if (hasOwn(serialized, key)) {
|
|
1319
|
+
body[key] = serialized[key];
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return this.canonicalizePersistedDocument(body);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private async createPersistedDocument(
|
|
1326
|
+
documentArg: TModel | TExactPersistedDocument<TPersisted>,
|
|
1327
|
+
): Promise<TExactPersistedDocument<TPersisted>> {
|
|
1328
|
+
if (typeof documentArg !== 'object' || documentArg === null) {
|
|
1329
|
+
throw new SmartdataExactPersistenceError(
|
|
1330
|
+
'invalid_document',
|
|
1331
|
+
'Exact persistence insert input must be an object.',
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
if (documentArg instanceof this.modelConstructor) {
|
|
1335
|
+
return this.serializeModel(documentArg);
|
|
1336
|
+
}
|
|
1337
|
+
return this.canonicalizePersistedDocument(documentArg);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private hydrateStoredDocument(
|
|
1341
|
+
documentArg: TStoredDocument<TPersisted>,
|
|
1342
|
+
): TModel {
|
|
1343
|
+
return this.modelConstructor.createInstanceFromMongoDbNativeDoc<TModel>(
|
|
1344
|
+
documentArg,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private async findByReconcileIdentity(
|
|
1349
|
+
identityArg: string,
|
|
1350
|
+
sessionArg?: plugins.mongodb.ClientSession,
|
|
1351
|
+
): Promise<TStoredDocument<TPersisted> | null> {
|
|
1352
|
+
const rawDocument = await this.collection.mongoDbCollection.findOne({
|
|
1353
|
+
[this.reconcileBy]: identityArg,
|
|
1354
|
+
}, { session: sessionArg });
|
|
1355
|
+
return rawDocument
|
|
1356
|
+
? this.canonicalizeStoredDocument(rawDocument)
|
|
1357
|
+
: null;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
private ambiguousWriteError(messageArg: string, causeArg: unknown): Error {
|
|
1361
|
+
return new SmartdataExactPersistenceError('ambiguous_write', messageArg, {
|
|
1362
|
+
cause: causeArg,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private canonicalizeDeleteCurrent(
|
|
1367
|
+
optionsArg: {
|
|
1368
|
+
current: TStoredDocument<TPersisted>;
|
|
1369
|
+
},
|
|
1370
|
+
): {
|
|
1371
|
+
current: TStoredDocument<TPersisted>;
|
|
1372
|
+
identity: string;
|
|
1373
|
+
} {
|
|
1374
|
+
if (
|
|
1375
|
+
typeof optionsArg !== 'object' ||
|
|
1376
|
+
optionsArg === null ||
|
|
1377
|
+
plugins.nodeUtil.types.isProxy(optionsArg) ||
|
|
1378
|
+
!isPlainObject(optionsArg)
|
|
1379
|
+
) {
|
|
1380
|
+
throw new SmartdataExactPersistenceError(
|
|
1381
|
+
'invalid_document',
|
|
1382
|
+
'Exact delete options must be an object containing only current.',
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
const optionKeys = Reflect.ownKeys(optionsArg);
|
|
1386
|
+
const currentDescriptor = Object.getOwnPropertyDescriptor(
|
|
1387
|
+
optionsArg,
|
|
1388
|
+
'current',
|
|
1389
|
+
);
|
|
1390
|
+
if (
|
|
1391
|
+
optionKeys.length !== 1 ||
|
|
1392
|
+
optionKeys[0] !== 'current' ||
|
|
1393
|
+
!currentDescriptor ||
|
|
1394
|
+
!('value' in currentDescriptor) ||
|
|
1395
|
+
currentDescriptor.enumerable !== true
|
|
1396
|
+
) {
|
|
1397
|
+
throw new SmartdataExactPersistenceError(
|
|
1398
|
+
'invalid_document',
|
|
1399
|
+
'Exact delete options must contain one inert enumerable current data property.',
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
const currentSnapshot = snapshotInertExactValue(
|
|
1403
|
+
currentDescriptor.value,
|
|
1404
|
+
'Exact delete current',
|
|
1405
|
+
);
|
|
1406
|
+
const current = this.canonicalizeStoredDocument(
|
|
1407
|
+
currentSnapshot as object,
|
|
1408
|
+
);
|
|
1409
|
+
const identity = this.validateReconcileIdentity(
|
|
1410
|
+
this.getPersistedBody(current),
|
|
1411
|
+
);
|
|
1412
|
+
return { current, identity };
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
private async reconcileDeleteOutcome(
|
|
1416
|
+
currentArg: TStoredDocument<TPersisted>,
|
|
1417
|
+
identityArg: string,
|
|
1418
|
+
causeArg: unknown,
|
|
1419
|
+
sessionArg?: plugins.mongodb.ClientSession,
|
|
1420
|
+
): Promise<TExactDeleteResult> {
|
|
1421
|
+
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1422
|
+
try {
|
|
1423
|
+
reconciled = await this.findByReconcileIdentity(identityArg, sessionArg);
|
|
1424
|
+
} catch (errorArg) {
|
|
1425
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1426
|
+
throw errorArg;
|
|
1427
|
+
}
|
|
1428
|
+
throw this.ambiguousWriteError(
|
|
1429
|
+
'Exact delete outcome was inconclusive and reconciliation could not be completed.',
|
|
1430
|
+
causeArg,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (reconciled && reconciled._id.equals(currentArg._id)) {
|
|
1434
|
+
return { status: 'concurrent_change', deletedCount: 0 };
|
|
1435
|
+
}
|
|
1436
|
+
throw this.ambiguousWriteError(
|
|
1437
|
+
'Exact delete outcome was inconclusive and could not be authoritatively reconciled.',
|
|
1438
|
+
causeArg,
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
public async insert(
|
|
1443
|
+
documentArg: TModel | TExactPersistedDocument<TPersisted>,
|
|
1444
|
+
optionsArg: IExactSessionOptions = {},
|
|
1445
|
+
): Promise<TExactInsertResult<TPersisted>> {
|
|
1446
|
+
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1447
|
+
const persistedDocument = await this.createPersistedDocument(documentArg);
|
|
1448
|
+
const identity = this.validateReconcileIdentity(persistedDocument);
|
|
1449
|
+
const expectedStoredDocument = this.canonicalizeStoredDocument({
|
|
1450
|
+
_id: new plugins.mongodb.ObjectId(),
|
|
1451
|
+
...persistedDocument,
|
|
1452
|
+
});
|
|
1453
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1454
|
+
const existing = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1455
|
+
if (existing) {
|
|
1456
|
+
return this.persistedDocumentsEqual(
|
|
1457
|
+
this.getPersistedBody(existing),
|
|
1458
|
+
persistedDocument,
|
|
1459
|
+
)
|
|
1460
|
+
? { status: 'already_committed', document: existing }
|
|
1461
|
+
: { status: 'conflict', document: existing };
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
let result: plugins.mongodb.InsertOneResult;
|
|
1466
|
+
try {
|
|
1467
|
+
result = await this.collection.mongoDbCollection.insertOne(
|
|
1468
|
+
expectedStoredDocument,
|
|
1469
|
+
{ session: sessionArg },
|
|
1470
|
+
);
|
|
1471
|
+
} catch (errorArg) {
|
|
1472
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1473
|
+
throw errorArg;
|
|
1474
|
+
}
|
|
1475
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1476
|
+
if (isDuplicateKeyError(errorArg)) {
|
|
1477
|
+
throw new SmartdataExactPersistenceError(
|
|
1478
|
+
'unique_conflict',
|
|
1479
|
+
'Exact transactional insert conflicts with a unique index.',
|
|
1480
|
+
{ cause: errorArg },
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
throw errorArg;
|
|
1484
|
+
}
|
|
1485
|
+
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1486
|
+
try {
|
|
1487
|
+
reconciled = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1488
|
+
} catch (reconciliationError) {
|
|
1489
|
+
if (isTransientTransactionError(reconciliationError, sessionArg)) {
|
|
1490
|
+
throw reconciliationError;
|
|
1491
|
+
}
|
|
1492
|
+
if (isDuplicateKeyError(errorArg)) {
|
|
1493
|
+
throw new SmartdataExactPersistenceError(
|
|
1494
|
+
'unique_conflict',
|
|
1495
|
+
'Exact insert conflicts with a unique index and reconciliation failed.',
|
|
1496
|
+
{ cause: errorArg },
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
throw this.ambiguousWriteError(
|
|
1500
|
+
'Exact insert failed and reconciliation could not be completed.',
|
|
1501
|
+
errorArg,
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
if (reconciled) {
|
|
1505
|
+
if (
|
|
1506
|
+
this.persistedDocumentsEqual(
|
|
1507
|
+
this.getPersistedBody(reconciled),
|
|
1508
|
+
persistedDocument,
|
|
1509
|
+
)
|
|
1510
|
+
) {
|
|
1511
|
+
return { status: 'already_committed', document: reconciled };
|
|
1512
|
+
}
|
|
1513
|
+
if (isDuplicateKeyError(errorArg)) {
|
|
1514
|
+
return { status: 'conflict', document: reconciled };
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
if (isDuplicateKeyError(errorArg)) {
|
|
1518
|
+
throw new SmartdataExactPersistenceError(
|
|
1519
|
+
'unique_conflict',
|
|
1520
|
+
'Exact insert conflicts with a secondary unique index.',
|
|
1521
|
+
{ cause: errorArg },
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
throw this.ambiguousWriteError(
|
|
1525
|
+
'Exact insert failed and its outcome could not be reconciled.',
|
|
1526
|
+
errorArg,
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (!result.acknowledged) {
|
|
1531
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1532
|
+
throw this.ambiguousWriteError(
|
|
1533
|
+
'Exact transactional insert was not acknowledged.',
|
|
1534
|
+
result,
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
let reconciled: TStoredDocument<TPersisted> | null;
|
|
1538
|
+
try {
|
|
1539
|
+
reconciled = await this.findByReconcileIdentity(identity, sessionArg);
|
|
1540
|
+
} catch (errorArg) {
|
|
1541
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1542
|
+
throw errorArg;
|
|
1543
|
+
}
|
|
1544
|
+
throw this.ambiguousWriteError(
|
|
1545
|
+
'Exact insert was not acknowledged and reconciliation could not be completed.',
|
|
1546
|
+
result,
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (
|
|
1550
|
+
reconciled &&
|
|
1551
|
+
this.persistedDocumentsEqual(
|
|
1552
|
+
this.getPersistedBody(reconciled),
|
|
1553
|
+
persistedDocument,
|
|
1554
|
+
)
|
|
1555
|
+
) {
|
|
1556
|
+
return { status: 'already_committed', document: reconciled };
|
|
1557
|
+
}
|
|
1558
|
+
throw this.ambiguousWriteError(
|
|
1559
|
+
'Exact insert was not acknowledged and could not be reconciled.',
|
|
1560
|
+
result,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
return { status: 'inserted', document: expectedStoredDocument };
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
public toPersisted(
|
|
1568
|
+
storedDocumentArg: TStoredDocument<TPersisted>,
|
|
1569
|
+
): TExactPersistedDocument<TPersisted> {
|
|
1570
|
+
const storedDocument = this.canonicalizeStoredDocument(storedDocumentArg);
|
|
1571
|
+
const persistedDocument = this.getPersistedBody(storedDocument);
|
|
1572
|
+
return plugins.mongodb.BSON.deserialize(
|
|
1573
|
+
plugins.mongodb.BSON.serialize(persistedDocument),
|
|
1574
|
+
) as TExactPersistedDocument<TPersisted>;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
public canonicalBytes(
|
|
1578
|
+
persistedDocumentArg: TExactPersistedDocument<TPersisted>,
|
|
1579
|
+
): Uint8Array {
|
|
1580
|
+
return plugins.mongodb.BSON.serialize(
|
|
1581
|
+
this.canonicalizePersistedDocument(persistedDocumentArg),
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
public async findStoredOne(
|
|
1586
|
+
filterArg: MongoFilter<TStoredDocument<TPersisted>>,
|
|
1587
|
+
optionsArg: IExactReadOptions = {},
|
|
1588
|
+
): Promise<TStoredDocument<TPersisted> | null> {
|
|
1589
|
+
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1590
|
+
const cursor = this.collection.mongoDbCollection.find(
|
|
1591
|
+
convertFilterForMongoDb(filterArg),
|
|
1592
|
+
{
|
|
1593
|
+
session: sessionArg,
|
|
1594
|
+
signal: optionsArg.signal,
|
|
1595
|
+
singleBatch: true,
|
|
1596
|
+
},
|
|
1597
|
+
).limit(1);
|
|
1598
|
+
const nativeCursorClose = cursor.close.bind(cursor);
|
|
1599
|
+
let cursorClosePromise: Promise<void> | undefined;
|
|
1600
|
+
cursor.close = ((...args: Parameters<typeof cursor.close>) => {
|
|
1601
|
+
cursorClosePromise ??= nativeCursorClose(...args);
|
|
1602
|
+
return cursorClosePromise;
|
|
1603
|
+
}) as typeof cursor.close;
|
|
1604
|
+
let storedDocument: TStoredDocument<TPersisted> | null;
|
|
1605
|
+
try {
|
|
1606
|
+
const rawDocument = await cursor.next();
|
|
1607
|
+
storedDocument = rawDocument
|
|
1608
|
+
? this.canonicalizeStoredDocument(rawDocument)
|
|
1609
|
+
: null;
|
|
1610
|
+
} catch (errorArg) {
|
|
1611
|
+
try {
|
|
1612
|
+
await cursor.close();
|
|
1613
|
+
} catch {
|
|
1614
|
+
// Cleanup must not replace the exact primary operation error.
|
|
1615
|
+
}
|
|
1616
|
+
throw errorArg;
|
|
1617
|
+
}
|
|
1618
|
+
await cursor.close();
|
|
1619
|
+
return storedDocument;
|
|
1620
|
+
}, optionsArg.signal);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
public async findStored(
|
|
1624
|
+
optionsArg: IFindStoredOptions<TPersisted> = {},
|
|
1625
|
+
): Promise<Array<TStoredDocument<TPersisted>>> {
|
|
1626
|
+
optionsArg.signal?.throwIfAborted();
|
|
1627
|
+
return this.runOperation(optionsArg, async (sessionArg) => {
|
|
1628
|
+
let cursor = this.collection.mongoDbCollection.find(
|
|
1629
|
+
convertFilterForMongoDb(optionsArg.filter || {}),
|
|
1630
|
+
{ session: sessionArg, signal: optionsArg.signal },
|
|
1631
|
+
);
|
|
1632
|
+
// The driver abort listener does not await close, so every close must join
|
|
1633
|
+
// the same native cleanup before runOperation releases its session lease.
|
|
1634
|
+
const nativeCursorClose = cursor.close.bind(cursor);
|
|
1635
|
+
let cursorClosePromise: Promise<void> | undefined;
|
|
1636
|
+
cursor.close = ((...args: Parameters<typeof cursor.close>) => {
|
|
1637
|
+
cursorClosePromise ??= nativeCursorClose(...args);
|
|
1638
|
+
return cursorClosePromise;
|
|
1639
|
+
}) as typeof cursor.close;
|
|
1640
|
+
let storedDocuments: Array<TStoredDocument<TPersisted>>;
|
|
1641
|
+
try {
|
|
1642
|
+
if (optionsArg.sort) {
|
|
1643
|
+
cursor = cursor.sort(optionsArg.sort as plugins.mongodb.Sort);
|
|
1644
|
+
}
|
|
1645
|
+
if (optionsArg.skip !== undefined) {
|
|
1646
|
+
cursor = cursor.skip(optionsArg.skip);
|
|
1647
|
+
}
|
|
1648
|
+
if (optionsArg.limit !== undefined) {
|
|
1649
|
+
cursor = cursor.limit(optionsArg.limit);
|
|
1650
|
+
}
|
|
1651
|
+
const rawDocuments = await cursor.toArray();
|
|
1652
|
+
storedDocuments = rawDocuments.map((documentArg) =>
|
|
1653
|
+
this.canonicalizeStoredDocument(documentArg),
|
|
1654
|
+
);
|
|
1655
|
+
} catch (errorArg) {
|
|
1656
|
+
try {
|
|
1657
|
+
await cursor.close();
|
|
1658
|
+
} catch {
|
|
1659
|
+
// Cleanup must not replace the exact primary operation error.
|
|
1660
|
+
}
|
|
1661
|
+
throw errorArg;
|
|
1662
|
+
}
|
|
1663
|
+
await cursor.close();
|
|
1664
|
+
return storedDocuments;
|
|
1665
|
+
}, optionsArg.signal);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
public async count(
|
|
1669
|
+
filterArg: MongoFilter<TStoredDocument<TPersisted>> = {},
|
|
1670
|
+
optionsArg: IExactSessionOptions = {},
|
|
1671
|
+
): Promise<number> {
|
|
1672
|
+
return this.runOperation(optionsArg, async (sessionArg) =>
|
|
1673
|
+
this.collection.mongoDbCollection.countDocuments(
|
|
1674
|
+
convertFilterForMongoDb(filterArg),
|
|
1675
|
+
{ session: sessionArg },
|
|
1676
|
+
)
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
public async distinct<
|
|
1681
|
+
TField extends TExactDocumentPath<TStoredDocument<TPersisted>>,
|
|
1682
|
+
>(
|
|
1683
|
+
fieldArg: TField,
|
|
1684
|
+
filterArg: MongoFilter<TStoredDocument<TPersisted>> = {},
|
|
1685
|
+
optionsArg: IExactSessionOptions = {},
|
|
1686
|
+
): Promise<Array<TExactDistinctValue<
|
|
1687
|
+
TExactDocumentPathValue<TStoredDocument<TPersisted>, TField>
|
|
1688
|
+
>>> {
|
|
1689
|
+
return this.runOperation(optionsArg, async (sessionArg) =>
|
|
1690
|
+
(await this.collection.mongoDbCollection.distinct(
|
|
1691
|
+
fieldArg as string,
|
|
1692
|
+
convertFilterForMongoDb(filterArg),
|
|
1693
|
+
{ session: sessionArg },
|
|
1694
|
+
)) as Array<TExactDistinctValue<
|
|
1695
|
+
TExactDocumentPathValue<TStoredDocument<TPersisted>, TField>
|
|
1696
|
+
>>
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
public async transition(
|
|
1701
|
+
optionsArg: {
|
|
1702
|
+
current: TStoredDocument<TPersisted>;
|
|
1703
|
+
change: (modelArg: TModel) => void;
|
|
1704
|
+
},
|
|
1705
|
+
sessionOptionsArg: IExactSessionOptions = {},
|
|
1706
|
+
): Promise<TExactTransitionResult<TPersisted>> {
|
|
1707
|
+
return this.runOperation(sessionOptionsArg, async (sessionArg) => {
|
|
1708
|
+
const current = this.canonicalizeStoredDocument(optionsArg.current);
|
|
1709
|
+
const currentBody = this.getPersistedBody(current);
|
|
1710
|
+
const currentIdentity = this.validateReconcileIdentity(currentBody);
|
|
1711
|
+
const model = this.hydrateStoredDocument(current);
|
|
1712
|
+
const untouchedBody = await this.serializeModel(model);
|
|
1713
|
+
if (!this.persistedDocumentsEqual(currentBody, untouchedBody)) {
|
|
1714
|
+
throw new SmartdataExactPersistenceError(
|
|
1715
|
+
'noncanonical_document',
|
|
1716
|
+
'Stored document does not survive an untouched model serialization roundtrip.',
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const changeResult = optionsArg.change(model);
|
|
1721
|
+
if (
|
|
1722
|
+
changeResult !== null &&
|
|
1723
|
+
(typeof changeResult === 'object' || typeof changeResult === 'function') &&
|
|
1724
|
+
typeof (changeResult as { then?: unknown }).then === 'function'
|
|
1725
|
+
) {
|
|
1726
|
+
void Promise.resolve(changeResult).catch(() => undefined);
|
|
1727
|
+
throw new SmartdataExactPersistenceError(
|
|
1728
|
+
'unsupported_operation',
|
|
1729
|
+
'Exact persistence transition callbacks must be synchronous.',
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const candidateBody = await this.serializeModel(model);
|
|
1734
|
+
const candidateIdentity = this.validateReconcileIdentity(candidateBody);
|
|
1735
|
+
if (candidateIdentity !== currentIdentity) {
|
|
1736
|
+
throw new SmartdataExactPersistenceError(
|
|
1737
|
+
'invalid_document',
|
|
1738
|
+
`Exact persistence reconcileBy field "${this.reconcileBy}" is immutable.`,
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const newRevision = globalThis.crypto.randomUUID();
|
|
1742
|
+
const expectedStoredDocument = this.canonicalizeStoredDocument({
|
|
1743
|
+
_id: current._id,
|
|
1744
|
+
...candidateBody,
|
|
1745
|
+
_smartdataRevision: newRevision,
|
|
1746
|
+
});
|
|
1747
|
+
const selector = current._smartdataRevision !== undefined
|
|
1748
|
+
? { _id: current._id, _smartdataRevision: current._smartdataRevision }
|
|
1749
|
+
: { _id: current._id, _smartdataRevision: { $exists: false } };
|
|
1750
|
+
|
|
1751
|
+
try {
|
|
1752
|
+
const postimage = await this.collection.mongoDbCollection.findOneAndReplace(
|
|
1753
|
+
selector,
|
|
1754
|
+
expectedStoredDocument,
|
|
1755
|
+
{
|
|
1756
|
+
returnDocument: 'after',
|
|
1757
|
+
includeResultMetadata: false,
|
|
1758
|
+
upsert: false,
|
|
1759
|
+
session: sessionArg,
|
|
1760
|
+
},
|
|
1761
|
+
);
|
|
1762
|
+
if (!postimage) {
|
|
1763
|
+
return { status: 'concurrent_change' };
|
|
1764
|
+
}
|
|
1765
|
+
const canonicalPostimage = this.canonicalizeStoredDocument(postimage);
|
|
1766
|
+
if (!this.storedDocumentsEqual(canonicalPostimage, expectedStoredDocument)) {
|
|
1767
|
+
throw new SmartdataExactPersistenceError(
|
|
1768
|
+
'ambiguous_write',
|
|
1769
|
+
'Exact transition returned a postimage different from the requested state.',
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
return { status: 'transitioned', document: canonicalPostimage };
|
|
1773
|
+
} catch (errorArg) {
|
|
1774
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1775
|
+
throw errorArg;
|
|
1776
|
+
}
|
|
1777
|
+
if (isDuplicateKeyError(errorArg)) {
|
|
1778
|
+
throw new SmartdataExactPersistenceError(
|
|
1779
|
+
'unique_conflict',
|
|
1780
|
+
'Exact transition conflicts with a secondary unique index.',
|
|
1781
|
+
{ cause: errorArg },
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1785
|
+
throw errorArg;
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
const reconciledRaw = await this.collection.mongoDbCollection.findOne({
|
|
1789
|
+
_id: current._id,
|
|
1790
|
+
}, { session: sessionArg });
|
|
1791
|
+
if (reconciledRaw) {
|
|
1792
|
+
const reconciled = this.canonicalizeStoredDocument(reconciledRaw);
|
|
1793
|
+
if (
|
|
1794
|
+
reconciled._smartdataRevision === newRevision &&
|
|
1795
|
+
this.storedDocumentsEqual(reconciled, expectedStoredDocument)
|
|
1796
|
+
) {
|
|
1797
|
+
return { status: 'transitioned', document: reconciled };
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
} catch (reconciliationError) {
|
|
1801
|
+
if (isTransientTransactionError(reconciliationError, sessionArg)) {
|
|
1802
|
+
throw reconciliationError;
|
|
1803
|
+
}
|
|
1804
|
+
throw this.ambiguousWriteError(
|
|
1805
|
+
'Exact transition failed and reconciliation could not be completed.',
|
|
1806
|
+
errorArg,
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
throw this.ambiguousWriteError(
|
|
1810
|
+
'Exact transition failed and its outcome could not be reconciled.',
|
|
1811
|
+
errorArg,
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
public async delete(
|
|
1818
|
+
optionsArg: {
|
|
1819
|
+
current: TStoredDocument<TPersisted>;
|
|
1820
|
+
},
|
|
1821
|
+
sessionOptionsArg: IExactSessionOptions = {},
|
|
1822
|
+
): Promise<TExactDeleteResult> {
|
|
1823
|
+
const { current, identity } = this.canonicalizeDeleteCurrent(optionsArg);
|
|
1824
|
+
return this.runOperation(sessionOptionsArg, async (sessionArg) => {
|
|
1825
|
+
const selector = current._smartdataRevision !== undefined
|
|
1826
|
+
? {
|
|
1827
|
+
_id: current._id,
|
|
1828
|
+
[this.reconcileBy]: identity,
|
|
1829
|
+
_smartdataRevision: current._smartdataRevision,
|
|
1830
|
+
}
|
|
1831
|
+
: {
|
|
1832
|
+
_id: current._id,
|
|
1833
|
+
[this.reconcileBy]: identity,
|
|
1834
|
+
_smartdataRevision: { $exists: false },
|
|
1835
|
+
};
|
|
1836
|
+
let result: plugins.mongodb.DeleteResult;
|
|
1837
|
+
try {
|
|
1838
|
+
result = await this.collection.mongoDbCollection.deleteOne(selector, {
|
|
1839
|
+
session: sessionArg,
|
|
1840
|
+
});
|
|
1841
|
+
} catch (errorArg) {
|
|
1842
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1843
|
+
throw errorArg;
|
|
1844
|
+
}
|
|
1845
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1846
|
+
throw errorArg;
|
|
1847
|
+
}
|
|
1848
|
+
return this.reconcileDeleteOutcome(current, identity, errorArg, sessionArg);
|
|
1849
|
+
}
|
|
1850
|
+
if (
|
|
1851
|
+
result.acknowledged === true &&
|
|
1852
|
+
result.deletedCount === 1
|
|
1853
|
+
) {
|
|
1854
|
+
return { status: 'deleted', deletedCount: 1 };
|
|
1855
|
+
}
|
|
1856
|
+
if (
|
|
1857
|
+
result.acknowledged === true &&
|
|
1858
|
+
result.deletedCount === 0
|
|
1859
|
+
) {
|
|
1860
|
+
return { status: 'concurrent_change', deletedCount: 0 };
|
|
1861
|
+
}
|
|
1862
|
+
if (result.acknowledged === false) {
|
|
1863
|
+
if (hasActiveTransaction(sessionArg)) {
|
|
1864
|
+
throw this.ambiguousWriteError(
|
|
1865
|
+
'Exact transactional delete was not acknowledged.',
|
|
1866
|
+
result,
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
return this.reconcileDeleteOutcome(current, identity, result, sessionArg);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
try {
|
|
1873
|
+
await this.findByReconcileIdentity(identity, sessionArg);
|
|
1874
|
+
} catch (errorArg) {
|
|
1875
|
+
if (isTransientTransactionError(errorArg, sessionArg)) {
|
|
1876
|
+
throw errorArg;
|
|
1877
|
+
}
|
|
1878
|
+
throw this.ambiguousWriteError(
|
|
1879
|
+
'Exact delete returned an invalid result and reconciliation could not be completed.',
|
|
1880
|
+
result,
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
throw this.ambiguousWriteError(
|
|
1884
|
+
'Exact delete returned an invalid acknowledged result.',
|
|
1885
|
+
result,
|
|
1886
|
+
);
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
export function exactPersistence<
|
|
1892
|
+
TAssertion extends TExactDocumentAssertion<object>,
|
|
1893
|
+
>(
|
|
1894
|
+
optionsArg: {
|
|
1895
|
+
assertDocument: TAssertion;
|
|
1896
|
+
reconcileBy?: TExactIdentityKey<TExactAssertedDocument<TAssertion>>;
|
|
1897
|
+
},
|
|
1898
|
+
) {
|
|
1899
|
+
type TPersisted = TExactAssertedDocument<TAssertion>;
|
|
1900
|
+
return function classDecorator<
|
|
1901
|
+
TConstructor extends new (
|
|
1902
|
+
...args: any[]
|
|
1903
|
+
) => SmartDataDbDoc<any, TPersisted, any> & TPersisted,
|
|
1904
|
+
>(
|
|
1905
|
+
valueArg: TConstructor,
|
|
1906
|
+
contextArg: ClassDecoratorContext,
|
|
1907
|
+
): void {
|
|
1908
|
+
if (contextArg.kind !== 'class') {
|
|
1909
|
+
throw new Error('exactPersistence can only decorate classes.');
|
|
1910
|
+
}
|
|
1911
|
+
if (getOrdinaryPersistencePolicy(valueArg)) {
|
|
1912
|
+
throw new Error('Ordinary and exact persistence policies cannot be combined.');
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
const exactOptions = Object.freeze({ ...optionsArg }) as unknown as
|
|
1916
|
+
IExactPersistenceOptions<TPersisted>;
|
|
1917
|
+
const constructor = valueArg as unknown as IExactModelConstructor<
|
|
1918
|
+
SmartDataDbDoc<any, TPersisted>
|
|
1919
|
+
>;
|
|
1920
|
+
|
|
1921
|
+
const exactPolicy = Object.freeze({
|
|
1922
|
+
options: exactOptions,
|
|
1923
|
+
unsupportedSave: unsupportedExactSave,
|
|
1924
|
+
forbiddenDelete: forbiddenExactDelete,
|
|
1925
|
+
});
|
|
1926
|
+
Object.defineProperty(constructor, exactPersistencePolicySymbol, {
|
|
1927
|
+
value: exactPolicy,
|
|
1928
|
+
enumerable: false,
|
|
1929
|
+
configurable: false,
|
|
1930
|
+
writable: false,
|
|
1931
|
+
});
|
|
1932
|
+
Object.defineProperty(constructor.prototype, 'save', {
|
|
1933
|
+
value: unsupportedExactSave,
|
|
1934
|
+
enumerable: false,
|
|
1935
|
+
configurable: false,
|
|
1936
|
+
writable: false,
|
|
1937
|
+
});
|
|
1938
|
+
Object.defineProperty(constructor.prototype, 'delete', {
|
|
1939
|
+
value: forbiddenExactDelete,
|
|
1940
|
+
enumerable: false,
|
|
1941
|
+
configurable: false,
|
|
1942
|
+
writable: false,
|
|
1943
|
+
});
|
|
1944
|
+
Object.defineProperty(constructor, 'exact', {
|
|
1945
|
+
get(this: IExactModelConstructor<SmartDataDbDoc<any, TPersisted>>) {
|
|
1946
|
+
const receiver = this;
|
|
1947
|
+
if (typeof receiver !== 'function' || (receiver !== constructor
|
|
1948
|
+
&& !Object.prototype.isPrototypeOf.call(constructor, receiver))
|
|
1949
|
+
|| Reflect.get(receiver, exactPersistencePolicySymbol) !== exactPolicy) {
|
|
1950
|
+
throw new SmartdataExactPersistenceError('invalid_configuration',
|
|
1951
|
+
'Exact persistence requires the decorated model or an inheriting receiver.');
|
|
1952
|
+
}
|
|
1953
|
+
const collection = receiver.collection;
|
|
1954
|
+
let byCollection = exactCollectionCache.get(receiver);
|
|
1955
|
+
if (!byCollection) {
|
|
1956
|
+
byCollection = new WeakMap();
|
|
1957
|
+
exactCollectionCache.set(receiver, byCollection);
|
|
1958
|
+
}
|
|
1959
|
+
let exactCollection = byCollection.get(collection);
|
|
1960
|
+
if (!exactCollection) {
|
|
1961
|
+
exactCollection = new SmartdataExactCollection(
|
|
1962
|
+
receiver,
|
|
1963
|
+
collection,
|
|
1964
|
+
exactOptions,
|
|
1965
|
+
);
|
|
1966
|
+
byCollection.set(collection, exactCollection);
|
|
1967
|
+
}
|
|
1968
|
+
return exactCollection;
|
|
1969
|
+
},
|
|
1970
|
+
enumerable: false,
|
|
1971
|
+
configurable: false,
|
|
1972
|
+
});
|
|
1973
|
+
};
|
|
1974
|
+
}
|