@atscript/mongo 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,304 +22,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
 
24
24
  //#endregion
25
- const __atscript_core = __toESM(require("@atscript/core"));
26
- const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"));
25
+ const __atscript_utils_db = __toESM(require("@atscript/utils-db"));
27
26
  const mongodb = __toESM(require("mongodb"));
27
+ const __atscript_typescript_utils = __toESM(require("@atscript/typescript/utils"));
28
28
 
29
- //#region packages/mongo/src/plugin/annotations.ts
30
- const analyzers = [
31
- "lucene.standard",
32
- "lucene.simple",
33
- "lucene.whitespace",
34
- "lucene.english",
35
- "lucene.french",
36
- "lucene.german",
37
- "lucene.italian",
38
- "lucene.portuguese",
39
- "lucene.spanish",
40
- "lucene.chinese",
41
- "lucene.hindi",
42
- "lucene.bengali",
43
- "lucene.russian",
44
- "lucene.arabic"
45
- ];
46
- const annotations = {
47
- collection: new __atscript_core.AnnotationSpec({
48
- description: "Marks an interface as a **MongoDB collection**.\n\n- Use together with `@db.table \"name\"` which provides the collection name.\n- Automatically injects a **non-optional** `_id` field if not explicitly defined.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n\n**Example:**\n```atscript\n@db.table \"users\"\n@db.mongo.collection\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
49
- nodeType: ["interface"],
50
- validate(token, args, doc) {
51
- const parent = token.parentNode;
52
- const struc = parent?.getDefinition();
53
- const errors = [];
54
- if ((0, __atscript_core.isInterface)(parent) && parent.props.has("_id") && (0, __atscript_core.isStructure)(struc)) {
55
- const _id = parent.props.get("_id");
56
- const isOptional = !!_id.token("optional");
57
- if (isOptional) errors.push({
58
- message: `[db.mongo] _id can't be optional in Mongo Collection`,
59
- severity: 1,
60
- range: _id.token("identifier").range
61
- });
62
- const definition = _id.getDefinition();
63
- if (!definition) return errors;
64
- let wrongType = false;
65
- if ((0, __atscript_core.isRef)(definition)) {
66
- const def = doc.unwindType(definition.id, definition.chain)?.def;
67
- if ((0, __atscript_core.isPrimitive)(def) && !["string", "number"].includes(def.config.type)) wrongType = true;
68
- } else wrongType = true;
69
- if (wrongType) errors.push({
70
- message: `[db.mongo] _id must be of type string, number or mongo.objectId`,
71
- severity: 1,
72
- range: _id.token("identifier").range
73
- });
74
- }
75
- return errors;
76
- },
77
- modify(token, args, doc) {
78
- const parent = token.parentNode;
79
- const struc = parent?.getDefinition();
80
- if ((0, __atscript_core.isInterface)(parent) && !parent.props.has("_id") && (0, __atscript_core.isStructure)(struc)) struc.addVirtualProp({
81
- name: "_id",
82
- type: "mongo.objectId",
83
- documentation: "Mongodb Primary Key ObjectId"
84
- });
85
- }
86
- }),
87
- autoIndexes: new __atscript_core.AnnotationSpec({
88
- description: "Switch on/off the automatic index creation. Works with as-mongo moost controller.\n\nDefault: true",
89
- nodeType: ["interface"],
90
- argument: {
91
- name: "type",
92
- type: "boolean",
93
- description: "On/Off the automatic index creation"
94
- }
95
- }),
96
- index: { text: new __atscript_core.AnnotationSpec({
97
- description: "Creates a **legacy MongoDB text index** for full-text search with optional **weight** specification.\n\nUse this when you need per-field weight control. For simple full-text indexing without weights, use the generic `@db.index.fulltext` instead.\n\n**Example:**\n```atscript\n@db.mongo.index.text 5\nbio: string\n```\n",
98
- nodeType: ["prop"],
99
- argument: {
100
- optional: true,
101
- name: "weight",
102
- type: "number",
103
- description: "Field importance in search results (higher = more relevant). Defaults to `1`."
104
- }
105
- }) },
106
- search: {
107
- dynamic: new __atscript_core.AnnotationSpec({
108
- description: "Creates a **dynamic MongoDB Search Index** that applies to the entire collection.\n\n- **Indexes all text fields automatically** (no need to specify fields).\n- Supports **language analyzers** for text tokenization.\n- Enables **fuzzy search** (typo tolerance) if needed.\n\n**Example:**\n```atscript\n@db.mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
109
- nodeType: ["interface"],
110
- multiple: false,
111
- argument: [{
112
- optional: true,
113
- name: "analyzer",
114
- type: "string",
115
- description: "The **text analyzer** for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, etc.",
116
- values: analyzers
117
- }, {
118
- optional: true,
119
- name: "fuzzy",
120
- type: "number",
121
- description: "Maximum typo tolerance (`0-2`). Defaults to `0` (no fuzzy search).\n\n- `0` → Exact match required.\n- `1` → Allows small typos (e.g., `\"mongo\"` ≈ `\"mango\"`).\n- `2` → More typo tolerance (e.g., `\"mongodb\"` ≈ `\"mangodb\"`)."
122
- }]
123
- }),
124
- static: new __atscript_core.AnnotationSpec({
125
- description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@db.mongo.search.text` annotation.\n\n- **Creates a named search index** for full-text search.\n- **Specify analyzers and fuzzy search** behavior at the index level.\n- **Fields must explicitly use `@db.mongo.search.text`** to be included in this search index.\n\n**Example:**\n```atscript\n@db.mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
126
- nodeType: ["interface"],
127
- multiple: true,
128
- argument: [
129
- {
130
- optional: true,
131
- name: "analyzer",
132
- type: "string",
133
- description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
134
- values: analyzers
135
- },
136
- {
137
- optional: true,
138
- name: "fuzzy",
139
- type: "number",
140
- description: "Maximum typo tolerance (`0-2`). **Defaults to `0` (no fuzzy matching).**\n\n- `0` → No typos allowed (exact match required).\n- `1` → Allows small typos (e.g., \"mongo\" ≈ \"mango\").\n- `2` → More typo tolerance (e.g., \"mongodb\" ≈ \"mangodb\")."
141
- },
142
- {
143
- optional: true,
144
- name: "indexName",
145
- type: "string",
146
- description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
147
- }
148
- ]
149
- }),
150
- text: new __atscript_core.AnnotationSpec({
151
- description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@db.mongo.search.static`.\n\n- **The field has to reference an existing search index name**.\n- If index name is not defined, a new search index with default attributes will be created.\n\n**Example:**\n```atscript\n@db.mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
152
- nodeType: ["prop"],
153
- multiple: true,
154
- argument: [{
155
- optional: true,
156
- name: "analyzer",
157
- type: "string",
158
- description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
159
- values: analyzers
160
- }, {
161
- optional: true,
162
- name: "indexName",
163
- type: "string",
164
- description: "The **name of the search index** defined in `@db.mongo.search.static`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
165
- }]
166
- }),
167
- vector: new __atscript_core.AnnotationSpec({
168
- description: "Creates a **MongoDB Vector Search Index** for **semantic search, embeddings, and AI-powered search**.\n\n- Each field that stores vector embeddings **must define its own vector index**.\n- Supports **cosine similarity, Euclidean distance, and dot product similarity**.\n- Vector fields must be an **array of numbers**.\n\n**Example:**\n```atscript\n@db.mongo.search.vector 512, \"cosine\"\nembedding: mongo.vector\n```\n",
169
- nodeType: ["prop"],
170
- multiple: false,
171
- argument: [
172
- {
173
- optional: false,
174
- name: "dimensions",
175
- type: "number",
176
- description: "The **number of dimensions in the vector** (e.g., 512 for OpenAI embeddings).",
177
- values: [
178
- "512",
179
- "768",
180
- "1024",
181
- "1536",
182
- "3072",
183
- "4096"
184
- ]
185
- },
186
- {
187
- optional: true,
188
- name: "similarity",
189
- type: "string",
190
- description: "The **similarity metric** used for vector search. Defaults to `\"cosine\"`.\n\n**Available options:** `\"cosine\"`, `\"euclidean\"`, `\"dotProduct\"`.",
191
- values: [
192
- "cosine",
193
- "euclidean",
194
- "dotProduct"
195
- ]
196
- },
197
- {
198
- optional: true,
199
- name: "indexName",
200
- type: "string",
201
- description: "The **name of the vector search index** (optional, defaults to property name)."
202
- }
203
- ]
204
- }),
205
- filter: new __atscript_core.AnnotationSpec({
206
- description: "Assigns a field as a **filter field** for a **MongoDB Vector Search Index**.\n\n- The assigned field **must be indexed** for efficient filtering.\n- Filters allow vector search queries to return results **only within a specific category, user group, or tag**.\n- The vector index must be defined using `@db.mongo.search.vector`.\n\n**Example:**\n```atscript\n@db.mongo.search.vector 512, \"cosine\"\nembedding: number[]\n\n@db.mongo.search.filter \"embedding\"\ncategory: string\n```\n",
207
- nodeType: ["prop"],
208
- multiple: true,
209
- argument: [{
210
- optional: false,
211
- name: "indexName",
212
- type: "string",
213
- description: "The **name of the vector search index** this field should be used as a filter for."
214
- }]
215
- })
216
- },
217
- patch: { strategy: new __atscript_core.AnnotationSpec({
218
- description: "Defines the **patching strategy** for updating MongoDB documents.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@db.mongo.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
219
- nodeType: ["prop"],
220
- multiple: false,
221
- argument: {
222
- name: "strategy",
223
- type: "string",
224
- description: "The **patch strategy** for this field: `\"replace\"` (default) or `\"merge\"`.",
225
- values: ["replace", "merge"]
226
- },
227
- validate(token, args, doc) {
228
- const field = token.parentNode;
229
- const errors = [];
230
- const definition = field.getDefinition();
231
- if (!definition) return errors;
232
- let wrongType = false;
233
- if ((0, __atscript_core.isRef)(definition)) {
234
- const def = doc.unwindType(definition.id, definition.chain)?.def;
235
- if (!(0, __atscript_core.isStructure)(def) && !(0, __atscript_core.isInterface)(def) && !(0, __atscript_core.isArray)(def)) wrongType = true;
236
- } else if (!(0, __atscript_core.isStructure)(definition) && !(0, __atscript_core.isInterface)(definition) && !(0, __atscript_core.isArray)(definition)) wrongType = true;
237
- if (wrongType) errors.push({
238
- message: `[db.mongo] type of object or array expected when using @db.mongo.patch.strategy`,
239
- severity: 1,
240
- range: token.range
241
- });
242
- return errors;
243
- }
244
- }) },
245
- array: { uniqueItems: new __atscript_core.AnnotationSpec({
246
- description: "Marks an **array field** as containing *globally unique items* when handling **patch `$insert` operations**.\n\n- Forces the patcher to use **set-semantics** (`$setUnion`) instead of a plain append, so duplicates are silently skipped.\n- Has **no effect** on `$replace`, `$update`, or `$remove`.\n- If the array's element type already defines one or more `@expect.array.key` properties, *uniqueness is implied* and this annotation is unnecessary (but harmless).\n\n**Example:**\n```atscript\n@db.mongo.array.uniqueItems\ntags: string[]\n\n// Later in a patch payload …\n{\n $insert: {\n tags: [\"mongo\", \"mongo\"] // second \"mongo\" is ignored\n }\n}\n```\n",
247
- nodeType: ["prop"],
248
- multiple: false,
249
- validate(token, args, doc) {
250
- const field = token.parentNode;
251
- const errors = [];
252
- const definition = field.getDefinition();
253
- if (!definition) return errors;
254
- let wrongType = false;
255
- if ((0, __atscript_core.isRef)(definition)) {
256
- const def = doc.unwindType(definition.id, definition.chain)?.def;
257
- if (!(0, __atscript_core.isArray)(def)) wrongType = true;
258
- } else if (!(0, __atscript_core.isArray)(definition)) wrongType = true;
259
- if (wrongType) errors.push({
260
- message: `[db.mongo] type of array expected when using @db.mongo.array.uniqueItems`,
261
- severity: 1,
262
- range: token.range
263
- });
264
- return errors;
265
- }
266
- }) }
267
- };
268
-
269
- //#endregion
270
- //#region packages/mongo/src/plugin/primitives.ts
271
- const primitives = { mongo: { extensions: {
272
- objectId: {
273
- type: "string",
274
- documentation: "Represents a **MongoDB ObjectId**.\n\n- Stored as a **string** but can be converted to an ObjectId at runtime.\n- Useful for handling `_id` fields and queries that require ObjectId conversion.\n- Automatically converts string `_id` values into **MongoDB ObjectId** when needed.\n\n**Example:**\n```atscript\nuserId: mongo.objectId\n```\n",
275
- annotations: { "expect.pattern": { pattern: "^[a-fA-F0-9]{24}$" } }
276
- },
277
- vector: {
278
- type: {
279
- kind: "array",
280
- of: "number"
281
- },
282
- documentation: "Represents a **MongoDB Vector (Array of Numbers)** for **Vector Search**.\n\n- Equivalent to `number[]` but explicitly used for **vector embeddings**.\n\n**Example:**\n```atscript\nembedding: mongo.vector\n```\n"
283
- }
284
- } } };
285
-
286
- //#endregion
287
- //#region packages/mongo/src/plugin/index.ts
288
- const MongoPlugin = () => ({
289
- name: "mongo",
290
- config() {
291
- return {
292
- primitives,
293
- annotations: { db: { mongo: annotations } }
294
- };
295
- }
296
- });
297
-
298
- //#endregion
299
29
  //#region packages/mongo/src/lib/validate-plugins.ts
