@atscript/mongo 0.1.31 → 0.1.32

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
@@ -354,25 +64,25 @@ var CollectionPatcher = class CollectionPatcher {
354
64
  * Build a runtime *Validator* that understands the extended patch payload.
355
65
  *
356
66
  * * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
357
- * * Honors `db.mongo.patch.strategy === "merge"` metadata.
67
+ * * Honors `db.patch.strategy === "merge"` metadata.
358
68
  *
359
69
  * @param collection Target collection wrapper
360
70
  * @returns Atscript Validator
361
- */ static prepareValidator(collection) {
362
- return collection.createValidator({
363
- plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
71
+ */ static prepareValidator(context) {
72
+ return context.createValidator({
73
+ plugins: [validateMongoIdPlugin],
364
74
  replace: (def, path) => {
365
75
  if (path === "" && def.type.kind === "object") {
366
76
  const obj = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(def.metadata);
367
77
  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
78
  return obj.$type;
369
79
  }
370
- if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
80
+ if (def.type.kind === "array" && context.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
371
81
  const defArray = def;
372
- const mergeStrategy = defArray.metadata.get("db.mongo.patch.strategy") === "merge";
82
+ const mergeStrategy = defArray.metadata.get("db.patch.strategy") === "merge";
373
83
  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;
84
+ const isPrimitive = (0, __atscript_typescript_utils.isAnnotatedTypeOfPrimitive)(defArray.type.of);
85
+ if (isPrimitive) return (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
376
86
  if (defArray.type.of.type.kind === "object") {
377
87
  const objType = defArray.type.of.type;
378
88
  const t = (0, __atscript_typescript_utils.defineAnnotatedType)("object").copyMetadata(defArray.type.of.metadata);
@@ -390,7 +100,7 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
390
100
  }
391
101
  return def;
392
102
  },
393
- partial: (def, path) => path !== "" && def.metadata.get("db.mongo.patch.strategy") === "merge"
103
+ partial: (def, path) => path !== "" && def.metadata.get("db.patch.strategy") === "merge"
394
104
  });
395
105
  }
396
106
  /**
@@ -443,7 +153,7 @@ else t.prop(key, (0, __atscript_typescript_utils.defineAnnotatedType)().refTo(va
443
153
  const flatType = this.collection.flatMap.get(key);
444
154
  const topLevelArray = flatType?.metadata?.get("db.mongo.__topLevelArray");
445
155
  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);
156
+ else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
447
157
  else if (key !== "_id") this._set(key, value);
448
158
  }
449
159
  return this.updatePipeline;
@@ -497,7 +207,7 @@ else if (key !== "_id") this._set(key, value);
497
207
  * - unique / keyed → delegate to _upsert (insert-or-update)
498
208
  */ _insert(key, input, keyProps) {
499
209
  if (!input?.length) return;
500
- const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("db.mongo.array.uniqueItems");
210
+ const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("expect.array.uniqueItems");
501
211
  if (uniqueItems || keyProps.size > 0) this._upsert(key, input, keyProps);
502
212
  else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
503
213
  }
@@ -535,7 +245,7 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
535
245
  */ _update(key, input, keyProps) {
536
246
  if (!input?.length) return;
537
247
  if (keyProps.size > 0) {
538
- const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("db.mongo.patch.strategy") === "merge";
248
+ const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("db.patch.strategy") === "merge";
539
249
  const keys = [...keyProps];
540
250
  this._set(key, { $reduce: {
541
251
  input,
@@ -592,17 +302,34 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
592
302
  };
593
303
 
594
304
  //#endregion
595
- //#region packages/mongo/src/lib/logger.ts
596
- const NoopLogger = {
597
- error: () => {},
598
- warn: () => {},
599
- log: () => {},
600
- info: () => {},
601
- debug: () => {}
305
+ //#region packages/mongo/src/lib/mongo-filter.ts
306
+ const EMPTY = {};
307
+ const mongoVisitor = {
308
+ comparison(field, op, value) {
309
+ if (op === "$eq") return { [field]: value };
310
+ return { [field]: { [op]: value } };
311
+ },
312
+ and(children) {
313
+ if (children.length === 0) return EMPTY;
314
+ if (children.length === 1) return children[0];
315
+ return { $and: children };
316
+ },
317
+ or(children) {
318
+ if (children.length === 0) return { _impossible: true };
319
+ if (children.length === 1) return children[0];
320
+ return { $or: children };
321
+ },
322
+ not(child) {
323
+ return { $nor: [child] };
324
+ }
602
325
  };
326
+ function buildMongoFilter(filter) {
327
+ if (!filter || Object.keys(filter).length === 0) return EMPTY;
328
+ return (0, __atscript_utils_db.walkFilter)(filter, mongoVisitor);
329
+ }
603
330
 
604
331
  //#endregion
605
- //#region packages/mongo/src/lib/as-collection.ts
332
+ //#region packages/mongo/src/lib/mongo-adapter.ts
606
333
  function _define_property$1(obj, key, value) {
607
334
  if (key in obj) Object.defineProperty(obj, key, {
608
335
  value,
@@ -615,43 +342,38 @@ else obj[key] = value;
615
342
  }
616
343
  const INDEX_PREFIX = "atscript__";
617
344
  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) {
345
+ function mongoIndexKey(type, name) {
624
346
  const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
625
347
  return `${INDEX_PREFIX}${type}__${cleanName}`;
626
348
  }
627
- var AsCollection = class {
628
- createValidator(opts) {
629
- return this._type.validator(opts);
630
- }
631
- async exists() {
632
- return this.asMongo.collectionExists(this.name);
349
+ var MongoAdapter = class extends __atscript_utils_db.BaseDbAdapter {
350
+ get collection() {
351
+ if (!this._collection) this._collection = this.db.collection(this.resolveTableName(false));
352
+ return this._collection;
633
353
  }
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" });
354
+ aggregate(pipeline) {
355
+ return this.collection.aggregate(pipeline);
637
356
  }
638
- /**
639
- * Returns the a type definition of the "_id" prop.
640
- */ get idType() {
641
- const idProp = this.type.type.props.get("_id");
357
+ get idType() {
358
+ const idProp = this._table.type.type.props.get("_id");
642
359
  const idTags = idProp?.type.tags;
643
360
  if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
644
361
  if (idProp?.type.kind === "") return idProp.type.designType;
645
362
  return "objectId";
646
363
  }
364
+ prepareId(id, fieldType) {
365
+ const tags = fieldType.type.tags;
366
+ if (tags?.has("objectId") && tags?.has("mongo")) return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
367
+ if (fieldType.type.kind === "") {
368
+ const dt = fieldType.type.designType;
369
+ if (dt === "number") return Number(id);
370
+ }
371
+ return String(id);
372
+ }
647
373
  /**
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) {
374
+ * Convenience method that uses `idType` to transform an ID value.
375
+ * For use in controllers that don't have access to the field type.
376
+ */ prepareIdFromIdType(id) {
655
377
  switch (this.idType) {
656
378
  case "objectId": return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
657
379
  case "number": return Number(id);
@@ -659,91 +381,59 @@ var AsCollection = class {
659
381
  default: throw new Error("Unknown \"_id\" type");
660
382
  }
661
383
  }
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;
384
+ supportsNestedObjects() {
385
+ return true;
386
+ }
387
+ supportsNativePatch() {
388
+ return true;
389
+ }
390
+ getValidatorPlugins() {
391
+ return [validateMongoIdPlugin];
392
+ }
393
+ getTopLevelArrayTag() {
394
+ return "db.mongo.__topLevelArray";
395
+ }
396
+ getAdapterTableName(type) {
397
+ return undefined;
398
+ }
399
+ buildInsertValidator(table) {
400
+ return table.createValidator({
401
+ plugins: this.getValidatorPlugins(),
402
+ replace: (type, path) => {
403
+ if (path === "_id" && type.type.tags?.has("objectId")) return {
404
+ ...type,
405
+ optional: true
406
+ };
407
+ if (table.defaults.has(path)) return {
408
+ ...type,
409
+ optional: true
410
+ };
411
+ return type;
683
412
  }
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;
413
+ });
698
414
  }
699
- get indexes() {
700
- this._flatten();
701
- return this._indexes;
415
+ buildPatchValidator(table) {
416
+ return CollectionPatcher.prepareValidator(this.getPatcherContext());
702
417
  }
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
- });
418
+ /** Returns the context object used by CollectionPatcher. */ getPatcherContext() {
419
+ return {
420
+ flatMap: this._table.flatMap,
421
+ prepareId: (id) => this.prepareIdFromIdType(id),
422
+ createValidator: (opts) => this._table.createValidator(opts)
423
+ };
729
424
  }
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
- }
425
+ async nativePatch(filter, patch) {
426
+ const mongoFilter = buildMongoFilter(filter);
427
+ const patcher = new CollectionPatcher(this.getPatcherContext(), patch);
428
+ const { updateFilter, updateOptions } = patcher.preparePatch();
429
+ const result = await this.collection.updateOne(mongoFilter, updateFilter, updateOptions);
430
+ return {
431
+ matchedCount: result.matchedCount,
432
+ modifiedCount: result.modifiedCount
433
+ };
744
434
  }
745
- _prepareIndexesForCollection() {
746
- const typeMeta = this.type.metadata;
435
+ onBeforeFlatten(type) {
436
+ const typeMeta = type.metadata;
747
437
  const dynamicText = typeMeta.get("db.mongo.search.dynamic");
748
438
  if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
749
439
  mappings: { dynamic: true },
@@ -756,73 +446,65 @@ else {
756
446
  text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
757
447
  });
758
448
  }
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
- });
449
+ onFieldScanned(field, type, metadata) {
450
+ if (field !== "_id" && metadata.has("meta.id")) {
451
+ this._table.removePrimaryKey(field);
452
+ this._addMongoIndexField("unique", "__pk", field);
453
+ this._table.addUniqueField(field);
769
454
  }
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);
455
+ const defaultFn = metadata.get("db.default.fn");
456
+ if (defaultFn === "increment") {
457
+ const physicalName = metadata.get("db.column") ?? field;
458
+ this._incrementFields.add(physicalName);
783
459
  }
784
460
  for (const index of metadata.get("db.index.fulltext") || []) {
785
461
  const name = index === true ? "" : index.name || "";
786
- this._addIndexField("text", name, fieldName, 1);
462
+ const weight = index !== true && typeof index === "object" ? index.weight || 1 : 1;
463
+ this._addMongoIndexField("text", name, field, weight);
787
464
  }
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);
465
+ for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, field, index.analyzer);
791
466
  const vectorIndex = metadata.get("db.mongo.search.vector");
792
- if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || fieldName, { fields: [{
467
+ if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || field, { fields: [{
793
468
  type: "vector",
794
- path: fieldName,
469
+ path: field,
795
470
  similarity: vectorIndex.similarity || "dotProduct",
796
471
  numDimensions: vectorIndex.dimensions
797
472
  }] });
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)
473
+ for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", index.indexName), field);
474
+ }
475
+ onAfterFlatten() {
476
+ this._table.addPrimaryKey("_id");
477
+ for (const [key, value] of this._vectorFilters.entries()) {
478
+ const index = this._mongoIndexes.get(key);
479
+ if (index && index.type === "vector") index.definition.fields?.push({
480
+ type: "filter",
481
+ path: value
807
482
  });
808
- this._finalizeIndexesForCollection();
809
483
  }
810
484
  }
811
- getSearchIndexes() {
485
+ /** Returns MongoDB-specific search index map (internal). */ getMongoSearchIndexes() {
812
486
  if (!this._searchIndexesMap) {
487
+ this._table.flatMap;
813
488
  this._searchIndexesMap = new Map();
814
- let deafultIndex;
815
- for (const index of this.indexes.values()) switch (index.type) {
489
+ let defaultIndex;
490
+ for (const index of this._table.indexes.values()) if (index.type === "fulltext" && !defaultIndex) defaultIndex = {
491
+ key: index.key,
492
+ name: index.name,
493
+ type: "text",
494
+ fields: Object.fromEntries(index.fields.map((f) => [f.name, "text"])),
495
+ weights: Object.fromEntries(index.fields.filter((f) => f.weight).map((f) => [f.name, f.weight]))
496
+ };
497
+ for (const index of this._mongoIndexes.values()) switch (index.type) {
816
498
  case "text": {
817
- if (!deafultIndex) deafultIndex = index;
499
+ if (!defaultIndex) defaultIndex = index;
818
500
  break;
819
501
  }
820
502
  case "dynamic_text": {
821
- deafultIndex = index;
503
+ defaultIndex = index;
822
504
  break;
823
505
  }
824
506
  case "search_text": {
825
- if (!deafultIndex || deafultIndex?.type === "text") deafultIndex = index;
507
+ if (!defaultIndex || defaultIndex.type === "text") defaultIndex = index;
826
508
  this._searchIndexesMap.set(index.name, index);
827
509
  break;
828
510
  }
@@ -832,21 +514,231 @@ else {
832
514
  }
833
515
  default:
834
516
  }
835
- if (deafultIndex && !this._searchIndexesMap.has(DEFAULT_INDEX_NAME)) this._searchIndexesMap.set(DEFAULT_INDEX_NAME, deafultIndex);
517
+ if (defaultIndex && !this._searchIndexesMap.has(DEFAULT_INDEX_NAME)) this._searchIndexesMap.set(DEFAULT_INDEX_NAME, defaultIndex);
836
518
  }
837
519
  return this._searchIndexesMap;
838
520
  }
839
- getSearchIndex(name = DEFAULT_INDEX_NAME) {
840
- return this.getSearchIndexes().get(name);
521
+ /** Returns a specific MongoDB search index by name. */ getMongoSearchIndex(name = DEFAULT_INDEX_NAME) {
522
+ return this.getMongoSearchIndexes().get(name);
523
+ }
524
+ /** Returns available search indexes as generic metadata for UI. */ getSearchIndexes() {
525
+ const mongoIndexes = this.getMongoSearchIndexes();
526
+ return [...mongoIndexes.entries()].map(([name, index]) => ({
527
+ name,
528
+ description: `${index.type} index`
529
+ }));
841
530
  }
842
- get flatMap() {
843
- this._flatten();
844
- return this._flatMap;
531
+ /**
532
+ * Builds a MongoDB `$search` pipeline stage.
533
+ * Override `buildVectorSearchStage` in subclasses to provide embeddings.
534
+ */ buildSearchStage(text, indexName) {
535
+ const index = this.getMongoSearchIndex(indexName);
536
+ if (!index) return undefined;
537
+ if (index.type === "vector") return this.buildVectorSearchStage(text, index);
538
+ return { $search: {
539
+ index: index.key,
540
+ text: {
541
+ query: text,
542
+ path: { wildcard: "*" }
543
+ }
544
+ } };
545
+ }
546
+ /**
547
+ * Builds a vector search stage. Override in subclasses to generate embeddings.
548
+ * Returns `undefined` by default (vector search requires custom implementation).
549
+ */ buildVectorSearchStage(text, index) {
550
+ return undefined;
551
+ }
552
+ async search(text, query, indexName) {
553
+ const searchStage = this.buildSearchStage(text, indexName);
554
+ if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
555
+ const filter = buildMongoFilter(query.filter);
556
+ const controls = query.controls || {};
557
+ const pipeline = [searchStage, { $match: filter }];
558
+ if (controls.$sort) pipeline.push({ $sort: controls.$sort });
559
+ if (controls.$skip) pipeline.push({ $skip: controls.$skip });
560
+ if (controls.$limit) pipeline.push({ $limit: controls.$limit });
561
+ else pipeline.push({ $limit: 1e3 });
562
+ if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
563
+ return this.collection.aggregate(pipeline).toArray();
564
+ }
565
+ async searchWithCount(text, query, indexName) {
566
+ const searchStage = this.buildSearchStage(text, indexName);
567
+ if (!searchStage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
568
+ const filter = buildMongoFilter(query.filter);
569
+ const controls = query.controls || {};
570
+ const pipeline = [
571
+ searchStage,
572
+ { $match: filter },
573
+ { $facet: {
574
+ data: [
575
+ controls.$sort ? { $sort: controls.$sort } : undefined,
576
+ controls.$skip ? { $skip: controls.$skip } : undefined,
577
+ controls.$limit ? { $limit: controls.$limit } : undefined,
578
+ controls.$select ? { $project: controls.$select.asProjection } : undefined
579
+ ].filter(Boolean),
580
+ meta: [{ $count: "count" }]
581
+ } }
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 findManyWithCount(query) {
590
+ const filter = buildMongoFilter(query.filter);
591
+ const controls = query.controls || {};
592
+ const pipeline = [{ $match: filter }, { $facet: {
593
+ data: [
594
+ controls.$sort ? { $sort: controls.$sort } : undefined,
595
+ controls.$skip ? { $skip: controls.$skip } : undefined,
596
+ controls.$limit ? { $limit: controls.$limit } : undefined,
597
+ controls.$select ? { $project: controls.$select.asProjection } : undefined
598
+ ].filter(Boolean),
599
+ meta: [{ $count: "count" }]
600
+ } }];
601
+ const result = await this.collection.aggregate(pipeline).toArray();
602
+ return {
603
+ data: result[0]?.data || [],
604
+ count: result[0]?.meta[0]?.count || 0
605
+ };
606
+ }
607
+ async collectionExists() {
608
+ if (this.asMongo) return this.asMongo.collectionExists(this._table.tableName);
609
+ const cols = await this.db.listCollections({ name: this._table.tableName }).toArray();
610
+ return cols.length > 0;
611
+ }
612
+ async ensureCollectionExists() {
613
+ const exists = await this.collectionExists();
614
+ if (!exists) await this.db.createCollection(this._table.tableName, { comment: "Created by Atscript Mongo Adapter" });
615
+ }
616
+ async insertOne(data) {
617
+ if (this._incrementFields.size > 0) {
618
+ const fields = this._fieldsNeedingIncrement(data);
619
+ if (fields.length > 0) {
620
+ const maxValues = await this._getMaxValues(fields);
621
+ for (const physical of fields) data[physical] = (maxValues.get(physical) ?? 0) + 1;
622
+ }
623
+ }
624
+ const result = await this.collection.insertOne(data);
625
+ return { insertedId: result.insertedId };
626
+ }
627
+ async insertMany(data) {
628
+ if (this._incrementFields.size > 0) {
629
+ const allFields = new Set();
630
+ for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
631
+ if (allFields.size > 0) {
632
+ const maxValues = await this._getMaxValues([...allFields]);
633
+ for (const item of data) for (const physical of allFields) if (item[physical] === undefined || item[physical] === null) {
634
+ const next = (maxValues.get(physical) ?? 0) + 1;
635
+ item[physical] = next;
636
+ maxValues.set(physical, next);
637
+ } else if (typeof item[physical] === "number") {
638
+ const current = maxValues.get(physical) ?? 0;
639
+ if (item[physical] > current) maxValues.set(physical, item[physical]);
640
+ }
641
+ }
642
+ }
643
+ const result = await this.collection.insertMany(data);
644
+ return {
645
+ insertedCount: result.insertedCount,
646
+ insertedIds: Object.values(result.insertedIds)
647
+ };
648
+ }
649
+ async findOne(query) {
650
+ const filter = buildMongoFilter(query.filter);
651
+ const opts = this._buildFindOptions(query.controls);
652
+ return this.collection.findOne(filter, opts);
653
+ }
654
+ async findMany(query) {
655
+ const filter = buildMongoFilter(query.filter);
656
+ const opts = this._buildFindOptions(query.controls);
657
+ return this.collection.find(filter, opts).toArray();
658
+ }
659
+ async count(query) {
660
+ const filter = buildMongoFilter(query.filter);
661
+ return this.collection.countDocuments(filter);
662
+ }
663
+ async updateOne(filter, data) {
664
+ const mongoFilter = buildMongoFilter(filter);
665
+ const result = await this.collection.updateOne(mongoFilter, { $set: data });
666
+ return {
667
+ matchedCount: result.matchedCount,
668
+ modifiedCount: result.modifiedCount
669
+ };
670
+ }
671
+ async replaceOne(filter, data) {
672
+ const mongoFilter = buildMongoFilter(filter);
673
+ const result = await this.collection.replaceOne(mongoFilter, data);
674
+ return {
675
+ matchedCount: result.matchedCount,
676
+ modifiedCount: result.modifiedCount
677
+ };
678
+ }
679
+ async deleteOne(filter) {
680
+ const mongoFilter = buildMongoFilter(filter);
681
+ const result = await this.collection.deleteOne(mongoFilter);
682
+ return { deletedCount: result.deletedCount };
683
+ }
684
+ async updateMany(filter, data) {
685
+ const mongoFilter = buildMongoFilter(filter);
686
+ const result = await this.collection.updateMany(mongoFilter, { $set: data });
687
+ return {
688
+ matchedCount: result.matchedCount,
689
+ modifiedCount: result.modifiedCount
690
+ };
691
+ }
692
+ async replaceMany(filter, data) {
693
+ const mongoFilter = buildMongoFilter(filter);
694
+ const result = await this.collection.updateMany(mongoFilter, { $set: data });
695
+ return {
696
+ matchedCount: result.matchedCount,
697
+ modifiedCount: result.modifiedCount
698
+ };
699
+ }
700
+ async deleteMany(filter) {
701
+ const mongoFilter = buildMongoFilter(filter);
702
+ const result = await this.collection.deleteMany(mongoFilter);
703
+ return { deletedCount: result.deletedCount };
704
+ }
705
+ async ensureTable() {
706
+ return this.ensureCollectionExists();
845
707
  }
846
708
  async syncIndexes() {
847
- await this.ensureExists();
709
+ await this.ensureCollectionExists();
710
+ const allIndexes = new Map();
711
+ for (const [key, index] of this._table.indexes.entries()) {
712
+ const fields = {};
713
+ const weights = {};
714
+ let mongoType;
715
+ if (index.type === "fulltext") {
716
+ mongoType = "text";
717
+ for (const f of index.fields) {
718
+ fields[f.name] = "text";
719
+ if (f.weight) weights[f.name] = f.weight;
720
+ }
721
+ } else {
722
+ mongoType = index.type;
723
+ for (const f of index.fields) fields[f.name] = 1;
724
+ }
725
+ allIndexes.set(key, {
726
+ key,
727
+ name: index.name,
728
+ type: mongoType,
729
+ fields,
730
+ weights
731
+ });
732
+ }
733
+ for (const [key, index] of this._mongoIndexes.entries()) if (index.type === "text") {
734
+ const existing = allIndexes.get(key);
735
+ if (existing && existing.type === "text") {
736
+ Object.assign(existing.fields, index.fields);
737
+ Object.assign(existing.weights, index.weights);
738
+ } else allIndexes.set(key, index);
739
+ } else allIndexes.set(key, index);
848
740
  const existingIndexes = await this.collection.listIndexes().toArray();
849
- const indexesToCreate = new Map(this.indexes);
741
+ const indexesToCreate = new Map(allIndexes);
850
742
  for (const remote of existingIndexes) {
851
743
  if (!remote.name.startsWith(INDEX_PREFIX)) continue;
852
744
  if (indexesToCreate.has(remote.name)) {
@@ -856,54 +748,21 @@ else {
856
748
  case "unique":
857
749
  case "text": {
858
750
  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
- }
863
- break;
864
- }
865
- default:
866
- }
867
- } else {
868
- this.logger.debug(`dropping index "${remote.name}"`);
869
- await this.collection.dropIndex(remote.name);
870
- }
871
- }
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);
751
+ else await this.collection.dropIndex(remote.name);
890
752
  break;
891
753
  }
892
754
  default:
893
755
  }
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`);
756
+ } else await this.collection.dropIndex(remote.name);
898
757
  }
899
- for (const [key, value] of Array.from(indexesToCreate.entries())) switch (value.type) {
758
+ for (const [key, value] of allIndexes.entries()) switch (value.type) {
900
759
  case "plain": {
901
- this.logger.debug(`creating index "${key}"`);
760
+ if (!indexesToCreate.has(key)) continue;
902
761
  await this.collection.createIndex(value.fields, { name: key });
903
762
  break;
904
763
  }
905
764
  case "unique": {
906
- this.logger.debug(`creating index "${key}"`);
765
+ if (!indexesToCreate.has(key)) continue;
907
766
  await this.collection.createIndex(value.fields, {
908
767
  name: key,
909
768
  unique: true
@@ -911,160 +770,168 @@ else toUpdate.add(remote.name);
911
770
  break;
912
771
  }
913
772
  case "text": {
914
- this.logger.debug(`creating index "${key}"`);
773
+ if (!indexesToCreate.has(key)) continue;
915
774
  await this.collection.createIndex(value.fields, {
916
775
  weights: value.weights,
917
776
  name: key
918
777
  });
919
778
  break;
920
779
  }
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({
780
+ default:
781
+ }
782
+ try {
783
+ const toUpdate = new Set();
784
+ const existingSearchIndexes = await this.collection.listSearchIndexes().toArray();
785
+ for (const remote of existingSearchIndexes) {
786
+ if (!remote.name.startsWith(INDEX_PREFIX)) continue;
787
+ if (indexesToCreate.has(remote.name)) {
788
+ const local = indexesToCreate.get(remote.name);
789
+ const right = remote.latestDefinition;
790
+ switch (local.type) {
791
+ case "dynamic_text":
792
+ case "search_text": {
793
+ const left = local.definition;
794
+ if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
795
+ else toUpdate.add(remote.name);
796
+ break;
797
+ }
798
+ case "vector": {
799
+ if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
800
+ else toUpdate.add(remote.name);
801
+ break;
802
+ }
803
+ default:
804
+ }
805
+ } else if (remote.status !== "DELETING") await this.collection.dropSearchIndex(remote.name);
806
+ }
807
+ for (const [key, value] of indexesToCreate.entries()) switch (value.type) {
808
+ case "dynamic_text":
809
+ case "search_text":
810
+ case "vector": {
811
+ if (toUpdate.has(key)) await this.collection.updateSearchIndex(key, value.definition);
812
+ else await this.collection.createSearchIndex({
930
813
  name: key,
931
814
  type: value.type === "vector" ? "vectorSearch" : "search",
932
815
  definition: value.definition
933
816
  });
817
+ break;
934
818
  }
935
- break;
819
+ default:
820
+ }
821
+ } catch {}
822
+ }
823
+ /** Returns physical field names of increment fields that are undefined in the data. */ _fieldsNeedingIncrement(data) {
824
+ const result = [];
825
+ for (const physical of this._incrementFields) if (data[physical] === undefined || data[physical] === null) result.push(physical);
826
+ return result;
827
+ }
828
+ /** Reads current max value for each field via $group aggregation. */ async _getMaxValues(physicalFields) {
829
+ const aliases = physicalFields.map((f) => [`max__${f.replace(/\./g, "__")}`, f]);
830
+ const group = { _id: null };
831
+ for (const [alias, field] of aliases) group[alias] = { $max: `$${field}` };
832
+ const result = await this.collection.aggregate([{ $group: group }]).toArray();
833
+ const maxMap = new Map();
834
+ if (result.length > 0) {
835
+ const row = result[0];
836
+ for (const [alias, field] of aliases) {
837
+ const val = row[alias];
838
+ maxMap.set(field, typeof val === "number" ? val : 0);
936
839
  }
937
- default:
938
840
  }
841
+ return maxMap;
842
+ }
843
+ _buildFindOptions(controls) {
844
+ const opts = {};
845
+ if (!controls) return opts;
846
+ if (controls.$sort) opts.sort = controls.$sort;
847
+ if (controls.$limit) opts.limit = controls.$limit;
848
+ if (controls.$skip) opts.skip = controls.$skip;
849
+ if (controls.$select) opts.projection = controls.$select.asProjection;
850
+ return opts;
851
+ }
852
+ _addMongoIndexField(type, name, field, weight) {
853
+ const key = mongoIndexKey(type, name);
854
+ let index = this._mongoIndexes.get(key);
855
+ const value = type === "text" ? "text" : 1;
856
+ if (index) index.fields[field] = value;
857
+ else {
858
+ index = {
859
+ key,
860
+ name,
861
+ type,
862
+ fields: { [field]: value },
863
+ weights: {}
864
+ };
865
+ this._mongoIndexes.set(key, index);
866
+ }
867
+ if (weight) index.weights[field] = weight;
939
868
  }
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
869
+ _setSearchIndex(type, name, definition) {
870
+ const key = mongoIndexKey(type, name || DEFAULT_INDEX_NAME);
871
+ this._mongoIndexes.set(key, {
872
+ key,
873
+ name: name || DEFAULT_INDEX_NAME,
874
+ type,
875
+ definition
956
876
  });
957
877
  }
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
- };
878
+ _addFieldToSearchIndex(type, _name, fieldName, analyzer) {
879
+ const name = _name || DEFAULT_INDEX_NAME;
880
+ let index = this._mongoIndexes.get(mongoIndexKey(type, name));
881
+ if (!index && type === "search_text") {
882
+ this._setSearchIndex(type, name, {
883
+ mappings: { fields: {} },
884
+ text: { fuzzy: { maxEdits: 0 } }
885
+ });
886
+ index = this._mongoIndexes.get(mongoIndexKey(type, name));
887
+ }
888
+ if (index) {
889
+ index.definition.mappings.fields[fieldName] = { type: "string" };
890
+ if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
988
891
  }
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);
892
+ }
893
+ constructor(db, asMongo) {
894
+ 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
895
  }
1022
896
  };
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;
897
+ function objMatch(o1, o2) {
898
+ const keys1 = Object.keys(o1);
899
+ const keys2 = Object.keys(o2);
900
+ if (keys1.length !== keys2.length) return false;
901
+ return keys1.every((key) => o1[key] === o2[key]);
1044
902
  }
1045
- /**
1046
- * Search Index fields matching
1047
- */ function fieldsMatch(left, right) {
903
+ function fieldsMatch(left, right) {
1048
904
  if (!left || !right) return left === right;
1049
905
  const leftKeys = Object.keys(left);
1050
906
  const rightKeys = Object.keys(right);
1051
907
  if (leftKeys.length !== rightKeys.length) return false;
1052
908
  return leftKeys.every((key) => {
1053
909
  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;
910
+ return left[key].type === right[key].type && left[key].analyzer === right[key].analyzer;
1057
911
  });
1058
912
  }
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]);
913
+ function vectorFieldsMatch(left, right) {
914
+ const leftMap = new Map(left.map((f) => [f.path, f]));
915
+ const rightMap = new Map((right || []).map((f) => [f.path, f]));
916
+ if (leftMap.size !== rightMap.size) return false;
917
+ for (const [key, l] of leftMap.entries()) {
918
+ const r = rightMap.get(key);
919
+ if (!r) return false;
920
+ if (l.type !== r.type || l.path !== r.path || l.similarity !== r.similarity || l.numDimensions !== r.numDimensions) return false;
921
+ }
922
+ return true;
1066
923
  }
1067
924
 
925
+ //#endregion
926
+ //#region packages/mongo/src/lib/logger.ts
927
+ const NoopLogger = {
928
+ error: () => {},
929
+ warn: () => {},
930
+ log: () => {},
931
+ info: () => {},
932
+ debug: () => {}
933
+ };
934
+
1068
935
  //#endregion
1069
936
  //#region packages/mongo/src/lib/as-mongo.ts
1070
937
  function _define_property(obj, key, value) {
@@ -1089,27 +956,39 @@ var AsMongo = class {
1089
956
  const list = await this.getCollectionsList();
1090
957
  return list.has(name);
1091
958
  }
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);
959
+ getAdapter(type) {
960
+ this._ensureCreated(type);
961
+ return this._adapters.get(type);
962
+ }
963
+ getTable(type, logger) {
964
+ this._ensureCreated(type, logger);
965
+ return this._tables.get(type);
966
+ }
967
+ _ensureCreated(type, logger) {
968
+ if (!this._adapters.has(type)) {
969
+ const adapter = new MongoAdapter(this.db, this);
970
+ const table = new __atscript_utils_db.AtscriptDbTable(type, adapter, logger || this.logger);
971
+ this._adapters.set(type, adapter);
972
+ this._tables.set(type, table);
1097
973
  }
1098
- return collection;
1099
974
  }
1100
975
  constructor(client, logger = NoopLogger) {
1101
976
  _define_property(this, "logger", void 0);
1102
977
  _define_property(this, "client", void 0);
1103
978
  _define_property(this, "collectionsList", void 0);
1104
- _define_property(this, "_collections", void 0);
979
+ _define_property(this, "_adapters", void 0);
980
+ _define_property(this, "_tables", void 0);
1105
981
  this.logger = logger;
1106
- this._collections = new WeakMap();
982
+ this._adapters = new WeakMap();
983
+ this._tables = new WeakMap();
1107
984
  if (typeof client === "string") this.client = new mongodb.MongoClient(client);
1108
985
  else this.client = client;
1109
986
  }
1110
987
  };
1111
988
 
1112
989
  //#endregion
1113
- exports.AsCollection = AsCollection
1114
990
  exports.AsMongo = AsMongo
1115
- exports.MongoPlugin = MongoPlugin
991
+ exports.CollectionPatcher = CollectionPatcher
992
+ exports.MongoAdapter = MongoAdapter
993
+ exports.buildMongoFilter = buildMongoFilter
994
+ exports.validateMongoIdPlugin = validateMongoIdPlugin