@arken/seer-protocol 0.1.8 → 0.1.9

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 (254) hide show
  1. package/.rush/temp/shrinkwrap-deps.json +1 -1
  2. package/area/area.models.ts +1 -1
  3. package/area/area.router.ts +1 -1
  4. package/area/area.types.ts +1 -1
  5. package/asset/asset.models.ts +1 -1
  6. package/asset/asset.router.ts +1 -1
  7. package/asset/asset.types.ts +1 -1
  8. package/build/area/area.models.d.ts +1 -1
  9. package/build/area/area.models.js +1 -1
  10. package/build/area/area.models.js.map +1 -1
  11. package/build/area/area.router.d.ts +5 -5
  12. package/build/area/area.router.js +1 -1
  13. package/build/area/area.router.js.map +1 -1
  14. package/build/area/area.schema.d.ts +8 -8
  15. package/build/area/area.types.d.ts +1 -1
  16. package/build/asset/asset.models.d.ts +1 -1
  17. package/build/asset/asset.models.js +1 -1
  18. package/build/asset/asset.models.js.map +1 -1
  19. package/build/asset/asset.router.d.ts +6 -6
  20. package/build/asset/asset.router.js +1 -1
  21. package/build/asset/asset.router.js.map +1 -1
  22. package/build/asset/asset.schema.d.ts +8 -8
  23. package/build/asset/asset.types.d.ts +1 -1
  24. package/build/chain/chain.models.d.ts +1 -1
  25. package/build/chain/chain.models.js +1 -1
  26. package/build/chain/chain.models.js.map +1 -1
  27. package/build/chain/chain.router.d.ts +5 -5
  28. package/build/chain/chain.router.js +1 -1
  29. package/build/chain/chain.router.js.map +1 -1
  30. package/build/chain/chain.schema.d.ts +10 -10
  31. package/build/chain/chain.types.d.ts +1 -1
  32. package/build/character/character.models.d.ts +1 -1
  33. package/build/character/character.models.js +1 -1
  34. package/build/character/character.models.js.map +1 -1
  35. package/build/character/character.router.d.ts +13 -13
  36. package/build/character/character.router.js +1 -1
  37. package/build/character/character.router.js.map +1 -1
  38. package/build/character/character.schema.d.ts +36 -36
  39. package/build/character/character.types.d.ts +1 -1
  40. package/build/chat/chat.models.d.ts +1 -1
  41. package/build/chat/chat.models.js +1 -1
  42. package/build/chat/chat.models.js.map +1 -1
  43. package/build/chat/chat.router.d.ts +4 -4
  44. package/build/chat/chat.router.js +1 -1
  45. package/build/chat/chat.router.js.map +1 -1
  46. package/build/chat/chat.schema.d.ts +4 -4
  47. package/build/chat/chat.types.d.ts +1 -1
  48. package/build/collection/collection.models.d.ts +1 -1
  49. package/build/collection/collection.models.js +1 -1
  50. package/build/collection/collection.models.js.map +1 -1
  51. package/build/collection/collection.router.d.ts +10 -10
  52. package/build/collection/collection.router.js +1 -1
  53. package/build/collection/collection.router.js.map +1 -1
  54. package/build/collection/collection.schema.d.ts +20 -20
  55. package/build/collection/collection.types.d.ts +1 -1
  56. package/build/core/core.models.d.ts +3 -3
  57. package/build/core/core.models.js +1 -1
  58. package/build/core/core.models.js.map +1 -1
  59. package/build/core/core.router.d.ts +85 -85
  60. package/build/core/core.router.js +1 -1
  61. package/build/core/core.router.js.map +1 -1
  62. package/build/core/core.schema.d.ts +182 -182
  63. package/build/core/core.types.d.ts +1 -1
  64. package/build/evolution/evolution.models.js +1 -1
  65. package/build/evolution/evolution.models.js.map +1 -1
  66. package/build/evolution/evolution.router.d.ts +2 -2
  67. package/build/evolution/evolution.router.js +2 -2
  68. package/build/evolution/evolution.router.js.map +1 -1
  69. package/build/evolution/evolution.schema.d.ts +2 -1
  70. package/build/evolution/evolution.schema.js +1 -1
  71. package/build/evolution/evolution.schema.js.map +1 -1
  72. package/build/game/game.models.d.ts +1 -1
  73. package/build/game/game.models.js +1 -1
  74. package/build/game/game.models.js.map +1 -1
  75. package/build/game/game.router.d.ts +8 -8
  76. package/build/game/game.router.js +1 -1
  77. package/build/game/game.router.js.map +1 -1
  78. package/build/game/game.schema.d.ts +12 -12
  79. package/build/game/game.types.d.ts +1 -1
  80. package/build/index.d.ts +1 -1
  81. package/build/infinite/infinite.models.js +1 -1
  82. package/build/infinite/infinite.models.js.map +1 -1
  83. package/build/infinite/infinite.router.d.ts +1 -1
  84. package/build/infinite/infinite.router.js +1 -1
  85. package/build/infinite/infinite.router.js.map +1 -1
  86. package/build/infinite/infinite.schema.d.ts +2 -1
  87. package/build/infinite/infinite.schema.js +1 -1
  88. package/build/infinite/infinite.schema.js.map +1 -1
  89. package/build/interface/interface.models.d.ts +1 -1
  90. package/build/interface/interface.models.js +1 -1
  91. package/build/interface/interface.models.js.map +1 -1
  92. package/build/interface/interface.router.d.ts +9 -9
  93. package/build/interface/interface.router.js +1 -1
  94. package/build/interface/interface.router.js.map +1 -1
  95. package/build/interface/interface.schema.d.ts +14 -14
  96. package/build/interface/interface.types.d.ts +1 -1
  97. package/build/isles/isles.models.js +1 -1
  98. package/build/isles/isles.models.js.map +1 -1
  99. package/build/isles/isles.router.d.ts +1 -1
  100. package/build/isles/isles.router.js +1 -1
  101. package/build/isles/isles.router.js.map +1 -1
  102. package/build/item/item.models.d.ts +1 -1
  103. package/build/item/item.models.js +1 -1
  104. package/build/item/item.models.js.map +1 -1
  105. package/build/item/item.router.d.ts +2 -2
  106. package/build/item/item.router.js +1 -1
  107. package/build/item/item.router.js.map +1 -1
  108. package/build/item/item.schema.d.ts +28 -28
  109. package/build/item/item.types.d.ts +1 -1
  110. package/build/job/job.models.d.ts +1 -1
  111. package/build/job/job.models.js +1 -1
  112. package/build/job/job.models.js.map +1 -1
  113. package/build/job/job.router.d.ts +2 -2
  114. package/build/job/job.router.js +1 -1
  115. package/build/job/job.router.js.map +1 -1
  116. package/build/job/job.schema.d.ts +2 -2
  117. package/build/job/job.types.d.ts +1 -1
  118. package/build/market/market.models.d.ts +1 -1
  119. package/build/market/market.models.js +1 -1
  120. package/build/market/market.models.js.map +1 -1
  121. package/build/market/market.router.d.ts +12 -12
  122. package/build/market/market.router.js +1 -1
  123. package/build/market/market.router.js.map +1 -1
  124. package/build/market/market.schema.d.ts +30 -30
  125. package/build/market/market.types.d.ts +1 -1
  126. package/build/oasis/oasis.models.js +1 -1
  127. package/build/oasis/oasis.models.js.map +1 -1
  128. package/build/oasis/oasis.router.d.ts +1 -1
  129. package/build/oasis/oasis.router.js +1 -1
  130. package/build/oasis/oasis.router.js.map +1 -1
  131. package/build/package.json +3 -2
  132. package/build/product/product.models.d.ts +1 -1
  133. package/build/product/product.models.js +1 -1
  134. package/build/product/product.models.js.map +1 -1
  135. package/build/product/product.router.d.ts +14 -14
  136. package/build/product/product.router.js +1 -1
  137. package/build/product/product.router.js.map +1 -1
  138. package/build/product/product.schema.d.ts +22 -22
  139. package/build/product/product.types.d.ts +1 -1
  140. package/build/profile/profile.models.d.ts +1 -1
  141. package/build/profile/profile.models.js +1 -1
  142. package/build/profile/profile.models.js.map +1 -1
  143. package/build/profile/profile.router.js +1 -1
  144. package/build/profile/profile.router.js.map +1 -1
  145. package/build/profile/profile.types.d.ts +1 -1
  146. package/build/raffle/raffle.models.d.ts +1 -1
  147. package/build/raffle/raffle.models.js +1 -1
  148. package/build/raffle/raffle.models.js.map +1 -1
  149. package/build/raffle/raffle.router.d.ts +8 -8
  150. package/build/raffle/raffle.router.js +1 -1
  151. package/build/raffle/raffle.router.js.map +1 -1
  152. package/build/raffle/raffle.schema.d.ts +8 -8
  153. package/build/raffle/raffle.types.d.ts +1 -1
  154. package/build/router.d.ts +211 -211
  155. package/build/schema.d.ts +2 -2
  156. package/build/skill/skill.models.d.ts +1 -1
  157. package/build/skill/skill.models.js +1 -1
  158. package/build/skill/skill.models.js.map +1 -1
  159. package/build/skill/skill.router.d.ts +14 -14
  160. package/build/skill/skill.router.js +1 -1
  161. package/build/skill/skill.router.js.map +1 -1
  162. package/build/skill/skill.schema.d.ts +18 -18
  163. package/build/skill/skill.types.d.ts +1 -1
  164. package/build/trek/trek.router.js +1 -1
  165. package/build/trek/trek.router.js.map +1 -1
  166. package/build/util/mongo.d.ts +163 -0
  167. package/build/util/mongo.js +1128 -0
  168. package/build/util/mongo.js.map +1 -0
  169. package/build/util/rpc.d.ts +59 -0
  170. package/build/util/rpc.js +311 -0
  171. package/build/util/rpc.js.map +1 -0
  172. package/build/util/schema.d.ts +279 -0
  173. package/build/util/schema.js +157 -0
  174. package/build/util/schema.js.map +1 -0
  175. package/build/video/video.models.d.ts +1 -1
  176. package/build/video/video.models.js +1 -1
  177. package/build/video/video.models.js.map +1 -1
  178. package/build/video/video.router.d.ts +12 -12
  179. package/build/video/video.router.js +1 -1
  180. package/build/video/video.router.js.map +1 -1
  181. package/build/video/video.schema.d.ts +14 -14
  182. package/build/video/video.types.d.ts +1 -1
  183. package/chain/chain.models.ts +1 -1
  184. package/chain/chain.router.ts +1 -1
  185. package/chain/chain.types.ts +1 -1
  186. package/character/character.models.ts +1 -1
  187. package/character/character.router.ts +1 -1
  188. package/character/character.types.ts +1 -1
  189. package/chat/chat.models.ts +1 -1
  190. package/chat/chat.router.ts +1 -1
  191. package/chat/chat.types.ts +1 -1
  192. package/collection/collection.models.ts +1 -1
  193. package/collection/collection.router.ts +1 -1
  194. package/collection/collection.types.ts +1 -1
  195. package/core/core.models.ts +1 -1
  196. package/core/core.router.ts +1 -2
  197. package/core/core.types.ts +1 -1
  198. package/evolution/evolution.models.ts +1 -1
  199. package/evolution/evolution.router.ts +2 -3
  200. package/evolution/evolution.schema.ts +1 -1
  201. package/evolution/evolution.types.ts +1 -1
  202. package/game/game.models.ts +1 -1
  203. package/game/game.router.ts +1 -1
  204. package/game/game.types.ts +1 -1
  205. package/index.ts +1 -1
  206. package/infinite/infinite.models.ts +1 -1
  207. package/infinite/infinite.router.ts +2 -3
  208. package/infinite/infinite.schema.ts +1 -1
  209. package/infinite/infinite.types.ts +0 -2
  210. package/interface/interface.models.ts +1 -1
  211. package/interface/interface.router.ts +1 -1
  212. package/interface/interface.types.ts +1 -1
  213. package/isles/isles.models.ts +1 -1
  214. package/isles/isles.router.ts +2 -3
  215. package/isles/isles.schema.ts +1 -1
  216. package/isles/isles.types.ts +1 -1
  217. package/item/item.models.ts +1 -1
  218. package/item/item.router.ts +1 -1
  219. package/item/item.types.ts +1 -1
  220. package/job/job.models.ts +1 -1
  221. package/job/job.router.ts +1 -1
  222. package/job/job.types.ts +1 -1
  223. package/market/market.models.ts +1 -1
  224. package/market/market.router.ts +1 -1
  225. package/market/market.types.ts +1 -1
  226. package/oasis/oasis.models.ts +1 -1
  227. package/oasis/oasis.router.ts +2 -2
  228. package/oasis/oasis.schema.ts +1 -1
  229. package/oasis/oasis.types.ts +1 -1
  230. package/package.json +3 -2
  231. package/product/product.models.ts +1 -1
  232. package/product/product.router.ts +1 -1
  233. package/product/product.types.ts +1 -1
  234. package/profile/profile.models.ts +1 -1
  235. package/profile/profile.router.ts +1 -1
  236. package/profile/profile.types.ts +1 -1
  237. package/raffle/raffle.models.ts +1 -1
  238. package/raffle/raffle.router.ts +1 -1
  239. package/raffle/raffle.types.ts +1 -1
  240. package/router.ts +1 -1
  241. package/skill/skill.models.ts +1 -1
  242. package/skill/skill.router.ts +1 -1
  243. package/skill/skill.types.ts +1 -1
  244. package/trek/trek.models.ts +1 -1
  245. package/trek/trek.router.ts +1 -1
  246. package/trek/trek.schema.ts +1 -1
  247. package/trek/trek.types.ts +1 -1
  248. package/tsconfig.json +31 -2
  249. package/util/mongo.ts +1735 -0
  250. package/util/rpc.ts +550 -0
  251. package/util/schema.ts +321 -0
  252. package/video/video.models.ts +1 -1
  253. package/video/video.router.ts +1 -1
  254. package/video/video.types.ts +1 -1
