@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.
Files changed (255) hide show
  1. package/.smartconfig.json +37 -0
  2. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  3. package/dist_ts/00_commitinfo_data.js +9 -0
  4. package/dist_ts/classes.losslessorgclient.d.ts +32 -0
  5. package/dist_ts/classes.losslessorgclient.js +103 -0
  6. package/dist_ts/core/classes.error.d.ts +16 -0
  7. package/dist_ts/core/classes.error.js +29 -0
  8. package/dist_ts/core/classes.operation.d.ts +17 -0
  9. package/dist_ts/core/classes.operation.js +70 -0
  10. package/dist_ts/core/interfaces.d.ts +18 -0
  11. package/dist_ts/core/interfaces.js +2 -0
  12. package/dist_ts/index.d.ts +4 -0
  13. package/dist_ts/index.js +3 -0
  14. package/dist_ts/nosqldb/classes.atomicdelete.d.ts +26 -0
  15. package/dist_ts/nosqldb/classes.atomicdelete.js +44 -0
  16. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.d.ts +27 -0
  17. package/dist_ts/nosqldb/classes.atomicfindoneandupdate.js +86 -0
  18. package/dist_ts/nosqldb/classes.atomicupdate.d.ts +26 -0
  19. package/dist_ts/nosqldb/classes.atomicupdate.js +55 -0
  20. package/dist_ts/nosqldb/classes.bootstraprecord.d.ts +14 -0
  21. package/dist_ts/nosqldb/classes.bootstraprecord.js +80 -0
  22. package/dist_ts/nosqldb/classes.bsonassertion.d.ts +2 -0
  23. package/dist_ts/nosqldb/classes.bsonassertion.js +29 -0
  24. package/dist_ts/nosqldb/classes.bsonobjectid.d.ts +2 -0
  25. package/dist_ts/nosqldb/classes.bsonobjectid.js +50 -0
  26. package/dist_ts/nosqldb/classes.bsonsnapshot.d.ts +3 -0
  27. package/dist_ts/nosqldb/classes.bsonsnapshot.js +126 -0
  28. package/dist_ts/nosqldb/classes.cached-document.d.ts +27 -0
  29. package/dist_ts/nosqldb/classes.cached-document.js +112 -0
  30. package/dist_ts/nosqldb/classes.collection.d.ts +302 -0
  31. package/dist_ts/nosqldb/classes.collection.js +1568 -0
  32. package/dist_ts/nosqldb/classes.collectionfactory.d.ts +30 -0
  33. package/dist_ts/nosqldb/classes.collectionfactory.js +65 -0
  34. package/dist_ts/nosqldb/classes.collectionlifecycle.d.ts +4 -0
  35. package/dist_ts/nosqldb/classes.collectionlifecycle.js +15 -0
  36. package/dist_ts/nosqldb/classes.collectionpreparation.d.ts +34 -0
  37. package/dist_ts/nosqldb/classes.collectionpreparation.js +134 -0
  38. package/dist_ts/nosqldb/classes.collectiontopology.d.ts +47 -0
  39. package/dist_ts/nosqldb/classes.collectiontopology.js +388 -0
  40. package/dist_ts/nosqldb/classes.connection.d.ts +18 -0
  41. package/dist_ts/nosqldb/classes.connection.js +23 -0
  42. package/dist_ts/nosqldb/classes.convenience.d.ts +1 -0
  43. package/dist_ts/nosqldb/classes.convenience.js +5 -0
  44. package/dist_ts/nosqldb/classes.cursor.d.ts +15 -0
  45. package/dist_ts/nosqldb/classes.cursor.js +82 -0
  46. package/dist_ts/nosqldb/classes.cursoroperation.d.ts +7 -0
  47. package/dist_ts/nosqldb/classes.cursoroperation.js +36 -0
  48. package/dist_ts/nosqldb/classes.db.d.ts +98 -0
  49. package/dist_ts/nosqldb/classes.db.js +434 -0
  50. package/dist_ts/nosqldb/classes.distributedcoordinator.d.ts +52 -0
  51. package/dist_ts/nosqldb/classes.distributedcoordinator.js +314 -0
  52. package/dist_ts/nosqldb/classes.doc.d.ts +722 -0
  53. package/dist_ts/nosqldb/classes.doc.js +2976 -0
  54. package/dist_ts/nosqldb/classes.easystore.d.ts +42 -0
  55. package/dist_ts/nosqldb/classes.easystore.js +199 -0
  56. package/dist_ts/nosqldb/classes.exactpersistence.d.ts +141 -0
  57. package/dist_ts/nosqldb/classes.exactpersistence.js +1110 -0
  58. package/dist_ts/nosqldb/classes.lucene.adapter.d.ts +173 -0
  59. package/dist_ts/nosqldb/classes.lucene.adapter.js +623 -0
  60. package/dist_ts/nosqldb/classes.mongoadministration.d.ts +73 -0
  61. package/dist_ts/nosqldb/classes.mongoadministration.js +497 -0
  62. package/dist_ts/nosqldb/classes.namespaceinspection.d.ts +42 -0
  63. package/dist_ts/nosqldb/classes.namespaceinspection.js +138 -0
  64. package/dist_ts/nosqldb/classes.operationbudget.d.ts +22 -0
  65. package/dist_ts/nosqldb/classes.operationbudget.js +65 -0
  66. package/dist_ts/nosqldb/classes.ordinarypersistence.d.ts +18 -0
  67. package/dist_ts/nosqldb/classes.ordinarypersistence.js +71 -0
  68. package/dist_ts/nosqldb/classes.persistence.d.ts +14 -0
  69. package/dist_ts/nosqldb/classes.persistence.js +34 -0
  70. package/dist_ts/nosqldb/classes.session.d.ts +28 -0
  71. package/dist_ts/nosqldb/classes.session.js +246 -0
  72. package/dist_ts/nosqldb/classes.storedinspection.d.ts +44 -0
  73. package/dist_ts/nosqldb/classes.storedinspection.js +100 -0
  74. package/dist_ts/nosqldb/classes.testdatabase.d.ts +56 -0
  75. package/dist_ts/nosqldb/classes.testdatabase.js +250 -0
  76. package/dist_ts/nosqldb/classes.watcher.d.ts +37 -0
  77. package/dist_ts/nosqldb/classes.watcher.js +98 -0
  78. package/dist_ts/nosqldb/index.d.ts +27 -0
  79. package/dist_ts/nosqldb/index.js +18 -0
  80. package/dist_ts/nosqldb/logging.d.ts +2 -0
  81. package/dist_ts/nosqldb/logging.js +3 -0
  82. package/dist_ts/nosqldb/plugins.d.ts +17 -0
  83. package/dist_ts/nosqldb/plugins.js +20 -0
  84. package/dist_ts/nosqldb/shim.d.ts +1 -0
  85. package/dist_ts/nosqldb/shim.js +8 -0
  86. package/dist_ts/nosqldb/testsupport.d.ts +3 -0
  87. package/dist_ts/nosqldb/testsupport.js +3 -0
  88. package/dist_ts/objectstorage/classes.bucket.d.ts +227 -0
  89. package/dist_ts/objectstorage/classes.bucket.js +939 -0
  90. package/dist_ts/objectstorage/classes.connection.d.ts +21 -0
  91. package/dist_ts/objectstorage/classes.connection.js +27 -0
  92. package/dist_ts/objectstorage/classes.directory.d.ts +124 -0
  93. package/dist_ts/objectstorage/classes.directory.js +335 -0
  94. package/dist_ts/objectstorage/classes.exactpathpurgeerror.d.ts +5 -0
  95. package/dist_ts/objectstorage/classes.exactpathpurgeerror.js +18 -0
  96. package/dist_ts/objectstorage/classes.exactuploaderror.d.ts +5 -0
  97. package/dist_ts/objectstorage/classes.exactuploaderror.js +19 -0
  98. package/dist_ts/objectstorage/classes.file.d.ts +91 -0
  99. package/dist_ts/objectstorage/classes.file.js +255 -0
  100. package/dist_ts/objectstorage/classes.listcursor.d.ts +41 -0
  101. package/dist_ts/objectstorage/classes.listcursor.js +67 -0
  102. package/dist_ts/objectstorage/classes.metadata.d.ts +57 -0
  103. package/dist_ts/objectstorage/classes.metadata.js +113 -0
  104. package/dist_ts/objectstorage/classes.smartbucket.d.ts +57 -0
  105. package/dist_ts/objectstorage/classes.smartbucket.js +357 -0
  106. package/dist_ts/objectstorage/classes.trash.d.ts +11 -0
  107. package/dist_ts/objectstorage/classes.trash.js +22 -0
  108. package/dist_ts/objectstorage/classes.watcher.d.ts +79 -0
  109. package/dist_ts/objectstorage/classes.watcher.js +249 -0
  110. package/dist_ts/objectstorage/helpers.d.ts +25 -0
  111. package/dist_ts/objectstorage/helpers.js +215 -0
  112. package/dist_ts/objectstorage/index.d.ts +13 -0
  113. package/dist_ts/objectstorage/index.js +13 -0
  114. package/dist_ts/objectstorage/interfaces.d.ts +276 -0
  115. package/dist_ts/objectstorage/interfaces.js +2 -0
  116. package/dist_ts/objectstorage/internal.exactpathpurge.operations.d.ts +11 -0
  117. package/dist_ts/objectstorage/internal.exactpathpurge.operations.js +1069 -0
  118. package/dist_ts/objectstorage/internal.exactupload.capability.d.ts +40 -0
  119. package/dist_ts/objectstorage/internal.exactupload.capability.js +382 -0
  120. package/dist_ts/objectstorage/internal.exactupload.operations.d.ts +11 -0
  121. package/dist_ts/objectstorage/internal.exactupload.operations.js +1104 -0
  122. package/dist_ts/objectstorage/internal.multipart.planner.d.ts +8 -0
  123. package/dist_ts/objectstorage/internal.multipart.planner.js +63 -0
  124. package/dist_ts/objectstorage/internal.multipart.stream.d.ts +17 -0
  125. package/dist_ts/objectstorage/internal.multipart.stream.js +440 -0
  126. package/dist_ts/objectstorage/internal.multipart.types.d.ts +82 -0
  127. package/dist_ts/objectstorage/internal.multipart.types.js +2 -0
  128. package/dist_ts/objectstorage/plugins.d.ts +17 -0
  129. package/dist_ts/objectstorage/plugins.js +23 -0
  130. package/dist_ts/plugins.d.ts +4 -0
  131. package/dist_ts/plugins.js +5 -0
  132. package/dist_ts/sqldb/classes.clickhouseconnection.d.ts +39 -0
  133. package/dist_ts/sqldb/classes.clickhouseconnection.js +225 -0
  134. package/dist_ts/sqldb/classes.sqlconnection.d.ts +50 -0
  135. package/dist_ts/sqldb/classes.sqlconnection.js +376 -0
  136. package/dist_ts/sqldb/classes.sqltable.d.ts +29 -0
  137. package/dist_ts/sqldb/classes.sqltable.js +70 -0
  138. package/dist_ts/sqldb/clickhouse/classes.metricwriter.d.ts +27 -0
  139. package/dist_ts/sqldb/clickhouse/classes.metricwriter.js +89 -0
  140. package/dist_ts/sqldb/clickhouse/index.d.ts +9 -0
  141. package/dist_ts/sqldb/clickhouse/index.js +13 -0
  142. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.d.ts +88 -0
  143. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.js +304 -0
  144. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.d.ts +28 -0
  145. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.js +46 -0
  146. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.d.ts +43 -0
  147. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.js +155 -0
  148. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.d.ts +13 -0
  149. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.resultset.js +36 -0
  150. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.d.ts +49 -0
  151. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.js +106 -0
  152. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.d.ts +51 -0
  153. package/dist_ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.js +96 -0
  154. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.d.ts +5 -0
  155. package/dist_ts/sqldb/clickhouse/smartclickhouse.plugins.js +6 -0
  156. package/dist_ts/sqldb/clickhouse/smartclickhouse.types.d.ts +53 -0
  157. package/dist_ts/sqldb/clickhouse/smartclickhouse.types.js +41 -0
  158. package/dist_ts/sqldb/index.d.ts +6 -0
  159. package/dist_ts/sqldb/index.js +6 -0
  160. package/dist_ts/sqldb/interfaces.d.ts +53 -0
  161. package/dist_ts/sqldb/interfaces.js +19 -0
  162. package/dist_ts/sqldb/internal.sqlpool.d.ts +51 -0
  163. package/dist_ts/sqldb/internal.sqlpool.js +214 -0
  164. package/dist_ts/sqldb/plugins.clickhouse.d.ts +4 -0
  165. package/dist_ts/sqldb/plugins.clickhouse.js +5 -0
  166. package/dist_ts/sqldb/plugins.mariadb.d.ts +4 -0
  167. package/dist_ts/sqldb/plugins.mariadb.js +5 -0
  168. package/docs/source/smartbucket/license.md +19 -0
  169. package/docs/source/smartclickhouse/license.md +19 -0
  170. package/license.md +21 -0
  171. package/package.json +101 -0
  172. package/readme.md +157 -0
  173. package/ts/00_commitinfo_data.ts +8 -0
  174. package/ts/classes.losslessorgclient.ts +104 -0
  175. package/ts/core/classes.error.ts +31 -0
  176. package/ts/core/classes.operation.ts +60 -0
  177. package/ts/core/interfaces.ts +24 -0
  178. package/ts/index.ts +4 -0
  179. package/ts/nosqldb/classes.atomicdelete.ts +81 -0
  180. package/ts/nosqldb/classes.atomicfindoneandupdate.ts +152 -0
  181. package/ts/nosqldb/classes.atomicupdate.ts +99 -0
  182. package/ts/nosqldb/classes.bootstraprecord.ts +107 -0
  183. package/ts/nosqldb/classes.bsonassertion.ts +25 -0
  184. package/ts/nosqldb/classes.bsonobjectid.ts +45 -0
  185. package/ts/nosqldb/classes.bsonsnapshot.ts +105 -0
  186. package/ts/nosqldb/classes.cached-document.ts +67 -0
  187. package/ts/nosqldb/classes.collection.ts +2399 -0
  188. package/ts/nosqldb/classes.collectionfactory.ts +87 -0
  189. package/ts/nosqldb/classes.collectionlifecycle.ts +24 -0
  190. package/ts/nosqldb/classes.collectionpreparation.ts +164 -0
  191. package/ts/nosqldb/classes.collectiontopology.ts +645 -0
  192. package/ts/nosqldb/classes.connection.ts +31 -0
  193. package/ts/nosqldb/classes.convenience.ts +5 -0
  194. package/ts/nosqldb/classes.cursor.ts +88 -0
  195. package/ts/nosqldb/classes.cursoroperation.ts +38 -0
  196. package/ts/nosqldb/classes.db.ts +637 -0
  197. package/ts/nosqldb/classes.distributedcoordinator.ts +305 -0
  198. package/ts/nosqldb/classes.doc.ts +4928 -0
  199. package/ts/nosqldb/classes.easystore.ts +154 -0
  200. package/ts/nosqldb/classes.exactpersistence.ts +1974 -0
  201. package/ts/nosqldb/classes.lucene.adapter.ts +780 -0
  202. package/ts/nosqldb/classes.mongoadministration.ts +867 -0
  203. package/ts/nosqldb/classes.namespaceinspection.ts +199 -0
  204. package/ts/nosqldb/classes.operationbudget.ts +75 -0
  205. package/ts/nosqldb/classes.ordinarypersistence.ts +84 -0
  206. package/ts/nosqldb/classes.persistence.ts +62 -0
  207. package/ts/nosqldb/classes.session.ts +389 -0
  208. package/ts/nosqldb/classes.storedinspection.ts +151 -0
  209. package/ts/nosqldb/classes.testdatabase.ts +272 -0
  210. package/ts/nosqldb/classes.watcher.ts +96 -0
  211. package/ts/nosqldb/index.ts +51 -0
  212. package/ts/nosqldb/logging.ts +3 -0
  213. package/ts/nosqldb/plugins.ts +37 -0
  214. package/ts/nosqldb/shim.ts +6 -0
  215. package/ts/nosqldb/testsupport.ts +13 -0
  216. package/ts/objectstorage/classes.bucket.ts +1114 -0
  217. package/ts/objectstorage/classes.connection.ts +36 -0
  218. package/ts/objectstorage/classes.directory.ts +425 -0
  219. package/ts/objectstorage/classes.exactpathpurgeerror.ts +24 -0
  220. package/ts/objectstorage/classes.exactuploaderror.ts +25 -0
  221. package/ts/objectstorage/classes.file.ts +303 -0
  222. package/ts/objectstorage/classes.listcursor.ts +89 -0
  223. package/ts/objectstorage/classes.metadata.ts +134 -0
  224. package/ts/objectstorage/classes.smartbucket.ts +445 -0
  225. package/ts/objectstorage/classes.trash.ts +30 -0
  226. package/ts/objectstorage/classes.watcher.ts +289 -0
  227. package/ts/objectstorage/helpers.ts +254 -0
  228. package/ts/objectstorage/index.ts +13 -0
  229. package/ts/objectstorage/interfaces.ts +389 -0
  230. package/ts/objectstorage/internal.exactpathpurge.operations.ts +1936 -0
  231. package/ts/objectstorage/internal.exactupload.capability.ts +580 -0
  232. package/ts/objectstorage/internal.exactupload.operations.ts +1715 -0
  233. package/ts/objectstorage/internal.multipart.planner.ts +90 -0
  234. package/ts/objectstorage/internal.multipart.stream.ts +606 -0
  235. package/ts/objectstorage/internal.multipart.types.ts +106 -0
  236. package/ts/objectstorage/plugins.ts +35 -0
  237. package/ts/plugins.ts +4 -0
  238. package/ts/sqldb/classes.clickhouseconnection.ts +189 -0
  239. package/ts/sqldb/classes.sqlconnection.ts +350 -0
  240. package/ts/sqldb/classes.sqltable.ts +70 -0
  241. package/ts/sqldb/clickhouse/classes.metricwriter.ts +72 -0
  242. package/ts/sqldb/clickhouse/index.ts +16 -0
  243. package/ts/sqldb/clickhouse/smartclickhouse.classes.clickhousetable.ts +372 -0
  244. package/ts/sqldb/clickhouse/smartclickhouse.classes.httpclient.ts +52 -0
  245. package/ts/sqldb/clickhouse/smartclickhouse.classes.querybuilder.ts +120 -0
  246. package/ts/sqldb/clickhouse/smartclickhouse.classes.resultset.ts +44 -0
  247. package/ts/sqldb/clickhouse/smartclickhouse.classes.smartclickhouse.ts +123 -0
  248. package/ts/sqldb/clickhouse/smartclickhouse.classes.timedatatable.ts +114 -0
  249. package/ts/sqldb/clickhouse/smartclickhouse.plugins.ts +5 -0
  250. package/ts/sqldb/clickhouse/smartclickhouse.types.ts +134 -0
  251. package/ts/sqldb/index.ts +6 -0
  252. package/ts/sqldb/interfaces.ts +61 -0
  253. package/ts/sqldb/internal.sqlpool.ts +179 -0
  254. package/ts/sqldb/plugins.clickhouse.ts +4 -0
  255. 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
+ }