300
30
  const validateMongoIdPlugin = (ctx, def, value) => {
301
31
  if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof mongodb.ObjectId ? value.toString() : value);
302
32
  };
303
- const validateMongoUniqueArrayItemsPlugin = (ctx, def, value) => {
304
- if (def.metadata.has("db.mongo.array.uniqueItems") && def.type.kind === "array") {
305
- if (Array.isArray(value)) {
306
- const separator = "▼↩";
307
- const seen = new Set();
308
- const keyProps = CollectionPatcher.getKeyProps(def);
309
- for (const item of value) {
310
- let key = "";
311
- if (keyProps.size > 0) for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
312
- else key = JSON.stringify(item);
313
- if (seen.has(key)) {
314
- ctx.error(`Duplicate items are not allowed`);
315
- return false;
316
- }
317
- seen.add(key);
318
- }
319
- }
320
- }
321
- return undefined;
322
- };
323
33
 
324
34
  //#endregion
325
35
  //#region packages/mongo/src/lib/collection-patcher.ts
@@ -334,45 +44,29 @@ else obj[key] = value;
334
44
  return obj;
335
45
  }
336
46
  var CollectionPatcher = class CollectionPatcher {
337
- /**
338
- * Extract a set of *key properties* (annotated with `@expect.array.key`) from an
339
- * array‐of‐objects type definition. These keys uniquely identify an element
340
- * inside the array and are later used for `$update`, `$remove` and `$upsert`.
341
- *
342
- * @param def Atscript array type
343
- * @returns Set of property names marked as keys; empty set if none
344
- */ static getKeyProps(def) {
345
- if (def.type.of.type.kind === "object") {
346
- const objType = def.type.of.type;
347
- const keyProps = new Set();
348
- for (const [key, val] of objType.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
349
- return keyProps;
350
- }
351
- return new Set();
352
- }
353
47
  /**
354
48
  * Build a runtime *Validator* that understands the extended patch payload.
355
49
  *
356
50
  * * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
357
- * * Honors `db.mongo.patch.strategy === "merge"` metadata.
51
+ * * Honors `db.patch.strategy === "merge"` metadata.
358
52
  *
359
53
  * @param collection Target collection wrapper
360
54
  * @returns Atscript Validator
361
- */ static prepareValidator(collection) {
362
- return collection.createValidator({
363
- plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
55
+ */ static prepareValidator(context) {
56
+ return context.createValidator({
57
+ plugins: [validateMongoIdPlugin],
364
58
  replace: (def, path) => {
365
59
  if (path === "" && def.type.kind === "object") {
366
60
  const obj = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(def.metadata);
367
61
  for (const [prop, type] of def.type.props.entries()) obj.prop(prop, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
368
62
  return obj.$type;
369
63
  }
370
- if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
64
+ if (def.type.kind === "array" && context.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
371
65
  const defArray = def;
372
- const mergeStrategy = defArray.metadata.get("db.mongo.patch.strategy") === "merge";
66
+ const mergeStrategy = defArray.metadata.get("db.patch.strategy") === "merge";
373
67
  function getPatchType() {
374
- const isPrimitive$1 = (0, __atscript_typescript_utils.isAnnotatedTypeOfPrimitive)(defArray.type.of);
375
- if (isPrimitive$1) return (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
68
+ const isPrimitive = (0, __atscript_typescript_utils.isAnnotatedTypeOfPrimitive)(defArray.type.of);
69
+ if (isPrimitive) return (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
376
70
  if (defArray.type.of.type.kind === "object") {
377
71
  const objType = defArray.type.of.type;
378
72
  const t = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(defArray.type.of.metadata);
@@ -390,7 +84,7 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
390
84
  }
391
85
  return def;
392
86
  },
393
- partial: (def, path) => path !== "" && def.metadata.get("db.mongo.patch.strategy") === "merge"
87
+ partial: (def, path) => path !== "" && def.metadata.get("db.patch.strategy") === "merge"
394
88
  });
395
89
  }
396
90
  /**
@@ -420,14 +114,12 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
420
114
  * @param val Value to be written
421
115
  * @private
422
116
  */ _set(key, val) {
423
- for (const pipe of this.updatePipeline) {
424
- if (!pipe.$set) pipe.$set = {};
425
- if (!pipe.$set[key]) {
426
- pipe.$set[key] = val;
427
- return;
428
- }
117
+ if (this.currentSetStage && !(key in this.currentSetStage.$set)) {
118
+ this.currentSetStage.$set[key] = val;
119
+ return;
429
120
  }
430
- this.updatePipeline.push({ $set: { [key]: val } });
121
+ this.currentSetStage = { $set: { [key]: val } };
122
+ this.updatePipeline.push(this.currentSetStage);
431
123
  }
432
124
  /**
433
125
  * Recursively walk through the patch *payload* and convert it into `$set`/…
@@ -442,8 +134,8 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
442
134
  const key = evalKey(_key);
443
135
  const flatType = this.collection.flatMap.get(key);
444
136
  const topLevelArray = flatType?.metadata?.get("db.mongo.__topLevelArray");
445
- if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value);
446
- else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("db.mongo.patch.strategy") === "merge") this.flattenPayload(value, key);
137
+ if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value, flatType);
138
+ else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
447
139
  else if (key !== "_id") this._set(key, value);
448
140
  }
449
141
  return this.updatePipeline;
@@ -455,19 +147,19 @@ else if (key !== "_id") this._set(key, value);
455
147
  * @param key Dotted path to the array field
456
148
  * @param value Payload slice for that field
457
149
  * @private
458
- */ parseArrayPatch(key, value) {
459
- const flatType = this.collection.flatMap.get(key);
150
+ */ parseArrayPatch(key, value, flatType) {
460
151
  const toRemove = value.$remove;
461
152
  const toReplace = value.$replace;
462
153
  const toInsert = value.$insert;
463
154
  const toUpsert = value.$upsert;
464
155
  const toUpdate = value.$update;
465
- const keyProps = flatType?.type.kind === "array" ? CollectionPatcher.getKeyProps(flatType) : new Set();
466
- this._remove(key, toRemove, keyProps);
156
+ const keyProps = flatType.type.kind === "array" ? (0, __atscript_utils_db.getKeyProps)(flatType) : new Set();
157
+ const keys = keyProps.size > 0 ? [...keyProps] : [];
158
+ this._remove(key, toRemove, keys, flatType);
467
159
  this._replace(key, toReplace);
468
- this._insert(key, toInsert, keyProps);
469
- this._upsert(key, toUpsert, keyProps);
470
- this._update(key, toUpdate, keyProps);
160
+ this._insert(key, toInsert, keys, flatType);
161
+ this._upsert(key, toUpsert, keys, flatType);
162
+ this._update(key, toUpdate, keys, flatType);
471
163
  }
472
164
  /**
473
165
  * Build an *aggregation‐expression* that checks equality by **all** keys in
@@ -480,7 +172,8 @@ else if (key !== "_id") this._set(key, value);
480
172
  * @param left Base token for *left* expression (e.g. `"$$el"`)
481
173
  * @param right Base token for *right* expression (e.g. `"$$this"`)
482
174
  */ _keysEqual(keys, left, right) {
483
- return keys.map((k) => ({ $eq: [`${left}.${k}`, `${right}.${k}`] })).reduce((acc, cur) => acc ? { $and: [acc, cur] } : cur);
175
+ const eqs = keys.map((k) => ({ $eq: [`${left}.${k}`, `${right}.${k}`] }));
176
+ return eqs.length === 1 ? eqs[0] : { $and: eqs };
484
177
  }
485
178
  /**
486
179
  * `$replace` – overwrite the entire array with `input`.
@@ -495,20 +188,19 @@ else if (key !== "_id") this._set(key, value);
495
188
  * `$insert`
496
189
  * - plain append → $concatArrays
497
190
  * - unique / keyed → delegate to _upsert (insert-or-update)
498
- */ _insert(key, input, keyProps) {
191
+ */ _insert(key, input, keys, flatType) {
499
192
  if (!input?.length) return;
500
- const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("db.mongo.array.uniqueItems");
501
- if (uniqueItems || keyProps.size > 0) this._upsert(key, input, keyProps);
193
+ const uniqueItems = flatType.metadata?.has("expect.array.uniqueItems");
194
+ if (uniqueItems || keys.length > 0) this._upsert(key, input, keys, flatType);
502
195
  else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
503
196
  }
504
197
  /**
505
198
  * `$upsert`
506
199
  * - keyed → remove existing matching by key(s) then append candidate
507
200
  * - unique → $setUnion (deep equality)
508
- */ _upsert(key, input, keyProps) {
201
+ */ _upsert(key, input, keys, _flatType) {
509
202
  if (!input?.length) return;
510
- if (keyProps.size > 0) {
511
- const keys = [...keyProps];
203
+ if (keys.length > 0) {
512
204
  this._set(key, { $reduce: {
513
205
  input,
514
206
  initialValue: { $ifNull: [`$${key}`, []] },
@@ -532,11 +224,10 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
532
224
  * `$update`
533
225
  * - keyed → map array and merge / replace matching element(s)
534
226
  * - non-keyed → behave like `$addToSet` (insert only when not present)
535
- */ _update(key, input, keyProps) {
227
+ */ _update(key, input, keys, flatType) {
536
228
  if (!input?.length) return;
537
- if (keyProps.size > 0) {
538
- const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("db.mongo.patch.strategy") === "merge";
539
- const keys = [...keyProps];
229
+ if (keys.length > 0) {
230
+ const mergeStrategy = flatType.metadata?.get("db.patch.strategy") === "merge";
540
231
  this._set(key, { $reduce: {
541
232
  input,
542
233
  initialValue: { $ifNull: [`$${key}`, []] },
@@ -556,23 +247,21 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
556
247
  * `$remove`
557
248
  * - keyed → filter out any element whose key set matches a payload item
558
249
  * - non-keyed → deep equality remove (`$setDifference`)
559
- */ _remove(key, input, keyProps) {
250
+ */ _remove(key, input, keys, _flatType) {
560
251
  if (!input?.length) return;
561
- if (keyProps.size > 0) {
562
- const keys = [...keyProps];
563
- this._set(key, { $let: {
564
- vars: { rem: input },
565
- in: { $filter: {
566
- input: { $ifNull: [`$${key}`, []] },
567
- as: "el",
568
- cond: { $not: { $anyElementTrue: { $map: {
569
- input: "$$rem",
570
- as: "r",
571
- in: this._keysEqual(keys, "$$el", "$$r")
572
- } } } }
573
- } }
574
- } });
575
- } else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
252
+ if (keys.length > 0) this._set(key, { $let: {
253
+ vars: { rem: input },
254
+ in: { $filter: {
255
+ input: { $ifNull: [`$${key}`, []] },
256
+ as: "el",
257
+ cond: { $not: { $anyElementTrue: { $map: {
258
+ input: "$$rem",
259
+ as: "r",
260
+ in: this._keysEqual(keys, "$$el", "$$r")
261
+ } } } }
262
+ } }
263
+ } });
264
+ else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
576
265
  }
577
266
  constructor(collection, payload) {
578
267
  _define_property$2(this, "collection", void 0);
@@ -582,27 +271,47 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
582
271
  * Filled only with the `_id` field right now.
583
272
  */ _define_property$2(this, "filterObj", void 0);
584
273
  /** MongoDB *update* document being built. */ _define_property$2(this, "updatePipeline", void 0);
274
+ /** Current `$set` stage being populated. */ _define_property$2(this, "currentSetStage", void 0);
585
275
  /** Additional *options* (mainly `arrayFilters`). */ _define_property$2(this, "optionsObj", void 0);
586
276
  this.collection = collection;
587
277
  this.payload = payload;
588
278
  this.filterObj = {};
589
279
  this.updatePipeline = [];
280
+ this.currentSetStage = null;
590
281
  this.optionsObj = {};
591
282
  }
592
283
  };
284
+ _define_property$2(CollectionPatcher, "getKeyProps", __atscript_utils_db.getKeyProps);
593
285
 
594
286
  //#endregion
595
- //#region packages/mongo/src/lib/logger.ts
596
- const NoopLogger = {
597
- error: () => {},
598
- warn: () => {},
599
- log: () => {},
600
- info: () => {},
601
- debug: () => {}
287
+ //#region packages/mongo/src/lib/mongo-filter.ts
288
+ const EMPTY = {};
289
+ const mongoVisitor = {
290
+ comparison(field, op, value) {
291
+ if (op === "$eq") return { [field]: value };
292
+ return { [field]: { [op]: value } };
293
+ },
294
+ and(children) {
295
+ if (children.length === 0) return EMPTY;
296
+ if (children.length === 1) return children[0];
297
+ return { $and: children };
298
+ },
299
+ or(children) {
300
+ if (children.length === 0) return { _impossible: true };
301
+ if (children.length === 1) return children[0];
302
+ return { $or: children };
303
+ },
304
+ not(child) {
305
+ return { $nor: [child] };
306
+ }
602
307
  };
308
+ function buildMongoFilter(filter) {
309
+ if (!filter || Object.keys(filter).length === 0) return EMPTY;
310
+ return (0, __atscript_utils_db.walkFilter)(filter, mongoVisitor);
311
+ }
603
312
 
604
313
  //#endregion
605
- //#region packages/mongo/src/lib/as-collection.ts
314
+ //#region packages/mongo/src/lib/mongo-adapter.ts
606
315
  function _define_property$1(obj, key, value) {
607
316
  if (key in obj) Object.defineProperty(obj, key, {
608
317
  value,
@@ -615,43 +324,38 @@ else obj[key] = value;
615
324
  }
616
325
  const INDEX_PREFIX = "atscript__";
617
326
  const DEFAULT_INDEX_NAME = "DEFAULT";
618
- /**
619
- * Generates a key for mongo index
620
- * @param type index type
621
- * @param name index name
622
- * @returns index key
623
- */ function indexKey(type, name) {
327
+ function mongoIndexKey(type, name) {
624
328
  const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
625
329
  return `${INDEX_PREFIX}${type}__${cleanName}`;
626
330
  }
627
- var AsCollection = class {
628
- createValidator(opts) {
629
- return this._type.validator(opts);
630
- }
631
- async exists() {
632
- return this.asMongo.collectionExists(this.name);
331
+ var MongoAdapter = class extends __atscript_utils_db.BaseDbAdapter {
332
+ get collection() {
333
+ if (!this._collection) this._collection = this.db.collection(this.resolveTableName(false));
334
+ return this._collection;
633
335
  }
634
- async ensureExists() {
635
- const exists = await this.exists();
636
- if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
336
+ aggregate(pipeline) {
337
+ return this.collection.aggregate(pipeline);
637
338
  }
638
- /**
639
- * Returns the a type definition of the "_id" prop.
640
- */ get idType() {
641
- const idProp = this.type.type.props.get("_id");
339
+ get idType() {
340
+ const idProp = this._table.type.type.props.get("_id");
642
341
  const idTags = idProp?.type.tags;
643
342
  if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
644
343
  if (idProp?.type.kind === "") return idProp.type.designType;
645
344
  return "objectId";
646
345
  }
346
+ prepareId(id, fieldType) {
347
+ const tags = fieldType.type.tags;
348
+ if (tags?.has("objectId") && tags?.has("mongo")) return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
349
+ if (fieldType.type.kind === "") {
350
+ const dt = fieldType.type.designType;
351
+ if (dt === "number") return Number(id);
352
+ }
353
+ return String(id);
354
+ }
647
355
  /**
648
- * Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
649
- * Assumes input has already been validated.
650
- *
651
- * @param {string | number | ObjectId} id - The validated ID.
652
- * @returns {string | number | ObjectId} - The transformed ID.
653
- * @throws {Error} If the `_id` type is unknown.
654
- */ prepareId(id) {
356
+ * Convenience method that uses `idType` to transform an ID value.
357
+ * For use in controllers that don't have access to the field type.
358
+ */ prepareIdFromIdType(id) {
655
359
  switch (this.idType) {
656
360
  case "objectId": return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
657
361
  case "number": return Number(id);
@@ -659,91 +363,59 @@ var AsCollection = class {
659
363
  default: throw new Error("Unknown \"_id\" type");
660
364
  }
661
365
  }
662
- /**
663
- * Retrieves a validator for a given purpose. If the validator is not already cached,
664
- * it creates and stores a new one based on the purpose.
665
- *
666
- * @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
667
- * @returns {Validator} The corresponding validator instance.
668
- * @throws {Error} If an unknown purpose is provided.
669
- */ getValidator(purpose) {
670
- if (!this.validators.has(purpose)) switch (purpose) {
671
- case "insert": {
672
- this.validators.set(purpose, this.createValidator({
673
- plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
674
- replace(type, path) {
675
- if (path === "_id" && type.type.tags.has("objectId")) return {
676
- ...type,
677
- optional: true
678
- };
679
- return type;
680
- }
681
- }));
682
- break;
366
+ supportsNestedObjects() {
367
+ return true;
368
+ }
369
+ supportsNativePatch() {
370
+ return true;
371
+ }
372
+ getValidatorPlugins() {
373
+ return [validateMongoIdPlugin];
374
+ }
375
+ getTopLevelArrayTag() {
376
+ return "db.mongo.__topLevelArray";
377
+ }
378
+ getAdapterTableName(type) {
379
+ return undefined;
380
+ }
381
+ buildInsertValidator(table) {
382
+ return table.createValidator({
383
+ plugins: this.getValidatorPlugins(),
384
+ replace: (type, path) => {
385
+ if (path === "_id" && type.type.tags?.has("objectId")) return {
386
+ ...type,
387
+ optional: true
388
+ };
389
+ if (table.defaults.has(path)) return {
390
+ ...type,
391
+ optional: true
392
+ };
393
+ return type;
683
394
  }
684
- case "update": {
685
- this.validators.set(purpose, this.createValidator({ plugins: [validateMongoIdPlugin] }));
686
- break;
687
- }
688
- case "patch": {
689
- this.validators.set(purpose, CollectionPatcher.prepareValidator(this));
690
- break;
691
- }
692
- default: throw new Error(`Unknown validator purpose: ${purpose}`);
693
- }
694
- return this.validators.get(purpose);
695
- }
696
- get type() {
697
- return this._type;
395
+ });
698
396
  }
699
- get indexes() {
700
- this._flatten();
701
- return this._indexes;
397
+ buildPatchValidator(table) {
398
+ return CollectionPatcher.prepareValidator(this.getPatcherContext());
702
399
  }
703
- _addIndexField(type, name, field, weight) {
704
- const key = indexKey(type, name);
705
- let index = this._indexes.get(key);
706
- const value = type === "text" ? "text" : 1;
707
- if (index) index.fields[field] = value;
708
- else {
709
- const weights = {};
710
- index = {
711
- key,
712
- name,
713
- type,
714
- fields: { [field]: value },
715
- weights
716
- };
717
- this._indexes.set(key, index);
718
- }
719
- if (weight) index.weights[field] = weight;
720
- }
721
- _setSearchIndex(type, name, definition) {
722
- const key = indexKey(type, name || DEFAULT_INDEX_NAME);
723
- this._indexes.set(key, {
724
- key,
725
- name: name || DEFAULT_INDEX_NAME,
726
- type,
727
- definition
728
- });
400
+ /** Returns the context object used by CollectionPatcher. */ getPatcherContext() {
401
+ return {
402
+ flatMap: this._table.flatMap,
403
+ prepareId: (id) => this.prepareIdFromIdType(id),
404
+ createValidator: (opts) => this._table.createValidator(opts)
405
+ };
729
406
  }
730
- _addFieldToSearchIndex(type, _name, fieldName, analyzer) {
731
- const name = _name || DEFAULT_INDEX_NAME;
732
- let index = this._indexes.get(indexKey(type, name));
733
- if (!index && type === "search_text") {
734
- this._setSearchIndex(type, name, {
735
- mappings: { fields: {} },
736
- text: { fuzzy: { maxEdits: 0 } }
737
- });
738
- index = this._indexes.get(indexKey(type, name));
739
- }
740
- if (index) {
741
- index.definition.mappings.fields[fieldName] = { type: "string" };
742
- if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
743
- }
407
+ async nativePatch(filter, patch) {
408
+ const mongoFilter = buildMongoFilter(filter);
409
+ const patcher = new CollectionPatcher(this.getPatcherContext(), patch);
410
+ const { updateFilter, updateOptions } = patcher.preparePatch();
411
+ const result = await this.collection.updateOne(mongoFilter, updateFilter, updateOptions);
412
+ return {
413
+ matchedCount: result.matchedCount,
414
+ modifiedCount: result.modifiedCount
415
+ };
744
416
  }
745
- _prepareIndexesForCollection() {
746
- const typeMeta = this.type.metadata;
417
+ onBeforeFlatten(type) {
418
+ const typeMeta = type.metadata;
747
419
  const dynamicText = typeMeta.get("db.mongo.search.dynamic");
748
420
  if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
749
421
  mappings: { dynamic: true },
@@ -756,73 +428,65 @@ else {
756
428
  text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
757
429
  });
758
430
  }
759
- get uniqueProps() {
760
- return this._uniqueProps;
761
- }
762
- _finalizeIndexesForCollection() {
763
- for (const [key, value] of Array.from(this._vectorFilters.entries())) {
764
- const index = this._indexes.get(key);
765
- if (index && index.type === "vector") index.definition.fields?.push({
766
- type: "filter",
767
- path: value
768
- });
431
+ onFieldScanned(field, type, metadata) {
432
+ if (field !== "_id" && metadata.has("meta.id")) {
433
+ this._table.removePrimaryKey(field);
434
+ this._addMongoIndexField("unique", "__pk", field);
435
+ this._table.addUniqueField(field);
769
436
  }
770
- for (const [, value] of Array.from(this._indexes.entries())) if (value.type === "unique") {
771
- const keys = Object.keys(value.fields);
772
- if (keys.length === 1) this._uniqueProps.add(keys[0]);
773
- }
774
- }
775
- _prepareIndexesForField(fieldName, metadata) {
776
- for (const index of metadata.get("db.index.plain") || []) {
777
- const name = index === true ? fieldName : index.name || fieldName;
778
- this._addIndexField("plain", name, fieldName);
779
- }
780
- for (const index of metadata.get("db.index.unique") || []) {
781
- const name = index === true ? fieldName : index.name || fieldName;
782
- this._addIndexField("unique", name, fieldName);
437
+ const defaultFn = metadata.get("db.default.fn");
438
+ if (defaultFn === "increment") {
439
+ const physicalName = metadata.get("db.column") ?? field;
440
+ this._incrementFields.add(physicalName);
783
441
  }
784
442
  for (const index of metadata.get("db.index.fulltext") || []) {
785
443
  const name = index === true ? "" : index.name || "";
786
- this._addIndexField("text", name, fieldName, 1);
444
+ const weight = index !== true && typeof index === "object" ? index.weight || 1 : 1;
445
+ this._addMongoIndexField("text", name, field, weight);
787
446
  }
788
- const textWeight = metadata.get("db.mongo.index.text");
789
- if (textWeight) this._addIndexField("text", "", fieldName, textWeight === true ? 1 : textWeight);
790
- for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, fieldName, index.analyzer);
447
+ for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer);
791
448
  const vectorIndex = metadata.get("db.mongo.search.vector");
792
- if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || fieldName, { fields: [{
449
+ if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || field, { fields: [{
793
450
  type: "vector",
794
- path: fieldName,
451
+ path: field,
795
452
  similarity: vectorIndex.similarity || "dotProduct",
796
453
  numDimensions: vectorIndex.dimensions
797
454
  }] });
798
- for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(indexKey("vector", index.indexName), fieldName);
799
- }
800
- _flatten() {
801
- if (!this._flatMap) {
802
- this._prepareIndexesForCollection();
803
- this._flatMap = (0, __atscript_typescript_utils.flattenAnnotatedType)(this.type, {
804
- topLevelArrayTag: "db.mongo.__topLevelArray",
805
- excludePhantomTypes: true,
806
- onField: (path, _type, metadata) => this._prepareIndexesForField(path, metadata)
455
+ for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", index.indexName), field);
456
+ }
457
+ onAfterFlatten() {
458
+ this._table.addPrimaryKey("_id");
459
+ for (const [key, value] of this._vectorFilters.entries()) {
460
+ const index = this._mongoIndexes.get(key);
461
+ if (index && index.type === "vector") index.definition.fields?.push({
462
+ type: "filter",
463
+ path: value
807
464
  });
808
- this._finalizeIndexesForCollection();
809
465
  }
810
466
  }
811
- getSearchIndexes() {
467
+ /** Returns MongoDB-specific search index map (internal). */ getMongoSearchIndexes() {
812
468
  if (!this._searchIndexesMap) {
469
+ this._table.flatMap;
813
470
  this._searchIndexesMap = new Map();
814
- let deafultIndex;
815
- for (const index of this.indexes.values()) switch (index.type) {
471
+ let defaultIndex;
472
+ for (const index of this._table.indexes.values()) if (index.type === "fulltext" && !defaultIndex) defaultIndex = {
473
+ key: index.key,
474
+ name: index.name,
475
+ type: "text",
476
+ fields: Object.fromEntries(index.fields.map((f) => [f.name, "text"])),
477
+ weights: Object.fromEntries(index.fields.filter((f) => f.weight).map((f) => [f.name, f.weight]))
478
+ };
479
+ for (const index of this._mongoIndexes.values()) switch (index.type) {
816
480
  case "text": {
817
- if (!deafultIndex) deafultIndex = index;
481
+ if (!defaultIndex) defaultIndex = index;
818
482
  break;
819
483
  }
820
484
  case "dynamic_text": {
821
- deafultIndex = index;
485
+ defaultIndex = index;
822
486
  break;
823
487
  }
824
488
  case "search_text": {
825
- if (!deafultIndex || deafultIndex?.type === "text") deafultIndex = index;
489
+ if (!defaultIndex || defaultIndex.type === "text") defaultIndex = index;
826
490
  this._searchIndexesMap.set(index.name, index);
827
491
  break;
828
492
  }
@@ -832,21 +496,231 @@ else {
832
496
  }
833
497
  default:
834
498
  }
835
- if (deafultIndex && !this._searchIndexesMap.has(DEFAULT_INDEX_NAME)) this._searchIndexesMap.set(DEFAULT_INDEX_NAME, deafultIndex);
499
+ if (defaultIndex && !this._searchIndexesMap.has(DEFAULT_INDEX_NAME)) this._searchIndexesMap.set(DEFAULT_INDEX_NAME, defaultIndex);
836
500
  }
837
501
  return this._searchIndexesMap;
838
502
  }
839
- getSearchIndex(name = DEFAULT_INDEX_NAME) {
840
- return this.getSearchIndexes().get(name);
503
+ /** Returns a specific MongoDB search index by name. */ getMongoSearchIndex(name = DEFAULT_INDEX_NAME) {
504
+ return this.getMongoSearchIndexes().get(name);
505
+ }
506
+ /** Returns available search indexes as generic metadata for UI. */ getSearchIndexes() {
507
+ const mongoIndexes = this.getMongoSearchIndexes();
508
+ return [...mongoIndexes.entries()].map(([name, index]) => ({
509
+ name,
510
+ description: `${index.type} index`
511
+ }));
512
+ }
513
+ /**
514
+ * Builds a MongoDB `$search` pipeline stage.
515
+ * Override `buildVectorSearchStage` in subclasses to provide embeddings.
516
+ */ buildSearchStage(text, indexName) {
517
+ const index = this.getMongoSearchIndex(indexName);
518
+ if (!index) return undefined;
519
+ if (index.type === "vector") return this.buildVectorSearchStage(text, index);
520
+ return { $search: {
521
+ index: index.key,
522
+ text: {
523
+ query: text,
524
+ path: { wildcard: "*" }
525
+ }
526
+ } };
527
+ }
528
+ /**
529
+ * Builds a vector search stage. Override in subclasses to generate embeddings.
530
+ * Returns `undefined` by default (vector search requires custom implementation).
531
+ */ buildVectorSearchStage(text, index) {
532
+ return undefined;
533
+ }
534
+ async search(text, query, indexName) {
535
+ const searchStage = this.buildSearchStage(text, indexName);
536
+ if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
537
+ const filter = buildMongoFilter(query.filter);
538
+ const controls = query.controls || {};
539
+ const pipeline = [searchStage, { $match: filter }];
540
+ if (controls.$sort) pipeline.push({ $sort: controls.$sort });
541
+ if (controls.$skip) pipeline.push({ $skip: controls.$skip });
542
+ if (controls.$limit) pipeline.push({ $limit: controls.$limit });
543
+ else pipeline.push({ $limit: 1e3 });
544
+ if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
545
+ return this.collection.aggregate(pipeline).toArray();
546
+ }
547
+ async searchWithCount(text, query, indexName) {
548
+ const searchStage = this.buildSearchStage(text, indexName);
549
+ if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
550
+ const filter = buildMongoFilter(query.filter);
551
+ const controls = query.controls || {};
552
+ const pipeline = [
553
+ searchStage,
554
+ { $match: filter },
555
+ { $facet: {
556
+ data: [
557
+ controls.$sort ? { $sort: controls.$sort } : undefined,
558
+ controls.$skip ? { $skip: controls.$skip } : undefined,
559
+ controls.$limit ? { $limit: controls.$limit } : undefined,
560
+ controls.$select ? { $project: controls.$select.asProjection } : undefined
561
+ ].filter(Boolean),
562
+ meta: [{ $count: "count" }]
563
+ } }
564
+ ];
565
+ const result = await this.collection.aggregate(pipeline).toArray();
566
+ return {
567
+ data: result[0]?.data || [],
568
+ count: result[0]?.meta[0]?.count || 0
569
+ };
570
+ }
571
+ async findManyWithCount(query) {
572
+ const filter = buildMongoFilter(query.filter);
573
+ const controls = query.controls || {};
574
+ const pipeline = [{ $match: filter }, { $facet: {
575
+ data: [
576
+ controls.$sort ? { $sort: controls.$sort } : undefined,
577
+ controls.$skip ? { $skip: controls.$skip } : undefined,
578
+ controls.$limit ? { $limit: controls.$limit } : undefined,
579
+ controls.$select ? { $project: controls.$select.asProjection } : undefined
580
+ ].filter(Boolean),
581
+ meta: [{ $count: "count" }]
582
+ } }];
583
+ const result = await this.collection.aggregate(pipeline).toArray();
584
+ return {
585
+ data: result[0]?.data || [],
586
+ count: result[0]?.meta[0]?.count || 0
587
+ };
588
+ }
589
+ async collectionExists() {
590
+ if (this.asMongo) return this.asMongo.collectionExists(this._table.tableName);
591
+ const cols = await this.db.listCollections({ name: this._table.tableName }).toArray();
592
+ return cols.length > 0;
593
+ }
594
+ async ensureCollectionExists() {
595
+ const exists = await this.collectionExists();
596
+ if (!exists) await this.db.createCollection(this._table.tableName, { comment: "Created by Atscript Mongo Adapter" });
597
+ }
598
+ async insertOne(data) {
599
+ if (this._incrementFields.size > 0) {
600
+ const fields = this._fieldsNeedingIncrement(data);
601
+ if (fields.length > 0) {
602
+ const maxValues = await this._getMaxValues(fields);
603
+ for (const physical of fields) data[physical] = (maxValues.get(physical) ?? 0) + 1;
604
+ }
605
+ }
606
+ const result = await this.collection.insertOne(data);
607
+ return { insertedId: result.insertedId };
608
+ }
609
+ async insertMany(data) {
610
+ if (this._incrementFields.size > 0) {
611
+ const allFields = new Set();
612
+ for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
613
+ if (allFields.size > 0) {
614
+ const maxValues = await this._getMaxValues([...allFields]);
615
+ for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
616
+ const next = (maxValues.get(physical) ?? 0) + 1;
617
+ item[physical] = next;
618
+ maxValues.set(physical, next);
619
+ } else if (typeof item[physical] === "number") {
620
+ const current = maxValues.get(physical) ?? 0;
621
+ if (item[physical] > current) maxValues.set(physical, item[physical]);
622
+ }
623
+ }
624
+ }
625
+ const result = await this.collection.insertMany(data);
626
+ return {
627
+ insertedCount: result.insertedCount,
628
+ insertedIds: Object.values(result.insertedIds)
629
+ };
630
+ }
631
+ async findOne(query) {
632
+ const filter = buildMongoFilter(query.filter);
633
+ const opts = this._buildFindOptions(query.controls);
634
+ return this.collection.findOne(filter, opts);
635
+ }
636
+ async findMany(query) {
637
+ const filter = buildMongoFilter(query.filter);
638
+ const opts = this._buildFindOptions(query.controls);
639
+ return this.collection.find(filter, opts).toArray();
640
+ }
641
+ async count(query) {
642
+ const filter = buildMongoFilter(query.filter);
643
+ return this.collection.countDocuments(filter);
644
+ }
645
+ async updateOne(filter, data) {
646
+ const mongoFilter = buildMongoFilter(filter);
647
+ const result = await this.collection.updateOne(mongoFilter, { $set: data });
648
+ return {
649
+ matchedCount: result.matchedCount,
650
+ modifiedCount: result.modifiedCount
651
+ };
652
+ }
653
+ async replaceOne(filter, data) {
654
+ const mongoFilter = buildMongoFilter(filter);
655
+ const result = await this.collection.replaceOne(mongoFilter, data);
656
+ return {
657
+ matchedCount: result.matchedCount,
658
+ modifiedCount: result.modifiedCount
659
+ };
660
+ }
661
+ async deleteOne(filter) {
662
+ const mongoFilter = buildMongoFilter(filter);
663
+ const result = await this.collection.deleteOne(mongoFilter);
664
+ return { deletedCount: result.deletedCount };
665
+ }
666
+ async updateMany(filter, data) {
667
+ const mongoFilter = buildMongoFilter(filter);
668
+ const result = await this.collection.updateMany(mongoFilter, { $set: data });
669
+ return {
670
+ matchedCount: result.matchedCount,
671
+ modifiedCount: result.modifiedCount
672
+ };
673
+ }
674
+ async replaceMany(filter, data) {
675
+ const mongoFilter = buildMongoFilter(filter);
676
+ const result = await this.collection.updateMany(mongoFilter, { $set: data });
677
+ return {
678
+ matchedCount: result.matchedCount,
679
+ modifiedCount: result.modifiedCount
680
+ };
841
681
  }
842
- get flatMap() {
843
- this._flatten();
844
- return this._flatMap;
682
+ async deleteMany(filter) {
683
+ const mongoFilter = buildMongoFilter(filter);
684
+ const result = await this.collection.deleteMany(mongoFilter);
685
+ return { deletedCount: result.deletedCount };
686
+ }
687
+ async ensureTable() {
688
+ return this.ensureCollectionExists();
845
689
  }
846
690
  async syncIndexes() {
847
- await this.ensureExists();
691
+ await this.ensureCollectionExists();
692
+ const allIndexes = new Map();
693
+ for (const [key, index] of this._table.indexes.entries()) {
694
+ const fields = {};
695
+ const weights = {};
696
+ let mongoType;
697
+ if (index.type === "fulltext") {
698
+ mongoType = "text";
699
+ for (const f of index.fields) {
700
+ fields[f.name] = "text";
701
+ if (f.weight) weights[f.name] = f.weight;
702
+ }
703
+ } else {
704
+ mongoType = index.type;
705
+ for (const f of index.fields) fields[f.name] = 1;
706
+ }
707
+ allIndexes.set(key, {
708
+ key,
709
+ name: index.name,
710
+ type: mongoType,
711
+ fields,
712
+ weights
713
+ });
714
+ }
715
+ for (const [key, index] of this._mongoIndexes.entries()) if (index.type === "text") {
716
+ const existing = allIndexes.get(key);
717
+ if (existing && existing.type === "text") {
718
+ Object.assign(existing.fields, index.fields);
719
+ Object.assign(existing.weights, index.weights);
720
+ } else allIndexes.set(key, index);
721
+ } else allIndexes.set(key, index);
848
722
  const existingIndexes = await this.collection.listIndexes().toArray();
849
- const indexesToCreate = new Map(this.indexes);
723
+ const indexesToCreate = new Map(allIndexes);
850
724
  for (const remote of existingIndexes) {
851
725
  if (!remote.name.startsWith(INDEX_PREFIX)) continue;
852
726
  if (indexesToCreate.has(remote.name)) {
@@ -856,54 +730,21 @@ else {
856
730
  case "unique":
857
731
  case "text": {
858
732
  if ((local.type === "text" || objMatch(local.fields, remote.key)) && objMatch(local.weights || {}, remote.weights || {})) indexesToCreate.delete(remote.name);
859
- else {
860
- this.logger.debug(`dropping index "${remote.name}"`);
861
- await this.collection.dropIndex(remote.name);
862
- }
733
+ else await this.collection.dropIndex(remote.name);
863
734
  break;
864
735
  }
865
736
  default:
866
737
  }
867
- } else {
868
- this.logger.debug(`dropping index "${remote.name}"`);
869
- await this.collection.dropIndex(remote.name);
870
- }
738
+ } else await this.collection.dropIndex(remote.name);
871
739
  }
872
- const toUpdate = new Set();
873
- const existingSearchIndexes = await this.collection.listSearchIndexes().toArray();
874
- for (const remote of existingSearchIndexes) {
875
- if (!remote.name.startsWith(INDEX_PREFIX)) continue;
876
- if (indexesToCreate.has(remote.name)) {
877
- const local = indexesToCreate.get(remote.name);
878
- const right = remote.latestDefinition;
879
- switch (local.type) {
880
- case "dynamic_text":
881
- case "search_text": {
882
- const left = local.definition;
883
- if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
884
- else toUpdate.add(remote.name);
885
- break;
886
- }
887
- case "vector": {
888
- if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
889
- else toUpdate.add(remote.name);
890
- break;
891
- }
892
- default:
893
- }
894
- } else if (remote.status !== "DELETING") {
895
- this.logger.debug(`dropping search index "${remote.name}"`);
896
- await this.collection.dropSearchIndex(remote.name);
897
- } else this.logger.debug(`search index "${remote.name}" is in deleting status`);
898
- }
899
- for (const [key, value] of Array.from(indexesToCreate.entries())) switch (value.type) {
740
+ for (const [key, value] of allIndexes.entries()) switch (value.type) {
900
741
  case "plain": {
901
- this.logger.debug(`creating index "${key}"`);
742
+ if (!indexesToCreate.has(key)) continue;
902
743
  await this.collection.createIndex(value.fields, { name: key });
903
744
  break;
904
745
  }
905
746
  case "unique": {
906
- this.logger.debug(`creating index "${key}"`);
747
+ if (!indexesToCreate.has(key)) continue;
907
748
  await this.collection.createIndex(value.fields, {
908
749
  name: key,
909
750
  unique: true
@@ -911,160 +752,168 @@ else toUpdate.add(remote.name);
911
752
  break;
912
753
  }
913
754
  case "text": {
914
- this.logger.debug(`creating index "${key}"`);
755
+ if (!indexesToCreate.has(key)) continue;
915
756
  await this.collection.createIndex(value.fields, {
916
757
  weights: value.weights,
917
758
  name: key
918
759
  });
919
760
  break;
920
761
  }
921
- case "dynamic_text":
922
- case "search_text":
923
- case "vector": {
924
- if (toUpdate.has(key)) {
925
- this.logger.debug(`updating search index "${key}"`);
926
- await this.collection.updateSearchIndex(key, value.definition);
927
- } else {
928
- this.logger.debug(`creating search index "${key}"`);
929
- await this.collection.createSearchIndex({
762
+ default:
763
+ }
764
+ try {
765
+ const toUpdate = new Set();
766
+ const existingSearchIndexes = await this.collection.listSearchIndexes().toArray();
767
+ for (const remote of existingSearchIndexes) {
768
+ if (!remote.name.startsWith(INDEX_PREFIX)) continue;
769
+ if (indexesToCreate.has(remote.name)) {
770
+ const local = indexesToCreate.get(remote.name);
771
+ const right = remote.latestDefinition;
772
+ switch (local.type) {
773
+ case "dynamic_text":
774
+ case "search_text": {
775
+ const left = local.definition;
776
+ if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
777
+ else toUpdate.add(remote.name);
778
+ break;
779
+ }
780
+ case "vector": {
781
+ if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
782
+ else toUpdate.add(remote.name);
783
+ break;
784
+ }
785
+ default:
786
+ }
787
+ } else if (remote.status !== "DELETING") await this.collection.dropSearchIndex(remote.name);
788
+ }
789
+ for (const [key, value] of indexesToCreate.entries()) switch (value.type) {
790
+ case "dynamic_text":
791
+ case "search_text":
792
+ case "vector": {
793
+ if (toUpdate.has(key)) await this.collection.updateSearchIndex(key, value.definition);
794
+ else await this.collection.createSearchIndex({
930
795
  name: key,
931
796
  type: value.type === "vector" ? "vectorSearch" : "search",
932
797
  definition: value.definition
933
798
  });
799
+ break;
934
800
  }
935
- break;
801
+ default:
802
+ }
803
+ } catch {}
804
+ }
805
+ /** Returns physical field names of increment fields that are undefined in the data. */ _fieldsNeedingIncrement(data) {
806
+ const result = [];
807
+ for (const physical of this._incrementFields) if (data[physical] === undefined || data[physical] === null) result.push(physical);
808
+ return result;
809
+ }
810
+ /** Reads current max value for each field via $group aggregation. */ async _getMaxValues(physicalFields) {
811
+ const aliases = physicalFields.map((f) => [`max__${f.replace(/\./g, "__")}`, f]);
812
+ const group = { _id: null };
813
+ for (const [alias, field] of aliases) group[alias] = { $max: `$${field}` };
814
+ const result = await this.collection.aggregate([{ $group: group }]).toArray();
815
+ const maxMap = new Map();
816
+ if (result.length > 0) {
817
+ const row = result[0];
818
+ for (const [alias, field] of aliases) {
819
+ const val = row[alias];
820
+ maxMap.set(field, typeof val === "number" ? val : 0);
936
821
  }
937
- default:
938
822
  }
823
+ return maxMap;
824
+ }
825
+ _buildFindOptions(controls) {
826
+ const opts = {};
827
+ if (!controls) return opts;
828
+ if (controls.$sort) opts.sort = controls.$sort;
829
+ if (controls.$limit) opts.limit = controls.$limit;
830
+ if (controls.$skip) opts.skip = controls.$skip;
831
+ if (controls.$select) opts.projection = controls.$select.asProjection;
832
+ return opts;
833
+ }
834
+ _addMongoIndexField(type, name, field, weight) {
835
+ const key = mongoIndexKey(type, name);
836
+ let index = this._mongoIndexes.get(key);
837
+ const value = type === "text" ? "text" : 1;
838
+ if (index) index.fields[field] = value;
839
+ else {
840
+ index = {
841
+ key,
842
+ name,
843
+ type,
844
+ fields: { [field]: value },
845
+ weights: {}
846
+ };
847
+ this._mongoIndexes.set(key, index);
848
+ }
849
+ if (weight) index.weights[field] = weight;
939
850
  }
940
- insert(payload, options) {
941
- const toInsert = this.prepareInsert(payload);
942
- return Array.isArray(toInsert) ? this.collection.insertMany(toInsert, options) : this.collection.insertOne(toInsert, options);
943
- }
944
- replace(payload, options) {
945
- const [filter, replace, opts] = this.prepareReplace(payload).toArgs();
946
- return this.collection.replaceOne(filter, replace, {
947
- ...opts,
948
- ...options
949
- });
950
- }
951
- update(payload, options) {
952
- const [filter, update, opts] = this.prepareUpdate(payload).toArgs();
953
- return this.collection.updateOne(filter, update, {
954
- ...opts,
955
- ...options
851
+ _setSearchIndex(type, name, definition) {
852
+ const key = mongoIndexKey(type, name || DEFAULT_INDEX_NAME);
853
+ this._mongoIndexes.set(key, {
854
+ key,
855
+ name: name || DEFAULT_INDEX_NAME,
856
+ type,
857
+ definition
956
858
  });
957
859
  }
958
- prepareInsert(payload) {
959
- const v = this.getValidator("insert");
960
- const arr = Array.isArray(payload) ? payload : [payload];
961
- const prepared = [];
962
- for (const item of arr) if (v.validate(item)) {
963
- const data = { ...item };
964
- if (data._id) data._id = this.prepareId(data._id);
965
- else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
966
- prepared.push(data);
967
- } else throw new Error("Invalid payload");
968
- return prepared.length === 1 ? prepared[0] : prepared;
969
- }
970
- prepareReplace(payload) {
971
- const v = this.getValidator("update");
972
- if (v.validate(payload)) {
973
- const _id = this.prepareId(payload._id);
974
- const data = {
975
- ...payload,
976
- _id
977
- };
978
- return {
979
- toArgs: () => [
980
- { _id },
981
- data,
982
- {}
983
- ],
984
- filter: { _id },
985
- updateFilter: data,
986
- updateOptions: {}
987
- };
860
+ _addFieldToSearchIndex(type, _name, fieldName, analyzer) {
861
+ const name = _name || DEFAULT_INDEX_NAME;
862
+ let index = this._mongoIndexes.get(mongoIndexKey(type, name));
863
+ if (!index && type === "search_text") {
864
+ this._setSearchIndex(type, name, {
865
+ mappings: { fields: {} },
866
+ text: { fuzzy: { maxEdits: 0 } }
867
+ });
868
+ index = this._mongoIndexes.get(mongoIndexKey(type, name));
988
869
  }
989
- throw new Error("Invalid payload");
990
- }
991
- prepareUpdate(payload) {
992
- const v = this.getValidator("patch");
993
- if (v.validate(payload)) return new CollectionPatcher(this, payload).preparePatch();
994
- throw new Error("Invalid payload");
995
- }
996
- constructor(asMongo, _type, logger = NoopLogger) {
997
- _define_property$1(this, "asMongo", void 0);
998
- _define_property$1(this, "_type", void 0);
999
- _define_property$1(this, "logger", void 0);
1000
- _define_property$1(this, "name", void 0);
1001
- _define_property$1(this, "collection", void 0);
1002
- _define_property$1(this, "validators", void 0);
1003
- _define_property$1(this, "_indexes", void 0);
1004
- _define_property$1(this, "_vectorFilters", void 0);
1005
- _define_property$1(this, "_flatMap", void 0);
1006
- _define_property$1(this, "_uniqueProps", void 0);
1007
- _define_property$1(this, "_searchIndexesMap", void 0);
1008
- this.asMongo = asMongo;
1009
- this._type = _type;
1010
- this.logger = logger;
1011
- this.validators = new Map();
1012
- this._indexes = new Map();
1013
- this._vectorFilters = new Map();
1014
- this._uniqueProps = new Set();
1015
- if (!(0, __atscript_typescript_utils.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
1016
- const name = _type.metadata.get("db.table");
1017
- if (!name) throw new Error("@db.table annotation expected with collection name");
1018
- if (_type.type.kind !== "object") throw new Error("Mongo collection must be an object type");
1019
- this.name = name;
1020
- this.collection = asMongo.db.collection(name);
870
+ if (index) {
871
+ index.definition.mappings.fields[fieldName] = { type: "string" };
872
+ if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
873
+ }
874
+ }
875
+ constructor(db, asMongo) {
876
+ super(), _define_property$1(this, "db", void 0), _define_property$1(this, "asMongo", void 0), _define_property$1(this, "_collection", void 0), _define_property$1(this, "_mongoIndexes", void 0), _define_property$1(this, "_vectorFilters", void 0), _define_property$1(this, "_searchIndexesMap", void 0), _define_property$1(this, "_incrementFields", void 0), this.db = db, this.asMongo = asMongo, this._mongoIndexes = new Map(), this._vectorFilters = new Map(), this._incrementFields = new Set();
1021
877
  }
1022
878
  };
1023
- /**
1024
- * Vector Index fields matching
1025
- */ function vectorFieldsMatch(left, right) {
1026
- const leftMap = new Map();
1027
- left.forEach((f) => leftMap.set(f.path, f));
1028
- const rightMap = new Map();
1029
- (right || []).forEach((f) => rightMap.set(f.path, f));
1030
- if (leftMap.size === rightMap.size) {
1031
- let match = true;
1032
- for (const [key, left$1] of leftMap.entries()) {
1033
- const right$1 = rightMap.get(key);
1034
- if (!right$1) {
1035
- match = false;
1036
- break;
1037
- }
1038
- if (left$1.type === right$1.type && left$1.path === right$1.path && left$1.similarity === right$1.similarity && left$1.numDimensions === right$1.numDimensions) continue;
1039
- match = false;
1040
- break;
1041
- }
1042
- return match;
1043
- } else return false;
879
+ function objMatch(o1, o2) {
880
+ const keys1 = Object.keys(o1);
881
+ const keys2 = Object.keys(o2);
882
+ if (keys1.length !== keys2.length) return false;
883
+ return keys1.every((key) => o1[key] === o2[key]);
1044
884
  }
1045
- /**
1046
- * Search Index fields matching
1047
- */ function fieldsMatch(left, right) {
885
+ function fieldsMatch(left, right) {
1048
886
  if (!left || !right) return left === right;
1049
887
  const leftKeys = Object.keys(left);
1050
888
  const rightKeys = Object.keys(right);
1051
889
  if (leftKeys.length !== rightKeys.length) return false;
1052
890
  return leftKeys.every((key) => {
1053
891
  if (!(key in right)) return false;
1054
- const leftField = left[key];
1055
- const rightField = right[key];
1056
- return leftField.type === rightField.type && leftField.analyzer === rightField.analyzer;
892
+ return left[key].type === right[key].type && left[key].analyzer === right[key].analyzer;
1057
893
  });
1058
894
  }
1059
- /**
1060
- * Shallow object matching
1061
- */ function objMatch(o1, o2) {
1062
- const keys1 = Object.keys(o1);
1063
- const keys2 = Object.keys(o2);
1064
- if (keys1.length !== keys2.length) return false;
1065
- return keys1.every((key) => o1[key] === o2[key]);
895
+ function vectorFieldsMatch(left, right) {
896
+ const leftMap = new Map(left.map((f) => [f.path, f]));
897
+ const rightMap = new Map((right || []).map((f) => [f.path, f]));
898
+ if (leftMap.size !== rightMap.size) return false;
899
+ for (const [key, l] of leftMap.entries()) {
900
+ const r = rightMap.get(key);
901
+ if (!r) return false;
902
+ if (l.type !== r.type || l.path !== r.path || l.similarity !== r.similarity || l.numDimensions !== r.numDimensions) return false;
903
+ }
904
+ return true;
1066
905
  }
1067
906
 
907
+ //#endregion
908
+ //#region packages/mongo/src/lib/logger.ts
909
+ const NoopLogger = {
910
+ error: () => {},
911
+ warn: () => {},
912
+ log: () => {},
913
+ info: () => {},
914
+ debug: () => {}
915
+ };
916
+
1068
917
  //#endregion
1069
918
  //#region packages/mongo/src/lib/as-mongo.ts
1070
919
  function _define_property(obj, key, value) {
@@ -1089,27 +938,39 @@ var AsMongo = class {
1089
938
  const list = await this.getCollectionsList();
1090
939
  return list.has(name);
1091
940
  }
1092
- getCollection(type, logger) {
1093
- let collection = this._collections.get(type);
1094
- if (!collection) {
1095
- collection = new AsCollection(this, type, logger || this.logger);
1096
- this._collections.set(type, collection);
941
+ getAdapter(type) {
942
+ this._ensureCreated(type);
943
+ return this._adapters.get(type);
944
+ }
945
+ getTable(type, logger) {
946
+ this._ensureCreated(type, logger);
947
+ return this._tables.get(type);
948
+ }
949
+ _ensureCreated(type, logger) {
950
+ if (!this._adapters.has(type)) {
951
+ const adapter = new MongoAdapter(this.db, this);
952
+ const table = new __atscript_utils_db.AtscriptDbTable(type, adapter, logger || this.logger);
953
+ this._adapters.set(type, adapter);
954
+ this._tables.set(type, table);
1097
955
  }
1098
- return collection;
1099
956
  }
1100
957
  constructor(client, logger = NoopLogger) {
1101
958
  _define_property(this, "logger", void 0);
1102
959
  _define_property(this, "client", void 0);
1103
960
  _define_property(this, "collectionsList", void 0);
1104
- _define_property(this, "_collections", void 0);
961
+ _define_property(this, "_adapters", void 0);
962
+ _define_property(this, "_tables", void 0);
1105
963
  this.logger = logger;
1106
- this._collections = new WeakMap();
964
+ this._adapters = new WeakMap();
965
+ this._tables = new WeakMap();
1107
966
  if (typeof client === "string") this.client = new mongodb.MongoClient(client);
1108
967
  else this.client = client;
1109
968
  }
1110
969
  };
1111
970
 
1112
971
  //#endregion
1113
- exports.AsCollection = AsCollection
1114
972
  exports.AsMongo = AsMongo
1115
- exports.MongoPlugin = MongoPlugin
973
+ exports.CollectionPatcher = CollectionPatcher
974
+ exports.MongoAdapter = MongoAdapter
975
+ exports.buildMongoFilter = buildMongoFilter
976
+ exports.validateMongoIdPlugin = validateMongoIdPlugin