package/util/mongo.ts ADDED
@@ -0,0 +1,1735 @@
1
+ // mongo.ts
2
+
3
+ import mongoose, {
4
+ Types,
5
+ Model as MongooseModel,
6
+ Schema,
7
+ SchemaDefinition,
8
+ SchemaOptions,
9
+ Document,
10
+ Query,
11
+ UpdateWriteOpResult,
12
+ QueryOptions,
13
+ FilterQuery,
14
+ UpdateQuery,
15
+ UpdateWithAggregationPipeline,
16
+ ProjectionType,
17
+ Collection,
18
+ VirtualType,
19
+ HydratedDocument,
20
+ } from 'mongoose';
21
+ import crypto from 'crypto';
22
+ export type { Mixed, ObjectIdSchemaDefinition, AnyArray, StringSchemaDefinition } from 'mongoose'; // Mixed type
23
+ import pluralize from 'pluralize';
24
+ export { z } from 'zod';
25
+
26
+ export const toCamelCase = (str: string): string => {
27
+ if (!str) return str;
28
+
29
+ return str
30
+ .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase()))
31
+ .replace(/[\s:,()&/\-\+]+/g, '');
32
+ };
33
+
34
+ type PreHookMethod = keyof Query<any, any> | 'save' | 'validate';
35
+
36
+ interface VirtualOptions<T = any> {
37
+ name: string;
38
+ ref?: string;
39
+ refPath?: string;
40
+ localField?: string;
41
+ foreignField?: string;
42
+ justOne?: boolean;
43
+ get?: (this: HydratedDocument<T>, value: any, virtual: VirtualType<T>) => any;
44
+ set?: (this: HydratedDocument<T>, value: any, virtual: VirtualType<T>) => void;
45
+ match?: any;
46
+ options?: any;
47
+ }
48
+
49
+ function isObjectId(val: any): boolean {
50
+ return val instanceof mongoose.Types.ObjectId || (val && typeof val === 'object' && val._bsontype === 'ObjectID');
51
+ }
52
+
53
+ function isPlainObject(val: any): boolean {
54
+ return (
55
+ val !== null && typeof val === 'object' && !Array.isArray(val) && Object.getPrototypeOf(val) === Object.prototype
56
+ );
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Hash helper for zk / event batching
60
+ // ---------------------------------------------------------------------------
61
+ export function hashEvents(events: any[]): string {
62
+ const raw = JSON.stringify(events);
63
+ return crypto.createHash('sha256').update(raw).digest('hex');
64
+ }
65
+
66
+ export { Document, Schema } from 'mongoose';
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Simple global sequence generator for SeerEvent.seq
70
+ // ---------------------------------------------------------------------------
71
+ interface CounterDocument extends Document {
72
+ key: string;
73
+ value: number;
74
+ }
75
+
76
+ const CounterSchema = new Schema<CounterDocument>({
77
+ key: { type: String, required: true, unique: true },
78
+ value: { type: Number, required: true, default: 0 },
79
+ });
80
+
81
+ const CounterModel = mongoose.model<CounterDocument>('SeerCounter', CounterSchema);
82
+
83
+ export async function getNextSeq(key: string = 'seerEvent'): Promise<number> {
84
+ const doc = await CounterModel.findOneAndUpdate({ key }, { $inc: { value: 1 } }, { upsert: true, new: true }).exec();
85
+ return doc.value;
86
+ }
87
+
88
+ /**
89
+ * Returns a *new* object / array, does not mutate the input.
90
+ * - Root: `_id` (ObjectId) → `id: string`, and `_id` is removed
91
+ * - Nested objects: if they have `_id` as ObjectId, it becomes a string; `id` is also added
92
+ * - Any field that is an ObjectId anywhere in the tree becomes a string
93
+ */
94
+ function deepNormalizeIds(value: any, isRoot = false): any {
95
+ // Primitive or null → return as-is
96
+ if (value === null || typeof value !== 'object') {
97
+ return value;
98
+ }
99
+
100
+ // ObjectId → string
101
+ if (isObjectId(value)) {
102
+ return value.toString();
103
+ }
104
+
105
+ // Arrays → map
106
+ if (Array.isArray(value)) {
107
+ return value.map((item) => deepNormalizeIds(item, false));
108
+ }
109
+
110
+ // Non-plain objects (Date, Map, mongoose docs, etc.) → leave as-is
111
+ if (!isPlainObject(value)) {
112
+ return value;
113
+ }
114
+
115
+ // Plain object: build a new object
116
+ const out: any = {};
117
+
118
+ for (const [key, val] of Object.entries(value)) {
119
+ out[key] = deepNormalizeIds(val, false);
120
+ }
121
+
122
+ // Handle _id on this object (after children are normalized)
123
+ if ('_id' in out) {
124
+ const idStr = out._id.toString();
125
+
126
+ if (!out.id) {
127
+ out.id = idStr;
128
+ }
129
+
130
+ delete out._id;
131
+ }
132
+
133
+ return out;
134
+ }
135
+
136
+ export function addIdTransformHelpers<T>(schema: Schema<T>) {
137
+ // toJSON / toObject for non-lean docs
138
+ schema.set('toJSON', {
139
+ virtuals: true,
140
+ versionKey: false,
141
+ transform: (_doc, ret) => {
142
+ return deepNormalizeIds(ret, true);
143
+ },
144
+ });
145
+
146
+ schema.set('toObject', {
147
+ virtuals: true,
148
+ versionKey: false,
149
+ transform: (_doc, ret) => {
150
+ return deepNormalizeIds(ret, true);
151
+ },
152
+ });
153
+
154
+ // Query helper for lean() use-cases
155
+ (schema.query as any).asJSON = async function () {
156
+ const res = await (this as mongoose.Query<any, any>).lean().exec();
157
+
158
+ if (Array.isArray(res)) {
159
+ return res.map((doc) => deepNormalizeIds(doc, true));
160
+ }
161
+
162
+ return deepNormalizeIds(res, true);
163
+ };
164
+ }
165
+
166
+ function applyJsonValueFromSchema(schemaPath: any, value: any) {
167
+ if (!schemaPath) return value;
168
+
169
+ // Simple scalar ObjectId field
170
+ if (schemaPath.instance === 'ObjectId' && typeof value === 'string') {
171
+ // @ts-ignore
172
+ return new mongoose.Types.ObjectId(value);
173
+ }
174
+
175
+ // Array of ObjectId (or array of subdocs) – naive handling
176
+ if (schemaPath.$embeddedSchemaType && schemaPath.$embeddedSchemaType.instance === 'ObjectId') {
177
+ if (Array.isArray(value)) {
178
+ // @ts-ignore
179
+ return value.map((v) => (typeof v === 'string' ? new mongoose.Types.ObjectId(v) : v));
180
+ }
181
+ }
182
+
183
+ return value;
184
+ }
185
+
186
+ /**
187
+ * Mutates `target` (Mongoose doc or plain object) in-place based on `json`,
188
+ * using `schema` to convert string IDs back into ObjectIds where needed.
189
+ */
190
+ function applyJsonToTarget(schema: Schema, target: any, json: any, pathPrefix = '', isRoot = false) {
191
+ if (!json || typeof json !== 'object') return;
192
+
193
+ for (const [key, value] of Object.entries(json)) {
194
+ // we treat top-level `id` as synthetic; don't reassign _id on existing docs
195
+ if (isRoot && key === 'id') {
196
+ continue;
197
+ }
198
+
199
+ const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;
200
+ const schemaPath: any = schema.path(fullPath);
201
+
202
+ if (Array.isArray(value)) {
203
+ const arr: any[] = [];
204
+ for (const item of value) {
205
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
206
+ const child: any = {};
207
+ applyJsonToTarget(schema, child, item, fullPath, false);
208
+ arr.push(child);
209
+ } else {
210
+ arr.push(applyJsonValueFromSchema(schemaPath, item));
211
+ }
212
+ }
213
+ target[key] = arr;
214
+ } else if (value && typeof value === 'object') {
215
+ const child = target[key] ?? {};
216
+ target[key] = child;
217
+ applyJsonToTarget(schema, child, value, fullPath, false);
218
+ } else {
219
+ target[key] = applyJsonValueFromSchema(schemaPath, value);
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Public helper: apply JSON (from .asJSON/.toJSON) back onto a doc or plain object.
226
+ * - Will NOT change _id on existing docs (we skip root `id`)
227
+ * - Converts strings → ObjectId for fields whose schema type is ObjectId
228
+ */
229
+ export function applyJsonToDoc<T = any>(schema: Schema, target: T, json: any): T {
230
+ applyJsonToTarget(schema, target as any, json, '', true);
231
+ return target;
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Ontology / Cluster layer
236
+ // ---------------------------------------------------------------------------
237
+
238
+ const MIN_CLUSTER_TAG_SCORE = 0.3; // below this = low-confidence match (with tags)
239
+ const CLUSTER_AMBIGUITY_DELTA = 0.2; // if top-2 scores are too close, log ambiguity
240
+
241
+ export interface WeightedTag {
242
+ key: string;
243
+ weight: number; // 0..1, 1 = strongest
244
+ }
245
+
246
+ export interface PkEntry {
247
+ field: string; // e.g. 'token', 'characterId', 'assetId'
248
+ type: 'string' | 'number' | 'objectId' | 'boolean';
249
+ s?: string; // string-ish
250
+ n?: number; // numeric
251
+ o?: Types.ObjectId; // ObjectId
252
+ }
253
+
254
+ export interface ClusterDoc extends Document {
255
+ kind: string; // modelName, e.g. 'Item'
256
+ applicationId?: Types.ObjectId;
257
+
258
+ keys: string[];
259
+ primaryKey?: string;
260
+
261
+ tags: WeightedTag[];
262
+
263
+ currentId?: Types.ObjectId;
264
+ currentRevision?: number;
265
+
266
+ pk: PkEntry[];
267
+
268
+ createdDate: Date;
269
+ updatedDate: Date;
270
+ }
271
+
272
+ const ClusterSchema = new Schema<ClusterDoc>(
273
+ {
274
+ kind: {
275
+ type: String,
276
+ required: true,
277
+ index: true,
278
+ trim: true,
279
+ },
280
+
281
+ applicationId: { type: Schema.Types.ObjectId, ref: 'Application', index: true },
282
+
283
+ keys: {
284
+ type: [String],
285
+ default: [],
286
+ index: true,
287
+ } as any,
288
+
289
+ primaryKey: { type: String, index: true },
290
+
291
+ tags: {
292
+ type: [
293
+ {
294
+ key: { type: String, required: true },
295
+ weight: {
296
+ type: Number,
297
+ min: 0,
298
+ max: 1,
299
+ default: 1, // 1.0 = strong / core tag
300
+ },
301
+ },
302
+ ],
303
+ default: [],
304
+ } as any,
305
+
306
+ currentId: { type: Schema.Types.ObjectId },
307
+ currentRevision: { type: Number, default: 0 },
308
+
309
+ pk: {
310
+ type: [
311
+ {
312
+ field: { type: String, required: true },
313
+ type: {
314
+ type: String,
315
+ enum: ['string', 'number', 'objectId', 'boolean'],
316
+ required: true,
317
+ },
318
+ s: { type: String },
319
+ n: { type: Number },
320
+ o: { type: Schema.Types.ObjectId },
321
+ },
322
+ ],
323
+ default: [],
324
+ } as any,
325
+
326
+ createdDate: { type: Date, default: Date.now },
327
+ updatedDate: { type: Date, default: Date.now },
328
+ },
329
+ {
330
+ collection: 'Cluster',
331
+ }
332
+ );
333
+
334
+ // Unique: one cluster per (kind, applicationId, primaryKey)
335
+ ClusterSchema.index({ kind: 1, applicationId: 1, primaryKey: 1 }, { unique: true, sparse: true });
336
+
337
+ // Multikey indexes over pk entries
338
+ ClusterSchema.index({ kind: 1, applicationId: 1, 'pk.field': 1, 'pk.s': 1 });
339
+ ClusterSchema.index({ kind: 1, applicationId: 1, 'pk.field': 1, 'pk.n': 1 });
340
+ ClusterSchema.index({ kind: 1, applicationId: 1, 'pk.field': 1, 'pk.o': 1 });
341
+
342
+ addIdTransformHelpers(ClusterSchema as any);
343
+
344
+ export const ClusterModel = mongoose.model<ClusterDoc>('Cluster', ClusterSchema);
345
+
346
+ // ---------- ontology helpers -----------------------------------------------
347
+
348
+ function normalizeAppId(appId?: any): Types.ObjectId | undefined {
349
+ if (!appId) return undefined;
350
+ if (isObjectId(appId)) return appId as Types.ObjectId;
351
+
352
+ try {
353
+ // Force the "hex string" overload, not the deprecated number overload
354
+ const str = String(appId);
355
+ return new (mongoose.Types.ObjectId as any)(str);
356
+ } catch {
357
+ return undefined;
358
+ }
359
+ }
360
+
361
+ function getPkFields(schema: Schema): string[] {
362
+ const schemaAny = schema as any;
363
+ const fromOptions = schemaAny.options?.pkFields as string[] | undefined;
364
+ if (fromOptions && fromOptions.length) return fromOptions;
365
+ return ['key', 'name', 'token'];
366
+ }
367
+
368
+ function getKeyFields(schema: Schema): string[] {
369
+ const schemaAny = schema as any;
370
+ const fromOptions = schemaAny.options?.keyFields as string[] | undefined;
371
+ if (fromOptions && fromOptions.length) return fromOptions;
372
+ return ['key', 'name', 'token'];
373
+ }
374
+
375
+ // Build pk entries from a doc using pkFields + schema types
376
+ function toPkEntriesFromDoc(doc: any, schema: Schema): PkEntry[] {
377
+ const pkEntries: PkEntry[] = [];
378
+ const pkFields = getPkFields(schema);
379
+
380
+ for (const field of pkFields) {
381
+ const value = doc[field];
382
+ if (value === undefined || value === null) continue;
383
+
384
+ const schemaPath: any = schema.path(field);
385
+ const entry: PkEntry = { field, type: 'string' };
386
+
387
+ if (!schemaPath) {
388
+ entry.s = String(value);
389
+ } else if (schemaPath.instance === 'ObjectId') {
390
+ entry.type = 'objectId';
391
+ const oid = isObjectId(value) ? (value as Types.ObjectId) : new (mongoose.Types.ObjectId as any)(value as any);
392
+ entry.o = oid;
393
+ entry.s = oid.toString();
394
+ } else if (schemaPath.instance === 'Number') {
395
+ entry.type = 'number';
396
+ entry.n = Number(value);
397
+ entry.s = String(entry.n);
398
+ } else if (schemaPath.instance === 'Boolean') {
399
+ entry.type = 'boolean';
400
+ entry.s = value ? 'true' : 'false';
401
+ } else {
402
+ entry.type = 'string';
403
+ entry.s = String(value);
404
+ }
405
+
406
+ pkEntries.push(entry);
407
+ }
408
+
409
+ return pkEntries;
410
+ }
411
+
412
+ // Build keys (aliases) from doc using keyFields
413
+ function buildKeysFromDoc(doc: any, schema: Schema): string[] {
414
+ const keys: string[] = [];
415
+ const keyFields = getKeyFields(schema);
416
+
417
+ for (const field of keyFields) {
418
+ const v = doc[field];
419
+ if (typeof v === 'string' && v.trim()) {
420
+ if (!keys.includes(v)) keys.push(v);
421
+ }
422
+ }
423
+
424
+ return keys;
425
+ }
426
+
427
+ // Extract pk conditions (pk.elemMatch) from a filter
428
+ function buildPkConditionsFromFilter(filter: any, schema: Schema): { pkConditions: any[]; rawTags: any[] } {
429
+ const pkConditions: any[] = [];
430
+ const pkFields = getPkFields(schema);
431
+
432
+ const rawTags = (filter.tags || []) as any[];
433
+
434
+ for (const [field, value] of Object.entries(filter)) {
435
+ if (field === '_id' || field === 'applicationId' || field === 'tags') continue;
436
+ if (!pkFields.includes(field)) continue;
437
+
438
+ const schemaPath: any = schema.path(field);
439
+ const cond: any = { field };
440
+
441
+ if (!schemaPath) {
442
+ cond.type = 'string';
443
+ cond.s = String(value);
444
+ } else if (schemaPath.instance === 'ObjectId') {
445
+ cond.type = 'objectId';
446
+ if (isObjectId(value)) {
447
+ cond.o = value as Types.ObjectId;
448
+ } else {
449
+ const str = String(value);
450
+ cond.o = new (mongoose.Types.ObjectId as any)(str);
451
+ }
452
+ } else if (schemaPath.instance === 'Number') {
453
+ cond.type = 'number';
454
+ cond.n = Number(value);
455
+ } else if (schemaPath.instance === 'Boolean') {
456
+ cond.type = 'boolean';
457
+ cond.s = value ? 'true' : 'false';
458
+ } else {
459
+ cond.type = 'string';
460
+ cond.s = String(value);
461
+ }
462
+
463
+ pkConditions.push(cond);
464
+ }
465
+
466
+ return { pkConditions, rawTags };
467
+ }
468
+
469
+ // Upsert or update a cluster for an entity doc.
470
+ // - Respects revision: cluster.currentRevision only moves forward.
471
+ export async function upsertClusterForEntity(kind: string, schema: Schema, doc: any): Promise<ClusterDoc> {
472
+ const appId = normalizeAppId(doc.applicationId);
473
+ const keys = buildKeysFromDoc(doc, schema);
474
+ const primaryKey = keys[0];
475
+
476
+ const pkEntries = toPkEntriesFromDoc(doc, schema);
477
+
478
+ const query: any = { kind };
479
+ if (appId) query.applicationId = appId;
480
+ if (primaryKey) query.primaryKey = primaryKey;
481
+
482
+ let cluster = await ClusterModel.findOne(query).exec();
483
+
484
+ const docRevision = typeof doc.revision === 'number' && !Number.isNaN(doc.revision) ? doc.revision : 1;
485
+
486
+ if (!cluster) {
487
+ cluster = new ClusterModel({
488
+ kind,
489
+ applicationId: appId,
490
+ keys,
491
+ primaryKey,
492
+ tags: [], // will merge from doc.tags below
493
+ pk: pkEntries,
494
+ currentId: doc._id,
495
+ currentRevision: docRevision,
496
+ });
497
+ } else {
498
+ // merge keys
499
+ const keySet = new Set(cluster.keys || []);
500
+ for (const k of keys) keySet.add(k);
501
+ cluster.keys = Array.from(keySet);
502
+
503
+ // merge pk entries by (field,type)
504
+ const pkMap = new Map<string, PkEntry>();
505
+ for (const entry of cluster.pk || []) {
506
+ pkMap.set(`${entry.field}:${entry.type}`, entry);
507
+ }
508
+ for (const entry of pkEntries) {
509
+ pkMap.set(`${entry.field}:${entry.type}`, entry);
510
+ }
511
+ cluster.pk = Array.from(pkMap.values());
512
+
513
+ // only advance currentId if revision is newer
514
+ if (!cluster.currentRevision || docRevision > cluster.currentRevision) {
515
+ cluster.currentId = doc._id;
516
+ cluster.currentRevision = docRevision;
517
+ }
518
+
519
+ if (primaryKey) cluster.primaryKey = primaryKey;
520
+ }
521
+
522
+ // merge tags (WeightedTag) from doc.tags / doc.meta.tags
523
+ const existingTagMap = new Map<string, number>();
524
+ for (const t of cluster.tags || []) {
525
+ const w = typeof t.weight === 'number' ? t.weight : 1;
526
+ existingTagMap.set(t.key, Math.max(0, Math.min(1, w)));
527
+ }
528
+
529
+ const rawTags = (doc.tags || doc.meta?.tags || []) as any[];
530
+ for (const t of rawTags) {
531
+ let key: string;
532
+ let weight = 1;
533
+ if (typeof t === 'string') {
534
+ key = t;
535
+ } else if (t && typeof t === 'object') {
536
+ key = t.key ?? t.name ?? String(t);
537
+ if (typeof t.weight === 'number') {
538
+ weight = Math.max(0, Math.min(1, t.weight));
539
+ }
540
+ } else {
541
+ key = String(t);
542
+ }
543
+ const existing = existingTagMap.get(key) ?? 0;
544
+ existingTagMap.set(key, Math.max(existing, weight));
545
+ }
546
+
547
+ cluster.tags = Array.from(existingTagMap.entries()).map(([key, weight]) => ({
548
+ key,
549
+ weight,
550
+ }));
551
+
552
+ cluster.updatedDate = new Date();
553
+ await cluster.save();
554
+
555
+ // link back to entity if it supports clusterId
556
+ if ('clusterId' in doc && !doc.clusterId) {
557
+ doc.clusterId = cluster._id;
558
+ }
559
+
560
+ return cluster;
561
+ }
562
+
563
+ // Resolve clusters for a filter (PK + tags), with scores.
564
+ export async function resolveClustersForFilter(
565
+ kind: string,
566
+ schema: Schema,
567
+ applicationId: Types.ObjectId | string | undefined,
568
+ filter: any
569
+ ): Promise<(ClusterDoc & { score?: number })[]> {
570
+ const appId = normalizeAppId(applicationId);
571
+
572
+ const { pkConditions, rawTags } = buildPkConditionsFromFilter(filter, schema);
573
+ const tagKeys = rawTags.map((t: any) => (typeof t === 'string' ? t : (t.key ?? t.name ?? String(t))));
574
+
575
+ const baseMatch: any = { kind };
576
+ if (appId) baseMatch.applicationId = appId;
577
+
578
+ const and: any[] = [];
579
+
580
+ for (const cond of pkConditions) {
581
+ and.push({
582
+ pk: { $elemMatch: cond },
583
+ });
584
+ }
585
+
586
+ if (and.length) {
587
+ baseMatch.$and = and;
588
+ }
589
+
590
+ const pipeline: any[] = [{ $match: baseMatch }];
591
+
592
+ const hasTags = tagKeys.length > 0;
593
+ if (hasTags) {
594
+ // clusters must share at least one tag
595
+ pipeline[0].$match['tags.key'] = { $in: tagKeys };
596
+ pipeline.push({
597
+ $addFields: {
598
+ score: {
599
+ $sum: {
600
+ $map: {
601
+ input: '$tags',
602
+ as: 't',
603
+ in: {
604
+ $cond: [{ $in: ['$$t.key', tagKeys] }, '$$t.weight', 0],
605
+ },
606
+ },
607
+ },
608
+ },
609
+ },
610
+ });
611
+ pipeline.push({ $sort: { score: -1, updatedDate: -1 } });
612
+ } else {
613
+ pipeline.push({ $sort: { updatedDate: -1 } });
614
+ }
615
+
616
+ const clusters = await ClusterModel.aggregate(pipeline).exec();
617
+ return clusters as any;
618
+ }
619
+
620
+ // ---------------------------------------------------------------------------
621
+ // Schema & model helpers
622
+ // ---------------------------------------------------------------------------
623
+
624
+ const CommonFields = {
625
+ // Ontology / cluster linkage + revision
626
+ clusterId: { type: Schema.Types.ObjectId, ref: 'Cluster', index: true },
627
+ revision: { type: Number, default: 1 },
628
+
629
+ key: { type: String, minlength: 1, maxlength: 200, trim: true },
630
+ name: { type: String },
631
+ description: { type: String },
632
+ status: {
633
+ type: String,
634
+ default: 'Active', // Default value set here
635
+ enum: ['Paused', 'Pending', 'Active', 'Archived'],
636
+ },
637
+ data: { type: Object, default: {} }, // Default value set here
638
+ meta: { type: Object, default: {} }, // Default value set here
639
+ merkleLeaf: { type: String },
640
+ merkleIndex: { type: Number },
641
+ createdById: { type: Schema.Types.ObjectId, ref: 'Profile' },
642
+ editedById: { type: Schema.Types.ObjectId, ref: 'Profile' },
643
+ deletedById: { type: Schema.Types.ObjectId, ref: 'Profile' },
644
+ createdDate: { type: Date, default: Date.now },
645
+ updatedDate: { type: Date },
646
+ deletedDate: { type: Date },
647
+ };
648
+
649
+ const EntityFields = {
650
+ applicationId: { type: Schema.Types.ObjectId, ref: 'Application', required: true },
651
+ ownerId: { type: Schema.Types.ObjectId, ref: 'Profile' },
652
+ };
653
+
654
+ interface CacheConfig {
655
+ enabled?: boolean;
656
+ ttlMs?: number;
657
+ }
658
+
659
+ interface CustomSchemaOptions extends SchemaOptions {
660
+ extend?: 'EntityFields' | 'CommonFields';
661
+ indexes?: { [field: string]: any }[];
662
+ virtuals?: VirtualOptions[];
663
+ pre?: { method: PreHookMethod | RegExp; handler: (this: Document, next: any) => void }[];
664
+
665
+ // Ontology config
666
+ pkFields?: string[];
667
+ keyFields?: string[];
668
+
669
+ // Per-model cache configuration
670
+ cache?: CacheConfig;
671
+ }
672
+
673
+ export function createSchema<T>(
674
+ name: string,
675
+ customFields: SchemaDefinition<T> = {} as SchemaDefinition<T>,
676
+ options: CustomSchemaOptions = {}
677
+ ): Schema<T> {
678
+ const extend = options.extend !== undefined ? options.extend : 'EntityFields';
679
+ const collectionName = options.collection || name;
680
+
681
+ let schema: Schema<T>;
682
+
683
+ const schemaOptions: any = {
684
+ minimize: false,
685
+ timestamps: { createdAt: 'createdDate', updatedAt: 'updatedDate' },
686
+ collection: collectionName,
687
+ };
688
+
689
+ // propagate ontology config into schema.options
690
+ if (options.pkFields) schemaOptions.pkFields = options.pkFields;
691
+ if (options.keyFields) schemaOptions.keyFields = options.keyFields;
692
+
693
+ if (extend === 'EntityFields') {
694
+ schema = new Schema<T>(
695
+ {
696
+ ...CommonFields,
697
+ ...EntityFields,
698
+ ...customFields,
699
+ } as SchemaDefinition<T>,
700
+ schemaOptions
701
+ );
702
+ } else {
703
+ schema = new Schema<T>(
704
+ {
705
+ ...CommonFields,
706
+ ...customFields,
707
+ } as SchemaDefinition<T>,
708
+ schemaOptions
709
+ );
710
+ }
711
+
712
+ // schema.plugin(require('mongoose-autopopulate'));
713
+
714
+ schema.set('toJSON', {
715
+ virtuals: true, // Include virtual fields
716
+ versionKey: false, // Remove the __v version field
717
+ transform: (doc, ret) => {
718
+ // @ts-ignore
719
+ ret.id = ret._id.toString(); // Assign _id to id
720
+ delete ret._id; // Remove _id from the output
721
+ return ret;
722
+ },
723
+ });
724
+
725
+ schema.set('toObject', {
726
+ virtuals: true, // Include virtual fields
727
+ versionKey: false, // Remove the __v version field
728
+ transform: (doc, ret) => {
729
+ // @ts-ignore
730
+ ret.id = ret._id.toString(); // Assign _id to id
731
+ delete ret._id; // Remove _id from the output
732
+ return ret;
733
+ },
734
+ });
735
+
736
+ addIdTransformHelpers(schema);
737
+
738
+ // Apply indexes
739
+ if (options.indexes) {
740
+ options.indexes.forEach((index) => schema.index(index));
741
+ } else {
742
+ schema.index({ key: 1 });
743
+ schema.index({ name: 1 });
744
+ schema.index({ status: 1 });
745
+ }
746
+
747
+ // Apply virtuals
748
+ if (options.virtuals) {
749
+ options.virtuals.forEach((virtual) => {
750
+ const virtualOptions: any = {
751
+ localField: virtual.localField || `${toCamelCase(virtual.name)}Id`,
752
+ foreignField: virtual.foreignField || '_id',
753
+ justOne: virtual.justOne !== undefined ? virtual.justOne : !pluralize.isPlural(virtual.name),
754
+ };
755
+
756
+ if (virtual.refPath) {
757
+ virtualOptions.refPath = virtual.refPath;
758
+ } else if (virtual.ref) {
759
+ virtualOptions.ref = virtual.ref;
760
+ } else if (schema.path(virtual.name + 'Id')) {
761
+ virtualOptions.ref = pluralize.singular(schema.path(virtual.name + 'Id').options.ref);
762
+ } else {
763
+ virtualOptions.ref = pluralize.singular(virtual.name.charAt(0).toUpperCase() + virtual.name.slice(1));
764
+ }
765
+
766
+ if (virtual.options) {
767
+ virtualOptions.options = virtual.options;
768
+ }
769
+
770
+ if (virtual.match) {
771
+ virtualOptions.match = virtual.match;
772
+ }
773
+
774
+ const schemaVirtual = schema.virtual(virtual.name, virtualOptions);
775
+
776
+ if (virtual.get) {
777
+ schemaVirtual.get(virtual.get);
778
+ }
779
+
780
+ if (virtual.set) {
781
+ schemaVirtual.set(virtual.set);
782
+ }
783
+ });
784
+ }
785
+
786
+ // Apply pre middleware
787
+ if (options.pre) {
788
+ options.pre.forEach((preHook) => {
789
+ schema.pre(preHook.method as any, preHook.handler); // Casting to 'any' for compatibility with Mongoose types
790
+ });
791
+ }
792
+
793
+ return schema;
794
+ }
795
+
796
+ const modelMap: Record<string, Model<any>> = {};
797
+
798
+ export function createModel<T extends Document>(
799
+ key: string,
800
+ schemaFields: SchemaDefinition<T> = {} as SchemaDefinition<T>,
801
+ options: CustomSchemaOptions = {}
802
+ ) {
803
+ if (modelMap[key]) return modelMap[key];
804
+
805
+ const schema = createSchema<T>(key, schemaFields, options);
806
+
807
+ // NEW: pass cache config to Model
808
+ const res = new Model<T>(mongoose.model<T>(key, schema), { cache: options.cache });
809
+
810
+ modelMap[key] = res;
811
+
812
+ return res;
813
+ }
814
+
815
+ // ---------------------------------------------------------------------------
816
+ // zkSNARK verification hook (pluggable)
817
+ // ---------------------------------------------------------------------------
818
+
819
+ export type ZkProofPayload = {
820
+ walletAddress: string; // address that "owns" this operation
821
+ proof: any; // zk proof blob
822
+ publicSignals?: any; // optional public inputs from the circuit
823
+ };
824
+
825
+ export type ZkVerifyContext = {
826
+ kind: string; // model name, e.g. 'Item'
827
+ operation: 'create' | 'update';
828
+ filter?: any;
829
+ update?: any;
830
+ doc?: any; // doc(s) being created, for createWithProof
831
+ };
832
+
833
+ export type ZkVerifier = (payload: ZkProofPayload, ctx: ZkVerifyContext) => Promise<boolean> | boolean;
834
+
835
+ let globalZkVerifier: ZkVerifier | null = null;
836
+
837
+ // Call this during app bootstrap to plug in your real zk verifier.
838
+ export function setZkVerifier(verifier: ZkVerifier) {
839
+ globalZkVerifier = verifier;
840
+ }
841
+
842
+ async function verifyZkOrThrow(payload: ZkProofPayload, ctx: ZkVerifyContext): Promise<void> {
843
+ if (!globalZkVerifier) {
844
+ // If no verifier is registered, treat as "no zk enforcement" (or throw if you want hard fail).
845
+ return;
846
+ }
847
+
848
+ const ok = await globalZkVerifier(payload, ctx);
849
+ if (!ok) {
850
+ throw new Error('Invalid zk proof for operation');
851
+ }
852
+ }
853
+
854
+ // ---------------------------------------------------------------------------
855
+ // Model wrapper
856
+ // ---------------------------------------------------------------------------
857
+
858
+ type ModelConfig = {
859
+ cache?: CacheConfig;
860
+ };
861
+
862
+ export class Model<T extends Document> {
863
+ protected model: MongooseModel<T>;
864
+ protected schema: Schema;
865
+ public filters: Record<string, any> = {};
866
+ public filterOmitModels: string[] = ['Omniverse', 'Metaverse', 'Application'];
867
+ public collection: Collection;
868
+
869
+ private docSaveQueue = new WeakMap<Document, Promise<T>>();
870
+
871
+ // NEW: cache config + store
872
+ private cacheConfig: { enabled: boolean; ttlMs: number };
873
+ private cache = new Map<string, { doc: T; fetchedAt: number }>();
874
+
875
+ constructor(model: MongooseModel<T>, config: ModelConfig = {}) {
876
+ this.model = model;
877
+ this.collection = model.collection;
878
+ this.schema = model.schema;
879
+
880
+ this.cacheConfig = {
881
+ enabled: config.cache?.enabled ?? false,
882
+ ttlMs: config.cache?.ttlMs ?? 60_000, // default 60s
883
+ };
884
+ }
885
+
886
+ private get kind(): string {
887
+ return this.model.modelName;
888
+ }
889
+
890
+ private buildCacheKey(id: Types.ObjectId | string, applicationId?: any): string {
891
+ const appId = normalizeAppId(applicationId);
892
+ const appKey = appId ? appId.toString() : 'global';
893
+ const idStr = typeof id === 'string' ? id : id.toString();
894
+ return `${appKey}:${idStr}`;
895
+ }
896
+
897
+ private getFromCache(key: string): T | null {
898
+ if (!this.cacheConfig.enabled) return null;
899
+ const entry = this.cache.get(key);
900
+ if (!entry) return null;
901
+
902
+ if (Date.now() - entry.fetchedAt > this.cacheConfig.ttlMs) {
903
+ this.cache.delete(key);
904
+ return null;
905
+ }
906
+
907
+ return entry.doc;
908
+ }
909
+
910
+ private setCache(key: string, doc: T): void {
911
+ if (!this.cacheConfig.enabled) return;
912
+ this.cache.set(key, { doc, fetchedAt: Date.now() });
913
+ }
914
+
915
+ private isClusterEnabled(): boolean {
916
+ const name = this.model.modelName;
917
+ if (name === 'Cluster') return false;
918
+ return !this.filterOmitModels.includes(name);
919
+ }
920
+
921
+ // // Cluster-aware find
922
+ // find(
923
+ // filter: FilterQuery<T> = {},
924
+ // projection?: ProjectionType<T> | null,
925
+ // options?: mongoose.QueryOptions
926
+ // ): Query<T[], T> {
927
+ // const finalFilter = this.applyDefaultFilters(filter);
928
+ // const q = this.model.find(finalFilter, projection, options);
929
+ // return this.wrapQueryWithCluster(q as any, false) as any;
930
+ // }
931
+
932
+ // // Cluster-aware findOne
933
+ // findOne(
934
+ // filter: FilterQuery<T> = {},
935
+ // projection?: ProjectionType<T> | null,
936
+ // options?: QueryOptions
937
+ // ): Query<T | null, T> {
938
+ // const finalFilter = this.applyDefaultFilters(filter);
939
+ // const q = this.model.findOne(finalFilter, projection, options);
940
+ // return this.wrapQueryWithCluster(q as any, true) as any;
941
+ // }
942
+
943
+ // // Cluster-aware findById (implemented as findOne({_id}))
944
+ // findById(
945
+ // id: Types.ObjectId | string,
946
+ // projection?: ProjectionType<T> | null,
947
+ // options?: QueryOptions
948
+ // ): Query<T | null, T> {
949
+ // const filter: any = { _id: id };
950
+ // const finalFilter = this.applyDefaultFilters(filter);
951
+ // const q = this.model.findOne(finalFilter, projection, options);
952
+ // return this.wrapQueryWithCluster(q as any, true) as any;
953
+ // }
954
+
955
+ // Wrap a query so that exec() does ontology resolution for find/findOne
956
+ private wrapQueryWithCluster(q: Query<any, T>, isFindOne: boolean): Query<any, T> {
957
+ if (!this.isClusterEnabled()) return q;
958
+
959
+ const wrapper = this;
960
+ const rawExec = q.exec;
961
+
962
+ q.exec = async function execWithCluster(this: any, ...args: any[]) {
963
+ const op = this.op; // 'find', 'findOne', etc.
964
+ if (op !== 'find' && op !== 'findOne') {
965
+ return rawExec.apply(this, args);
966
+ }
967
+
968
+ let filter = this.getFilter ? this.getFilter() : this._conditions || {};
969
+ filter = { ...filter }; // clone
970
+
971
+ // direct _id-only with no tags: try cache first, skip cluster
972
+ const hasIdOnly =
973
+ filter &&
974
+ Object.keys(filter).length === 1 &&
975
+ Object.prototype.hasOwnProperty.call(filter, '_id') &&
976
+ !Object.prototype.hasOwnProperty.call(filter, 'tags');
977
+
978
+ if (hasIdOnly) {
979
+ const appId =
980
+ filter.applicationId ??
981
+ (wrapper.filterOmitModels.includes(wrapper.kind) ? undefined : wrapper.filters.applicationId);
982
+
983
+ const idCond = filter._id;
984
+
985
+ // Attempt cache lookup
986
+ if (wrapper.cacheConfig.enabled && idCond) {
987
+ if (op === 'findOne') {
988
+ if (!idCond.$in && !idCond.$nin && typeof idCond !== 'object') {
989
+ const key = wrapper.buildCacheKey(idCond, appId);
990
+ const cached = wrapper.getFromCache(key);
991
+ if (cached) return cached;
992
+ }
993
+ } else if (op === 'find') {
994
+ if (idCond && typeof idCond === 'object' && '$in' in idCond) {
995
+ const ids: any[] = idCond.$in || [];
996
+ const results: any[] = [];
997
+ let allHit = true;
998
+
999
+ for (const id of ids) {
1000
+ const key = wrapper.buildCacheKey(id, appId);
1001
+ const cached = wrapper.getFromCache(key);
1002
+ if (!cached) {
1003
+ allHit = false;
1004
+ break;
1005
+ }
1006
+ results.push(cached);
1007
+ }
1008
+
1009
+ if (allHit) {
1010
+ return results;
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ // No cache hit, fallback to DB and then populate cache (for simple cases)
1017
+ const res = await rawExec.apply(this, args);
1018
+
1019
+ if (wrapper.cacheConfig.enabled && res) {
1020
+ if (op === 'findOne' && res && !Array.isArray(res)) {
1021
+ const key = wrapper.buildCacheKey(res._id, appId);
1022
+ wrapper.setCache(key, res);
1023
+ } else if (op === 'find' && Array.isArray(res)) {
1024
+ for (const d of res) {
1025
+ if (!d || !d._id) continue;
1026
+ const key = wrapper.buildCacheKey(d._id, appId);
1027
+ wrapper.setCache(key, d);
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ return res;
1033
+ }
1034
+
1035
+ // get pk/tags presence
1036
+ const { pkConditions, rawTags } = buildPkConditionsFromFilter(filter, wrapper.schema);
1037
+ const hasPk = pkConditions.length > 0;
1038
+ const hasTags = rawTags.length > 0;
1039
+
1040
+ // If no pk fields and no tags, don't do ontology resolution
1041
+ if (!hasPk && !hasTags) {
1042
+ return rawExec.apply(this, args);
1043
+ }
1044
+
1045
+ // Figure out applicationId
1046
+ const applicationId =
1047
+ filter.applicationId ??
1048
+ (wrapper.filterOmitModels.includes(wrapper.kind) ? undefined : wrapper.filters.applicationId);
1049
+
1050
+ try {
1051
+ const clusters = await resolveClustersForFilter(wrapper.kind, wrapper.schema, applicationId as any, filter);
1052
+
1053
+ if (!clusters.length) {
1054
+ // Fallback: raw query, then backfill Cluster
1055
+ const res = await rawExec.apply(this, args);
1056
+ if (res) {
1057
+ if (Array.isArray(res)) {
1058
+ for (const doc of res) {
1059
+ if (doc && wrapper.isClusterEnabled()) {
1060
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, doc);
1061
+ }
1062
+ }
1063
+ } else if (wrapper.isClusterEnabled()) {
1064
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, res);
1065
+ }
1066
+ }
1067
+ return res;
1068
+ }
1069
+
1070
+ const best = clusters[0];
1071
+ const bestScore = typeof best.score === 'number' ? best.score : 0;
1072
+ const secondScore =
1073
+ clusters.length > 1 && typeof clusters[1].score === 'number' ? (clusters[1].score as number) : 0;
1074
+
1075
+ if (hasTags && bestScore < MIN_CLUSTER_TAG_SCORE) {
1076
+ console.warn('[Cluster] low-confidence resolution', {
1077
+ model: wrapper.kind,
1078
+ filter,
1079
+ bestClusterId: best._id?.toString(),
1080
+ bestScore,
1081
+ });
1082
+
1083
+ const res = await rawExec.apply(this, args);
1084
+ if (res) {
1085
+ if (Array.isArray(res)) {
1086
+ for (const doc of res) {
1087
+ if (doc && wrapper.isClusterEnabled()) {
1088
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, doc);
1089
+ }
1090
+ }
1091
+ } else if (wrapper.isClusterEnabled()) {
1092
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, res);
1093
+ }
1094
+ }
1095
+ return res;
1096
+ }
1097
+
1098
+ if (hasTags && Math.abs(bestScore - secondScore) < CLUSTER_AMBIGUITY_DELTA && clusters.length > 1) {
1099
+ console.warn('[Cluster] ambiguous resolution', {
1100
+ model: wrapper.kind,
1101
+ filter,
1102
+ bestClusterId: best._id?.toString(),
1103
+ bestScore,
1104
+ secondScore,
1105
+ candidateIds: clusters.slice(0, 3).map((c) => c._id?.toString()),
1106
+ });
1107
+ }
1108
+
1109
+ if (isFindOne) {
1110
+ if (!best.currentId) {
1111
+ const res = await rawExec.apply(this, args);
1112
+ if (res && wrapper.isClusterEnabled()) {
1113
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, res);
1114
+ }
1115
+ return res;
1116
+ }
1117
+
1118
+ const newFilter: any = { _id: best.currentId };
1119
+ if (applicationId) newFilter.applicationId = applicationId;
1120
+ this._conditions = newFilter;
1121
+
1122
+ // Try cache with resolved _id
1123
+ if (wrapper.cacheConfig.enabled) {
1124
+ const key = wrapper.buildCacheKey(best.currentId, applicationId);
1125
+ const cached = wrapper.getFromCache(key);
1126
+ if (cached) return cached;
1127
+ }
1128
+
1129
+ const res = await rawExec.apply(this, args);
1130
+
1131
+ if (res && wrapper.cacheConfig.enabled) {
1132
+ const key = wrapper.buildCacheKey(best.currentId, applicationId);
1133
+ wrapper.setCache(key, res);
1134
+ }
1135
+
1136
+ return res;
1137
+ } else {
1138
+ const ids = clusters.map((c) => c.currentId).filter((id): id is Types.ObjectId => !!id);
1139
+
1140
+ if (!ids.length) {
1141
+ const res = await rawExec.apply(this, args);
1142
+ if (res && Array.isArray(res) && wrapper.isClusterEnabled()) {
1143
+ for (const doc of res) {
1144
+ await upsertClusterForEntity(wrapper.kind, wrapper.schema, doc);
1145
+ }
1146
+ }
1147
+ return res;
1148
+ }
1149
+
1150
+ const newFilter: any = { _id: { $in: ids } };
1151
+ if (applicationId) newFilter.applicationId = applicationId;
1152
+ this._conditions = newFilter;
1153
+
1154
+ // Optional: serve from cache if we have all ids cached
1155
+ if (wrapper.cacheConfig.enabled) {
1156
+ const results: T[] = [];
1157
+ let allHit = true;
1158
+
1159
+ for (const id of ids) {
1160
+ const key = wrapper.buildCacheKey(id, applicationId);
1161
+ const cached = wrapper.getFromCache(key);
1162
+ if (!cached) {
1163
+ allHit = false;
1164
+ break;
1165
+ }
1166
+ results.push(cached);
1167
+ }
1168
+
1169
+ if (allHit) return results;
1170
+ }
1171
+
1172
+ const res = await rawExec.apply(this, args);
1173
+
1174
+ if (res && Array.isArray(res) && wrapper.cacheConfig.enabled) {
1175
+ for (const d of res) {
1176
+ if (!d || !d._id) continue;
1177
+ const key = wrapper.buildCacheKey(d._id, applicationId);
1178
+ wrapper.setCache(key, d);
1179
+ }
1180
+ }
1181
+
1182
+ return res;
1183
+ }
1184
+ } catch (err) {
1185
+ console.warn('[Cluster] error during ontology resolution, falling back', {
1186
+ model: wrapper.kind,
1187
+ filter,
1188
+ error: (err as Error).message,
1189
+ });
1190
+ return rawExec.apply(this, args);
1191
+ }
1192
+ };
1193
+
1194
+ return q;
1195
+ }
1196
+
1197
+ populate(
1198
+ docs: T | T[],
1199
+ options: string | mongoose.PopulateOptions | string[] | mongoose.PopulateOptions[]
1200
+ ): Promise<T | T[]> {
1201
+ // If options is an array of strings, convert it to an array of PopulateOptions
1202
+ if (Array.isArray(options) && typeof options[0] === 'string') {
1203
+ options = (options as string[]).map((path) => ({ path }));
1204
+ }
1205
+
1206
+ return this.model.populate(docs, options as string | mongoose.PopulateOptions | mongoose.PopulateOptions[]);
1207
+ }
1208
+
1209
+ // New method to directly access a related model
1210
+ related(name: string) {
1211
+ return mongoose.model(name);
1212
+ }
1213
+
1214
+ // New method to get related documents via virtuals
1215
+ findWithRelations(filter: FilterQuery<T> = {}, relations: string[] = [], options?: QueryOptions): Query<T[], T> {
1216
+ return this.find(filter, null, options).populate(relations.join(' '));
1217
+ }
1218
+
1219
+ findOneWithRelations(
1220
+ filter: FilterQuery<T> = {},
1221
+ relations: string[] = [],
1222
+ options?: QueryOptions
1223
+ ): Query<T | null, T> {
1224
+ return this.findOne(filter, null, options).populate(relations.join(' '));
1225
+ }
1226
+
1227
+ // Overridden exec method (raw)
1228
+ async exec(query: Query<any, T>): Promise<any> {
1229
+ return query.exec();
1230
+ }
1231
+
1232
+ private applyDefaultFilters(filter: FilterQuery<T> = {}): FilterQuery<T> {
1233
+ const f: any = { ...filter };
1234
+
1235
+ // applicationId scoping for most models
1236
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1237
+ if (f.applicationId === undefined) {
1238
+ f.applicationId = this.filters.applicationId;
1239
+ }
1240
+ }
1241
+
1242
+ // Default: ignore archived records, unless caller explicitly set status
1243
+ if (this.schema.path('status') && f.status === undefined) {
1244
+ f.status = { $ne: 'Archived' } as any;
1245
+ }
1246
+
1247
+ return f;
1248
+ }
1249
+
1250
+ // Cluster-aware find: returns Query, ontology resolution happens in exec()
1251
+ // Override the find method to include filters
1252
+ find(
1253
+ filter: FilterQuery<T> = {},
1254
+ projection?: ProjectionType<T> | null,
1255
+ options?: mongoose.QueryOptions
1256
+ ): Query<T[], T> {
1257
+ const finalFilter = this.applyDefaultFilters(filter);
1258
+ console.log('find', finalFilter);
1259
+ return this.model.find(finalFilter, projection, options);
1260
+ }
1261
+
1262
+ // Override the findOne method to include filters
1263
+ findOne(
1264
+ filter: FilterQuery<T> = {},
1265
+ projection?: ProjectionType<T> | null,
1266
+ options?: QueryOptions
1267
+ ): Query<T | null, T> {
1268
+ const finalFilter = this.applyDefaultFilters(filter);
1269
+ console.log('findOne', finalFilter);
1270
+ return this.model.findOne(finalFilter, projection, options);
1271
+ }
1272
+
1273
+ // Override the findById method so it also ignores Archived by default
1274
+ findById(
1275
+ id: Types.ObjectId | string,
1276
+ projection?: ProjectionType<T> | null,
1277
+ options?: QueryOptions
1278
+ ): Query<T | null, T> {
1279
+ const filter: any = { _id: id };
1280
+ const finalFilter = this.applyDefaultFilters(filter);
1281
+ return this.model.findOne(finalFilter, projection, options);
1282
+ }
1283
+
1284
+ // Raw find (no cluster) if you ever need it
1285
+ findRaw(
1286
+ filter: FilterQuery<T> = {},
1287
+ projection?: ProjectionType<T> | null,
1288
+ options?: mongoose.QueryOptions
1289
+ ): Query<T[], T> {
1290
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1291
+ // @ts-ignore
1292
+ filter.applicationId = this.filters.applicationId;
1293
+ }
1294
+ return this.model.find(filter, projection, options);
1295
+ }
1296
+
1297
+ // Override the findOneAndUpdate method to include filters (raw)
1298
+ findOneAndUpdate(
1299
+ filter: FilterQuery<T>,
1300
+ update: UpdateQuery<T> | mongoose.UpdateWithAggregationPipeline,
1301
+ options?: QueryOptions & { new?: boolean }
1302
+ ): Query<T | null, T> {
1303
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1304
+ // @ts-ignore
1305
+ filter.applicationId = this.filters.applicationId;
1306
+ }
1307
+
1308
+ return this.model.findOneAndUpdate(filter, update, options);
1309
+ }
1310
+
1311
+ // Override the findOneAndDelete method to include filters (raw)
1312
+ findOneAndDelete(filter: FilterQuery<T>, options?: QueryOptions): Query<T | null, T> {
1313
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1314
+ // @ts-ignore
1315
+ filter.applicationId = this.filters.applicationId;
1316
+ }
1317
+
1318
+ return this.model.findOneAndDelete(filter, options);
1319
+ }
1320
+
1321
+ // Override the findByIdAndUpdate method to include filters (raw)
1322
+ findByIdAndUpdate(
1323
+ id: Types.ObjectId | string,
1324
+ update: UpdateQuery<T> | mongoose.UpdateWithAggregationPipeline,
1325
+ options?: QueryOptions & { new?: boolean }
1326
+ ): Query<T | null, T> {
1327
+ const filter: FilterQuery<T> = { _id: id } as FilterQuery<T>;
1328
+
1329
+ if (!this.filterOmitModels.includes(this.model.modelName)) {
1330
+ // @ts-ignore
1331
+ filter.applicationId = this.filters.applicationId;
1332
+ }
1333
+
1334
+ return this.model.findOneAndUpdate(filter, update, options);
1335
+ }
1336
+
1337
+ // Override the findByIdAndDelete method to include filters (raw)
1338
+ findByIdAndDelete(id: Types.ObjectId | string, options?: QueryOptions): Query<T | null, T> {
1339
+ const filter: FilterQuery<T> = { _id: id } as FilterQuery<T>;
1340
+
1341
+ if (!this.filterOmitModels.includes(this.model.modelName)) {
1342
+ // @ts-ignore
1343
+ filter.applicationId = this.filters.applicationId;
1344
+ }
1345
+
1346
+ return this.model.findOneAndDelete(filter, options);
1347
+ }
1348
+
1349
+ create(doc: Partial<T>): Promise<T>;
1350
+ create(doc: Partial<T>[]): Promise<T[]>;
1351
+ create(doc: Partial<T> | Partial<T>[]): Promise<T | T[]> {
1352
+ console.log('create', this.filters.applicationId);
1353
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1354
+ if (Array.isArray(doc)) {
1355
+ // @ts-ignore
1356
+ doc.forEach((d) => (d.applicationId = this.filters.applicationId));
1357
+ } else {
1358
+ // @ts-ignore
1359
+ doc.applicationId = this.filters.applicationId;
1360
+ }
1361
+ }
1362
+
1363
+ const p = this.model.create(doc as any) as Promise<T | T[]>;
1364
+
1365
+ if (!this.isClusterEnabled()) {
1366
+ return p;
1367
+ }
1368
+
1369
+ return p.then(async (res: any) => {
1370
+ const docsArray = Array.isArray(res) ? res : [res];
1371
+ for (const d of docsArray) {
1372
+ // Pass the raw Mongoose document so _id, applicationId, revision, etc. are intact
1373
+ const cluster = await upsertClusterForEntity(this.kind, this.schema, d);
1374
+ if ('clusterId' in d && !d.clusterId) {
1375
+ d.clusterId = cluster._id;
1376
+ await d.save();
1377
+ }
1378
+
1379
+ // NEW: seed cache
1380
+ if (this.cacheConfig.enabled && d._id) {
1381
+ const appId = (d as any).applicationId ?? this.filters.applicationId;
1382
+ const key = this.buildCacheKey(d._id, appId);
1383
+ this.setCache(key, d);
1384
+ }
1385
+ }
1386
+ return res;
1387
+ });
1388
+ }
1389
+
1390
+ // Override the upsert method to include filters
1391
+ async upsert(
1392
+ filter: FilterQuery<T> = {},
1393
+ create: Partial<T> = {},
1394
+ update: UpdateQuery<T> = {},
1395
+ options: QueryOptions = {}
1396
+ ): Promise<T> {
1397
+ const existing = await this.findOne(filter, null, options).exec();
1398
+ if (existing) {
1399
+ await this.updateOne(filter, update, options).exec();
1400
+ return (await this.findOne(filter, null, options).exec()) as T;
1401
+ } else {
1402
+ return this.create(create) as Promise<T>;
1403
+ }
1404
+ }
1405
+
1406
+ // Override the updateOne method to include filters (raw)
1407
+ updateOne(
1408
+ filter: FilterQuery<T>,
1409
+ update: UpdateQuery<T> | UpdateWithAggregationPipeline,
1410
+ options?: any
1411
+ ): Query<UpdateWriteOpResult, T> {
1412
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1413
+ // @ts-ignore
1414
+ filter.applicationId = this.filters.applicationId;
1415
+ // @ts-ignore
1416
+ update.applicationId = this.filters.applicationId;
1417
+ }
1418
+
1419
+ return this.model.updateOne(filter, update, options);
1420
+ }
1421
+
1422
+ // Override the updateMany method to include filters (raw)
1423
+ updateMany(
1424
+ filter: FilterQuery<T>,
1425
+ update: UpdateQuery<T> | UpdateWithAggregationPipeline,
1426
+ options?: any
1427
+ ): Query<UpdateWriteOpResult, T> {
1428
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1429
+ // @ts-ignore
1430
+ filter.applicationId = this.filters.applicationId;
1431
+ }
1432
+
1433
+ return this.model.updateMany(filter, update, options);
1434
+ }
1435
+
1436
+ // Count documents method
1437
+ countDocuments(): any {
1438
+ return this.model.countDocuments();
1439
+ }
1440
+
1441
+ // Method for handling aggregate (raw)
1442
+ aggregate(...props: any[]): any {
1443
+ return this.model.aggregate(...props);
1444
+ }
1445
+
1446
+ // Method for handling where conditions (raw)
1447
+ where(arg1: string, arg2?: any): Query<T[], T>;
1448
+ where(arg1: object): Query<T[], T>;
1449
+ where(arg1: string | object, arg2?: any): Query<T[], T> {
1450
+ return this.model.where(arg1 as any, arg2);
1451
+ }
1452
+
1453
+ // Find all documents method (raw)
1454
+ findAll(): Query<T[], T> {
1455
+ return this.model.find();
1456
+ }
1457
+
1458
+ async findOneProxy(
1459
+ filter: FilterQuery<T> = {},
1460
+ projection?: ProjectionType<T> | null,
1461
+ options?: QueryOptions
1462
+ ): Promise<(T & Record<string, any>) | null> {
1463
+ const doc = await this.findOne(filter, projection, options).exec();
1464
+ if (!doc) return null;
1465
+
1466
+ // Return a doc+model merged proxy
1467
+ return createDocProxy(doc, this);
1468
+ }
1469
+
1470
+ /**
1471
+ * saveQueued(doc):
1472
+ * - If there's an existing in-flight save promise for the same doc, wait until that finishes.
1473
+ * - Then call doc.save().
1474
+ * - Store this new save promise in the WeakMap so future calls chain onto it.
1475
+ */
1476
+ public async saveQueued(doc: T): Promise<T> {
1477
+ const existingPromise = this.docSaveQueue.get(doc) ?? Promise.resolve<T>(doc);
1478
+
1479
+ // Chain a new promise onto the existing one
1480
+ const newSavePromise = existingPromise
1481
+ .then(async () => {
1482
+ // By the time we reach here, previous saves are done.
1483
+ return await doc.save(); // Mongoose's normal doc.save()
1484
+ })
1485
+ .catch((err) => {
1486
+ // If the previous promise was rejected, propagate the error
1487
+ // but remove from the queue so a later call can try again
1488
+ this.docSaveQueue.delete(doc);
1489
+ throw err;
1490
+ });
1491
+
1492
+ // Store the new promise in the queue
1493
+ this.docSaveQueue.set(doc, newSavePromise);
1494
+
1495
+ // Return the doc once the new promise completes
1496
+ return newSavePromise;
1497
+ }
1498
+
1499
+ // -------------------------------------------------------------------------
1500
+ // zkSNARK-aware helpers
1501
+ // -------------------------------------------------------------------------
1502
+
1503
+ async createWithProof(doc: Partial<T>, proof: ZkProofPayload): Promise<T>;
1504
+ async createWithProof(docs: Partial<T>[], proof: ZkProofPayload): Promise<T[]>;
1505
+ async createWithProof(docOrDocs: Partial<T> | Partial<T>[], proof: ZkProofPayload): Promise<T | T[]> {
1506
+ await verifyZkOrThrow(proof, {
1507
+ kind: this.kind,
1508
+ operation: 'create',
1509
+ doc: docOrDocs,
1510
+ });
1511
+
1512
+ return this.create(docOrDocs as any);
1513
+ }
1514
+
1515
+ async updateOneWithProof(
1516
+ filter: FilterQuery<T>,
1517
+ update: UpdateQuery<T> | UpdateWithAggregationPipeline,
1518
+ proof: ZkProofPayload,
1519
+ options?: any
1520
+ ): Promise<UpdateWriteOpResult> {
1521
+ await verifyZkOrThrow(proof, {
1522
+ kind: this.kind,
1523
+ operation: 'update',
1524
+ filter,
1525
+ update,
1526
+ });
1527
+
1528
+ return this.updateOne(filter, update, options).exec();
1529
+ }
1530
+
1531
+ // ---------------------------------------------------------------------------
1532
+ // JSON helpers (normalize ids, hide asJSON typings from callers)
1533
+ // ---------------------------------------------------------------------------
1534
+
1535
+ /**
1536
+ * Find many docs and return normalized JSON (id instead of _id, ObjectIds → string).
1537
+ * This hides the mongoose Query/asJSON typing from callers.
1538
+ */
1539
+ async findJSON(
1540
+ filter: FilterQuery<T> = {},
1541
+ projection?: ProjectionType<T> | null,
1542
+ options?: QueryOptions
1543
+ ): Promise<any[]> {
1544
+ // Use the same default filter logic (applicationId, status != Archived)
1545
+ const q = this.find(filter, projection, options) as any;
1546
+
1547
+ // If query helper asJSON exists, use it
1548
+ if (typeof q.asJSON === 'function') {
1549
+ return q.asJSON();
1550
+ }
1551
+
1552
+ // Fallback: lean() + deepNormalizeIds
1553
+ const res = await q.lean().exec();
1554
+ if (Array.isArray(res)) {
1555
+ return res.map((doc: any) => deepNormalizeIds(doc, true));
1556
+ }
1557
+ return [];
1558
+ }
1559
+
1560
+ /**
1561
+ * Find one doc and return normalized JSON (or null).
1562
+ */
1563
+ async findOneJSON(
1564
+ filter: FilterQuery<T> = {},
1565
+ projection?: ProjectionType<T> | null,
1566
+ options?: QueryOptions
1567
+ ): Promise<any | null> {
1568
+ const q = this.findOne(filter, projection, options) as any;
1569
+
1570
+ if (typeof q.asJSON === 'function') {
1571
+ return q.asJSON();
1572
+ }
1573
+
1574
+ const res = await q.lean().exec();
1575
+ if (!res) return null;
1576
+ return deepNormalizeIds(res, true);
1577
+ }
1578
+
1579
+ /**
1580
+ * Count documents with the same default filter behavior (applicationId, status != Archived).
1581
+ */
1582
+ async countDocumentsFiltered(filter: FilterQuery<T> = {}): Promise<number> {
1583
+ const finalFilter = this.applyDefaultFilters(filter);
1584
+ return this.model.countDocuments(finalFilter).exec();
1585
+ }
1586
+ insertMany(docs: any[], options?: any): Promise<any[]> {
1587
+ if (!Array.isArray(docs) || docs.length === 0) return Promise.resolve([]);
1588
+
1589
+ // auto-inject applicationId like create() does
1590
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1591
+ for (const d of docs) {
1592
+ if (d.applicationId === undefined) d.applicationId = this.filters.applicationId;
1593
+ }
1594
+ }
1595
+
1596
+ return (this.model as any).insertMany(docs, options);
1597
+ }
1598
+ /**
1599
+ * bulkWrite proxy:
1600
+ * - Applies default filters (applicationId scoping + status != Archived) to each op.filter
1601
+ * - Ensures applicationId is set on $setOnInsert for upserts (like create())
1602
+ */
1603
+ bulkWrite(
1604
+ operations: any[], // you can type as AnyBulkWriteOperation<T>[] if you want
1605
+ options: any = {}
1606
+ ): Promise<any> {
1607
+ const ops = (operations || []).map((op) => {
1608
+ // updateOne / updateMany
1609
+ if (op.updateOne?.filter) {
1610
+ op.updateOne.filter = this.applyDefaultFilters(op.updateOne.filter);
1611
+
1612
+ // ensure $setOnInsert.applicationId for upserts
1613
+ const upsert = !!op.updateOne.upsert;
1614
+ if (upsert && this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1615
+ const u = op.updateOne.update || {};
1616
+ op.updateOne.update = u;
1617
+
1618
+ // If using aggregation pipeline updates, skip (hard to inject safely)
1619
+ if (!Array.isArray(u)) {
1620
+ u.$setOnInsert = u.$setOnInsert || {};
1621
+ if (u.$setOnInsert.applicationId === undefined) {
1622
+ u.$setOnInsert.applicationId = this.filters.applicationId;
1623
+ }
1624
+ }
1625
+ }
1626
+
1627
+ return op;
1628
+ }
1629
+
1630
+ if (op.updateMany?.filter) {
1631
+ op.updateMany.filter = this.applyDefaultFilters(op.updateMany.filter);
1632
+ return op;
1633
+ }
1634
+
1635
+ // deleteOne / deleteMany
1636
+ if (op.deleteOne?.filter) {
1637
+ op.deleteOne.filter = this.applyDefaultFilters(op.deleteOne.filter);
1638
+ return op;
1639
+ }
1640
+ if (op.deleteMany?.filter) {
1641
+ op.deleteMany.filter = this.applyDefaultFilters(op.deleteMany.filter);
1642
+ return op;
1643
+ }
1644
+
1645
+ // replaceOne (rare)
1646
+ if (op.replaceOne?.filter) {
1647
+ op.replaceOne.filter = this.applyDefaultFilters(op.replaceOne.filter);
1648
+
1649
+ const upsert = !!op.replaceOne.upsert;
1650
+ if (upsert && this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1651
+ // replacement doc must include applicationId
1652
+ if (op.replaceOne.replacement && op.replaceOne.replacement.applicationId === undefined) {
1653
+ op.replaceOne.replacement.applicationId = this.filters.applicationId;
1654
+ }
1655
+ }
1656
+
1657
+ return op;
1658
+ }
1659
+
1660
+ // insertOne
1661
+ if (op.insertOne?.document) {
1662
+ if (this.filters.applicationId && !this.filterOmitModels.includes(this.model.modelName)) {
1663
+ if (op.insertOne.document.applicationId === undefined) {
1664
+ op.insertOne.document.applicationId = this.filters.applicationId;
1665
+ }
1666
+ }
1667
+ return op;
1668
+ }
1669
+
1670
+ return op;
1671
+ });
1672
+
1673
+ // Delegate to real mongoose model bulkWrite
1674
+ return (this.model as any).bulkWrite(ops, options);
1675
+ }
1676
+ }
1677
+
1678
+ // ---------------------------------------------------------------------------
1679
+ // Virtual helpers & proxies
1680
+ // ---------------------------------------------------------------------------
1681
+
1682
+ export const addTagVirtuals = (modelName: string) => [
1683
+ {
1684
+ name: 'tags',
1685
+ ref: 'Node',
1686
+ localField: '_id',
1687
+ foreignField: 'from',
1688
+ justOne: false,
1689
+ match: { relationKey: 'tag', fromModel: modelName },
1690
+ },
1691
+ ];
1692
+
1693
+ export const addApplicationVirtual = () => [
1694
+ {
1695
+ name: 'application',
1696
+ ref: 'Application',
1697
+ localField: 'applicationId',
1698
+ foreignField: '_id',
1699
+ justOne: true,
1700
+ },
1701
+ ];
1702
+
1703
+ export function createDocProxy<T extends Document>(doc: T, modelWrapper: Model<T>) {
1704
+ return new Proxy(doc as T & Record<string, any>, {
1705
+ get(target, prop, receiver) {
1706
+ // 1. If the property exists on the doc itself (fields, doc methods, etc.)
1707
+ if (prop in target) {
1708
+ return Reflect.get(target, prop, receiver);
1709
+ }
1710
+
1711
+ // 2. Otherwise, if the property is in your Model wrapper, return that
1712
+ if (prop in modelWrapper) {
1713
+ const val = Reflect.get(modelWrapper, prop, modelWrapper);
1714
+ // If it's a function (like saveQueued), bind it or adapt it
1715
+ if (typeof val === 'function') {
1716
+ // For example, if we want `proxyDoc.saveQueued()` to automatically
1717
+ // call modelWrapper.saveQueued(doc), we can do:
1718
+ if (prop === 'saveQueued') {
1719
+ return function (...args: any[]) {
1720
+ // automatically pass `doc` as the first arg
1721
+ // @ts-ignore
1722
+ return modelWrapper.saveQueued(doc, ...args);
1723
+ };
1724
+ }
1725
+ // Otherwise just bind the method normally
1726
+ return val.bind(modelWrapper);
1727
+ }
1728
+ return val;
1729
+ }
1730
+
1731
+ // 3. If not on doc or modelWrapper, return undefined
1732
+ return undefined;
1733
+ },
1734
+ });
1735
+ }