@atscript/mongo 0.0.1
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/LICENSE +21 -0
- package/dist/index.cjs +611 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.mjs +585 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Atscript
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
const __atscript_core = __toESM(require("@atscript/core"));
|
|
26
|
+
const __atscript_typescript = __toESM(require("@atscript/typescript"));
|
|
27
|
+
const mongodb = __toESM(require("mongodb"));
|
|
28
|
+
|
|
29
|
+
//#region packages/mongo/src/plugin/primitives.ts
|
|
30
|
+
const primitives = { mongo: { extensions: {
|
|
31
|
+
objectId: {
|
|
32
|
+
type: "string",
|
|
33
|
+
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"
|
|
34
|
+
},
|
|
35
|
+
vector: {
|
|
36
|
+
type: {
|
|
37
|
+
kind: "array",
|
|
38
|
+
of: "number"
|
|
39
|
+
},
|
|
40
|
+
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"
|
|
41
|
+
}
|
|
42
|
+
} } };
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region packages/mongo/src/plugin/annotations.ts
|
|
46
|
+
const analyzers = [
|
|
47
|
+
"lucene.standard",
|
|
48
|
+
"lucene.simple",
|
|
49
|
+
"lucene.whitespace",
|
|
50
|
+
"lucene.english",
|
|
51
|
+
"lucene.french",
|
|
52
|
+
"lucene.german",
|
|
53
|
+
"lucene.italian",
|
|
54
|
+
"lucene.portuguese",
|
|
55
|
+
"lucene.spanish",
|
|
56
|
+
"lucene.chinese",
|
|
57
|
+
"lucene.hindi",
|
|
58
|
+
"lucene.bengali",
|
|
59
|
+
"lucene.russian",
|
|
60
|
+
"lucene.arabic"
|
|
61
|
+
];
|
|
62
|
+
const annotations = { mongo: {
|
|
63
|
+
collection: new __atscript_core.AnnotationSpec({
|
|
64
|
+
description: "Defines a **MongoDB collection**. This annotation is required to mark an interface as a collection.\n\n- Automatically enforces a **non-optional** `_id` field.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n- Ensures that `_id` is included if not explicitly defined.\n\n**Example:**\n```atscript\n@mongo.collection \"users\"\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
|
|
65
|
+
nodeType: ["interface"],
|
|
66
|
+
argument: {
|
|
67
|
+
name: "name",
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "The **name of the MongoDB collection**."
|
|
70
|
+
},
|
|
71
|
+
validate(token, args, doc) {
|
|
72
|
+
const parent = token.parentNode;
|
|
73
|
+
const struc = parent?.getDefinition();
|
|
74
|
+
const errors = [];
|
|
75
|
+
if ((0, __atscript_core.isInterface)(parent) && parent.props.has("_id") && (0, __atscript_core.isStructure)(struc)) {
|
|
76
|
+
const _id = parent.props.get("_id");
|
|
77
|
+
const isOptional = !!_id.token("optional");
|
|
78
|
+
if (isOptional) errors.push({
|
|
79
|
+
message: `[mongo] _id can't be optional in Mongo Collection`,
|
|
80
|
+
severity: 1,
|
|
81
|
+
range: _id.token("identifier").range
|
|
82
|
+
});
|
|
83
|
+
const definition = _id.getDefinition();
|
|
84
|
+
if (!definition) return errors;
|
|
85
|
+
let wrongType = false;
|
|
86
|
+
if ((0, __atscript_core.isRef)(definition)) {
|
|
87
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
88
|
+
if ((0, __atscript_core.isPrimitive)(def) && !["string", "number"].includes(def.config.type)) wrongType = true;
|
|
89
|
+
} else wrongType = true;
|
|
90
|
+
if (wrongType) errors.push({
|
|
91
|
+
message: `[mongo] _id must be of type string, number or mongo.objectId`,
|
|
92
|
+
severity: 1,
|
|
93
|
+
range: _id.token("identifier").range
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
},
|
|
98
|
+
modify(token, args, doc) {
|
|
99
|
+
const parent = token.parentNode;
|
|
100
|
+
const struc = parent?.getDefinition();
|
|
101
|
+
if ((0, __atscript_core.isInterface)(parent) && !parent.props.has("_id") && (0, __atscript_core.isStructure)(struc)) struc.addVirtualProp({
|
|
102
|
+
name: "_id",
|
|
103
|
+
type: "mongo.objectId",
|
|
104
|
+
documentation: "Mongodb Primary Key ObjectId"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
index: {
|
|
109
|
+
plain: new __atscript_core.AnnotationSpec({
|
|
110
|
+
description: "Defines a **standard MongoDB index** on a field.\n\n- Improves query performance on indexed fields.\n- Can be used for **single-field** or **compound** indexes.\n\n**Example:**\n```atscript\n@mongo.index.plain \"departmentIndex\"\ndepartment: string\n```\n",
|
|
111
|
+
multiple: true,
|
|
112
|
+
nodeType: ["prop"],
|
|
113
|
+
argument: {
|
|
114
|
+
optional: true,
|
|
115
|
+
name: "indexName",
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "The **name of the index** (optional). If omitted, property name is used."
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
unique: new __atscript_core.AnnotationSpec({
|
|
121
|
+
description: "Creates a **unique index** on a field to ensure no duplicate values exist.\n\n- Enforces uniqueness at the database level.\n- Automatically prevents duplicate entries.\n- Typically used for **emails, usernames, and IDs**.\n\n**Example:**\n```atscript\n@mongo.index.unique \"uniqueEmailIndex\"\nemail: string.email\n```\n",
|
|
122
|
+
multiple: true,
|
|
123
|
+
nodeType: ["prop"],
|
|
124
|
+
argument: {
|
|
125
|
+
optional: true,
|
|
126
|
+
name: "indexName",
|
|
127
|
+
type: "string",
|
|
128
|
+
description: "The **name of the unique index** (optional). If omitted, property name is used."
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
text: new __atscript_core.AnnotationSpec({
|
|
132
|
+
description: "Creates a **legacy MongoDB text index** for full-text search.\n\n**⚠ WARNING:** *Text indexes slow down database operations. Use `@mongo.defineTextSearch` instead for better performance.*\n\n- Allows **basic full-text search** on a field.\n- Does **not support fuzzy matching or ranking**.\n- **Replaced by MongoDB Atlas Search Indexes (`@mongo.searchIndex.text`).**\n\n**Example:**\n```atscript\n@mongo.index.text 5\nbio: string\n```\n",
|
|
133
|
+
nodeType: ["prop"],
|
|
134
|
+
argument: {
|
|
135
|
+
optional: true,
|
|
136
|
+
name: "weight",
|
|
137
|
+
type: "number",
|
|
138
|
+
description: "Field importance in search results (higher = more relevant). Defaults to `1`."
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
search: {
|
|
143
|
+
dynamic: new __atscript_core.AnnotationSpec({
|
|
144
|
+
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@mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
|
|
145
|
+
nodeType: ["interface"],
|
|
146
|
+
multiple: false,
|
|
147
|
+
argument: [{
|
|
148
|
+
optional: true,
|
|
149
|
+
name: "analyzer",
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "The **text analyzer** for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, etc.",
|
|
152
|
+
values: analyzers
|
|
153
|
+
}, {
|
|
154
|
+
optional: true,
|
|
155
|
+
name: "fuzzy",
|
|
156
|
+
type: "number",
|
|
157
|
+
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\"`)."
|
|
158
|
+
}]
|
|
159
|
+
}),
|
|
160
|
+
static: new __atscript_core.AnnotationSpec({
|
|
161
|
+
description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@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 `@mongo.useTextSearch`** to be included in this search index.\n\n**Example:**\n```atscript\n@mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
|
|
162
|
+
nodeType: ["interface"],
|
|
163
|
+
multiple: true,
|
|
164
|
+
argument: [
|
|
165
|
+
{
|
|
166
|
+
optional: true,
|
|
167
|
+
name: "analyzer",
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
170
|
+
values: analyzers
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
optional: true,
|
|
174
|
+
name: "fuzzy",
|
|
175
|
+
type: "number",
|
|
176
|
+
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\")."
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
optional: true,
|
|
180
|
+
name: "indexName",
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "The name of the search index. Fields must reference this name using `@mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}),
|
|
186
|
+
text: new __atscript_core.AnnotationSpec({
|
|
187
|
+
description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@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@mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
|
|
188
|
+
nodeType: ["prop"],
|
|
189
|
+
multiple: true,
|
|
190
|
+
argument: [{
|
|
191
|
+
optional: true,
|
|
192
|
+
name: "analyzer",
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
195
|
+
values: analyzers
|
|
196
|
+
}, {
|
|
197
|
+
optional: true,
|
|
198
|
+
name: "indexName",
|
|
199
|
+
type: "string",
|
|
200
|
+
description: "The **name of the search index** defined in `@mongo.defineTextSearch`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
|
|
201
|
+
}]
|
|
202
|
+
}),
|
|
203
|
+
vector: new __atscript_core.AnnotationSpec({
|
|
204
|
+
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@mongo.search.vector 512, \"cosine\"\nembedding: mongo.vector\n```\n",
|
|
205
|
+
nodeType: ["prop"],
|
|
206
|
+
multiple: false,
|
|
207
|
+
argument: [
|
|
208
|
+
{
|
|
209
|
+
optional: false,
|
|
210
|
+
name: "dimensions",
|
|
211
|
+
type: "number",
|
|
212
|
+
description: "The **number of dimensions in the vector** (e.g., 512 for OpenAI embeddings).",
|
|
213
|
+
values: [
|
|
214
|
+
"512",
|
|
215
|
+
"768",
|
|
216
|
+
"1024",
|
|
217
|
+
"1536",
|
|
218
|
+
"3072",
|
|
219
|
+
"4096"
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
optional: true,
|
|
224
|
+
name: "similarity",
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "The **similarity metric** used for vector search. Defaults to `\"cosine\"`.\n\n**Available options:** `\"cosine\"`, `\"euclidean\"`, `\"dotProduct\"`.",
|
|
227
|
+
values: [
|
|
228
|
+
"cosine",
|
|
229
|
+
"euclidean",
|
|
230
|
+
"dotProduct"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
optional: true,
|
|
235
|
+
name: "indexName",
|
|
236
|
+
type: "string",
|
|
237
|
+
description: "The **name of the vector search index** (optional, defaults to property name)."
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}),
|
|
241
|
+
filter: new __atscript_core.AnnotationSpec({
|
|
242
|
+
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 `@mongo.search.vector`.\n\n**Example:**\n```atscript\n@mongo.search.vector 512, \"cosine\"\nembedding: number[]\n\n@mongo.search.filter \"embedding\"\ncategory: string\n```\n",
|
|
243
|
+
nodeType: ["prop"],
|
|
244
|
+
multiple: true,
|
|
245
|
+
argument: [{
|
|
246
|
+
optional: false,
|
|
247
|
+
name: "indexName",
|
|
248
|
+
type: "string",
|
|
249
|
+
description: "The **name of the vector search index** this field should be used as a filter for."
|
|
250
|
+
}]
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
} };
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region packages/mongo/src/plugin/index.ts
|
|
257
|
+
const MongoPlugin = () => {
|
|
258
|
+
return {
|
|
259
|
+
name: "mongo",
|
|
260
|
+
config() {
|
|
261
|
+
return {
|
|
262
|
+
primitives,
|
|
263
|
+
annotations
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region packages/mongo/src/lib/logger.ts
|
|
271
|
+
const NoopLogger = {
|
|
272
|
+
error: () => {},
|
|
273
|
+
warn: () => {},
|
|
274
|
+
log: () => {},
|
|
275
|
+
info: () => {},
|
|
276
|
+
debug: () => {}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region packages/mongo/src/lib/as-collection.ts
|
|
281
|
+
function _define_property$1(obj, key, value) {
|
|
282
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
283
|
+
value,
|
|
284
|
+
enumerable: true,
|
|
285
|
+
configurable: true,
|
|
286
|
+
writable: true
|
|
287
|
+
});
|
|
288
|
+
else obj[key] = value;
|
|
289
|
+
return obj;
|
|
290
|
+
}
|
|
291
|
+
const INDEX_PREFIX = "anscript__";
|
|
292
|
+
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
293
|
+
function indexKey(type, name) {
|
|
294
|
+
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
295
|
+
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
296
|
+
}
|
|
297
|
+
var AsCollection = class {
|
|
298
|
+
async exists() {
|
|
299
|
+
return this.asMongo.collectionExists(this.name);
|
|
300
|
+
}
|
|
301
|
+
async ensureExists() {
|
|
302
|
+
const exists = await this.exists();
|
|
303
|
+
if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
|
|
304
|
+
}
|
|
305
|
+
get type() {
|
|
306
|
+
return this._type;
|
|
307
|
+
}
|
|
308
|
+
get indexes() {
|
|
309
|
+
this._flatten();
|
|
310
|
+
return this._indexes;
|
|
311
|
+
}
|
|
312
|
+
_addIndexField(type, name, field, weight) {
|
|
313
|
+
const key = indexKey(type, name);
|
|
314
|
+
let index = this._indexes.get(key);
|
|
315
|
+
const value = type === "text" ? "text" : 1;
|
|
316
|
+
if (index) index.fields[field] = value;
|
|
317
|
+
else {
|
|
318
|
+
const weights = {};
|
|
319
|
+
index = {
|
|
320
|
+
type,
|
|
321
|
+
fields: { [field]: value },
|
|
322
|
+
weights
|
|
323
|
+
};
|
|
324
|
+
this._indexes.set(key, index);
|
|
325
|
+
}
|
|
326
|
+
if (weight) index.weights[field] = weight;
|
|
327
|
+
}
|
|
328
|
+
_setSearchIndex(type, name, definition) {
|
|
329
|
+
this._indexes.set(indexKey(type, name || DEFAULT_INDEX_NAME), {
|
|
330
|
+
type,
|
|
331
|
+
definition
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
_addFieldToSearchIndex(type, _name, fieldName, analyzer) {
|
|
335
|
+
const name = _name || DEFAULT_INDEX_NAME;
|
|
336
|
+
let index = this._indexes.get(indexKey(type, name));
|
|
337
|
+
if (!index && type === "search_text") {
|
|
338
|
+
this._setSearchIndex(type, name, {
|
|
339
|
+
mappings: { fields: {} },
|
|
340
|
+
text: { fuzzy: { maxEdits: 0 } }
|
|
341
|
+
});
|
|
342
|
+
index = this._indexes.get(indexKey(type, name));
|
|
343
|
+
}
|
|
344
|
+
if (index) {
|
|
345
|
+
index.definition.mappings.fields[fieldName] = { type: "string" };
|
|
346
|
+
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
_flattenType(type, prefix) {
|
|
350
|
+
switch (type.type.kind) {
|
|
351
|
+
case "object":
|
|
352
|
+
const items = Array.from(type.type.props.entries());
|
|
353
|
+
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
|
|
354
|
+
break;
|
|
355
|
+
case "array":
|
|
356
|
+
this._flattenType(type.type.of, prefix);
|
|
357
|
+
break;
|
|
358
|
+
case "intersection":
|
|
359
|
+
case "tuple":
|
|
360
|
+
case "union": for (const item of type.type.items) this._flattenType(item, prefix);
|
|
361
|
+
default:
|
|
362
|
+
this._flatMap?.set(prefix || "", type);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (prefix) this._prepareIndexesForField(prefix, type.metadata);
|
|
366
|
+
}
|
|
367
|
+
_prepareIndexesForCollection() {
|
|
368
|
+
const typeMeta = this.type.metadata;
|
|
369
|
+
const dynamicText = typeMeta.get("mongo.search.dynamic");
|
|
370
|
+
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
371
|
+
mappings: { dynamic: true },
|
|
372
|
+
analyzer: dynamicText.analyzer,
|
|
373
|
+
text: { fuzzy: { maxEdits: dynamicText.fuzzy || 0 } }
|
|
374
|
+
});
|
|
375
|
+
for (const textSearch of typeMeta.get("mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
|
|
376
|
+
mappings: { fields: {} },
|
|
377
|
+
analyzer: textSearch.analyzer,
|
|
378
|
+
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
_finalizeIndexesForCollection() {
|
|
382
|
+
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
383
|
+
const index = this._indexes.get(key);
|
|
384
|
+
if (index && index.type === "vector") index.definition.fields?.push({
|
|
385
|
+
type: "filter",
|
|
386
|
+
path: value
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
_prepareIndexesForField(fieldName, metadata) {
|
|
391
|
+
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
392
|
+
for (const index of metadata.get("mongo.index.unique") || []) this._addIndexField("unique", index === true ? fieldName : index, fieldName);
|
|
393
|
+
const textWeight = metadata.get("mongo.index.text");
|
|
394
|
+
if (textWeight) this._addIndexField("text", "", fieldName, textWeight === true ? 1 : textWeight);
|
|
395
|
+
for (const index of metadata.get("mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, fieldName, index.analyzer);
|
|
396
|
+
const vectorIndex = metadata.get("mongo.search.vector");
|
|
397
|
+
if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || fieldName, { fields: [{
|
|
398
|
+
type: "vector",
|
|
399
|
+
path: fieldName,
|
|
400
|
+
similarity: vectorIndex.similarity || "dotProduct",
|
|
401
|
+
numDimensions: vectorIndex.dimensions
|
|
402
|
+
}] });
|
|
403
|
+
for (const index of metadata.get("mongo.search.filter") || []) this._vectorFilters.set(indexKey("vector", index.indexName), fieldName);
|
|
404
|
+
}
|
|
405
|
+
_flatten() {
|
|
406
|
+
if (!this._flatMap) {
|
|
407
|
+
this._flatMap = new Map();
|
|
408
|
+
this._prepareIndexesForCollection();
|
|
409
|
+
this._flattenType(this.type);
|
|
410
|
+
this._finalizeIndexesForCollection();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
get flatMap() {
|
|
414
|
+
this._flatten();
|
|
415
|
+
return this._flatMap;
|
|
416
|
+
}
|
|
417
|
+
async syncIndexes() {
|
|
418
|
+
await this.ensureExists();
|
|
419
|
+
const existingIndexes = await this.collection.listIndexes().toArray();
|
|
420
|
+
const indexesToCreate = new Map(this.indexes);
|
|
421
|
+
for (const remote of existingIndexes) {
|
|
422
|
+
if (!remote.name.startsWith(INDEX_PREFIX)) continue;
|
|
423
|
+
if (indexesToCreate.has(remote.name)) {
|
|
424
|
+
const local = indexesToCreate.get(remote.name);
|
|
425
|
+
switch (local.type) {
|
|
426
|
+
case "plain":
|
|
427
|
+
case "unique":
|
|
428
|
+
case "text":
|
|
429
|
+
if ((local.type === "text" || objMatch(local.fields, remote.key)) && objMatch(local.weights || {}, remote.weights || {})) indexesToCreate.delete(remote.name);
|
|
430
|
+
else {
|
|
431
|
+
this.logger.debug(`dropping index "${remote.name}"`);
|
|
432
|
+
await this.collection.dropIndex(remote.name);
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
this.logger.debug(`dropping index "${remote.name}"`);
|
|
439
|
+
await this.collection.dropIndex(remote.name);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const toUpdate = new Set();
|
|
443
|
+
const existingSearchIndexes = await this.collection.listSearchIndexes().toArray();
|
|
444
|
+
for (const remote of existingSearchIndexes) {
|
|
445
|
+
if (!remote.name.startsWith(INDEX_PREFIX)) continue;
|
|
446
|
+
if (indexesToCreate.has(remote.name)) {
|
|
447
|
+
const local = indexesToCreate.get(remote.name);
|
|
448
|
+
const right = remote.latestDefinition;
|
|
449
|
+
switch (local.type) {
|
|
450
|
+
case "dynamic_text":
|
|
451
|
+
case "search_text":
|
|
452
|
+
let left = local.definition;
|
|
453
|
+
if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
|
|
454
|
+
else toUpdate.add(remote.name);
|
|
455
|
+
break;
|
|
456
|
+
case "vector":
|
|
457
|
+
if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
|
|
458
|
+
else toUpdate.add(remote.name);
|
|
459
|
+
break;
|
|
460
|
+
default:
|
|
461
|
+
}
|
|
462
|
+
} else if (remote.status !== "DELETING") {
|
|
463
|
+
this.logger.debug(`dropping search index "${remote.name}"`);
|
|
464
|
+
await this.collection.dropSearchIndex(remote.name);
|
|
465
|
+
} else this.logger.debug(`search index "${remote.name}" is in deleting status`);
|
|
466
|
+
}
|
|
467
|
+
for (const [key, value] of Array.from(indexesToCreate.entries())) switch (value.type) {
|
|
468
|
+
case "plain":
|
|
469
|
+
this.logger.debug(`creating index "${key}"`);
|
|
470
|
+
await this.collection.createIndex(value.fields, { name: key });
|
|
471
|
+
break;
|
|
472
|
+
case "unique":
|
|
473
|
+
this.logger.debug(`creating index "${key}"`);
|
|
474
|
+
await this.collection.createIndex(value.fields, {
|
|
475
|
+
name: key,
|
|
476
|
+
unique: true
|
|
477
|
+
});
|
|
478
|
+
break;
|
|
479
|
+
case "text":
|
|
480
|
+
this.logger.debug(`creating index "${key}"`);
|
|
481
|
+
await this.collection.createIndex(value.fields, {
|
|
482
|
+
weights: value.weights,
|
|
483
|
+
name: key
|
|
484
|
+
});
|
|
485
|
+
break;
|
|
486
|
+
case "dynamic_text":
|
|
487
|
+
case "search_text":
|
|
488
|
+
case "vector":
|
|
489
|
+
if (toUpdate.has(key)) {
|
|
490
|
+
this.logger.debug(`updating search index "${key}"`);
|
|
491
|
+
await this.collection.updateSearchIndex(key, value.definition);
|
|
492
|
+
} else {
|
|
493
|
+
this.logger.debug(`creating search index "${key}"`);
|
|
494
|
+
await this.collection.createSearchIndex({
|
|
495
|
+
name: key,
|
|
496
|
+
type: value.type === "vector" ? "vectorSearch" : "search",
|
|
497
|
+
definition: value.definition
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
constructor(asMongo, _type, logger = NoopLogger) {
|
|
505
|
+
_define_property$1(this, "asMongo", void 0);
|
|
506
|
+
_define_property$1(this, "_type", void 0);
|
|
507
|
+
_define_property$1(this, "logger", void 0);
|
|
508
|
+
_define_property$1(this, "name", void 0);
|
|
509
|
+
_define_property$1(this, "collection", void 0);
|
|
510
|
+
_define_property$1(this, "_indexes", void 0);
|
|
511
|
+
_define_property$1(this, "_vectorFilters", void 0);
|
|
512
|
+
_define_property$1(this, "_flatMap", void 0);
|
|
513
|
+
this.asMongo = asMongo;
|
|
514
|
+
this._type = _type;
|
|
515
|
+
this.logger = logger;
|
|
516
|
+
this._indexes = new Map();
|
|
517
|
+
this._vectorFilters = new Map();
|
|
518
|
+
if (!(0, __atscript_typescript.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
519
|
+
const name = _type.metadata.get("mongo.collection");
|
|
520
|
+
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
|
521
|
+
if (_type.type.kind !== "object") throw new Error("Mongo collection must be an object type");
|
|
522
|
+
this.name = name;
|
|
523
|
+
this.collection = asMongo.db.collection(name);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
/**
|
|
527
|
+
* Vector Index fields matching
|
|
528
|
+
*/ function vectorFieldsMatch(left, right) {
|
|
529
|
+
const leftMap = new Map();
|
|
530
|
+
left.forEach((f) => leftMap.set(f.path, f));
|
|
531
|
+
const rightMap = new Map();
|
|
532
|
+
(right || []).forEach((f) => rightMap.set(f.path, f));
|
|
533
|
+
if (leftMap.size === rightMap.size) {
|
|
534
|
+
let match = true;
|
|
535
|
+
for (const [key, left$1] of leftMap.entries()) {
|
|
536
|
+
const right$1 = rightMap.get(key);
|
|
537
|
+
if (!right$1) {
|
|
538
|
+
match = false;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
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;
|
|
542
|
+
match = false;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
return match;
|
|
546
|
+
} else return false;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Search Index fields matching
|
|
550
|
+
*/ function fieldsMatch(left, right) {
|
|
551
|
+
if (!left || !right) return left === right;
|
|
552
|
+
const leftKeys = Object.keys(left);
|
|
553
|
+
const rightKeys = Object.keys(right);
|
|
554
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
555
|
+
return leftKeys.every((key) => {
|
|
556
|
+
if (!(key in right)) return false;
|
|
557
|
+
const leftField = left[key];
|
|
558
|
+
const rightField = right[key];
|
|
559
|
+
return leftField.type === rightField.type && leftField.analyzer === rightField.analyzer;
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Shallow object matching
|
|
564
|
+
*/ function objMatch(o1, o2) {
|
|
565
|
+
const keys1 = Object.keys(o1);
|
|
566
|
+
const keys2 = Object.keys(o2);
|
|
567
|
+
if (keys1.length !== keys2.length) return false;
|
|
568
|
+
return keys1.every((key) => o1[key] === o2[key]);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region packages/mongo/src/lib/as-mongo.ts
|
|
573
|
+
function _define_property(obj, key, value) {
|
|
574
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
575
|
+
value,
|
|
576
|
+
enumerable: true,
|
|
577
|
+
configurable: true,
|
|
578
|
+
writable: true
|
|
579
|
+
});
|
|
580
|
+
else obj[key] = value;
|
|
581
|
+
return obj;
|
|
582
|
+
}
|
|
583
|
+
var AsMongo = class {
|
|
584
|
+
get db() {
|
|
585
|
+
return this.client.db();
|
|
586
|
+
}
|
|
587
|
+
getCollectionsList() {
|
|
588
|
+
if (!this.collectionsList) this.collectionsList = this.db.listCollections().toArray().then((c) => new Set(c.map((c$1) => c$1.name)));
|
|
589
|
+
return this.collectionsList;
|
|
590
|
+
}
|
|
591
|
+
async collectionExists(name) {
|
|
592
|
+
const list = await this.getCollectionsList();
|
|
593
|
+
return list.has(name);
|
|
594
|
+
}
|
|
595
|
+
getCollection(type, logger) {
|
|
596
|
+
return new AsCollection(this, type, logger || this.logger);
|
|
597
|
+
}
|
|
598
|
+
constructor(client, logger = NoopLogger) {
|
|
599
|
+
_define_property(this, "logger", void 0);
|
|
600
|
+
_define_property(this, "client", void 0);
|
|
601
|
+
_define_property(this, "collectionsList", void 0);
|
|
602
|
+
this.logger = logger;
|
|
603
|
+
if (typeof client === "string") this.client = new mongodb.MongoClient(client);
|
|
604
|
+
else this.client = client;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
//#endregion
|
|
609
|
+
exports.AsCollection = AsCollection
|
|
610
|
+
exports.AsMongo = AsMongo
|
|
611
|
+
exports.MongoPlugin = MongoPlugin
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TAtscriptPlugin } from '@atscript/core';
|
|
2
|
+
import { TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap } from '@atscript/typescript';
|
|
3
|
+
import * as mongodb from 'mongodb';
|
|
4
|
+
import { MongoClient, Collection } from 'mongodb';
|
|
5
|
+
|
|
6
|
+
declare const MongoPlugin: () => TAtscriptPlugin;
|
|
7
|
+
|
|
8
|
+
interface TGenericLogger {
|
|
9
|
+
error(...messages: any[]): void;
|
|
10
|
+
warn(...messages: any[]): void;
|
|
11
|
+
log(...messages: any[]): void;
|
|
12
|
+
info(...messages: any[]): void;
|
|
13
|
+
debug(...messages: any[]): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare class AsMongo {
|
|
17
|
+
protected readonly logger: TGenericLogger;
|
|
18
|
+
readonly client: MongoClient;
|
|
19
|
+
constructor(client: string | MongoClient, logger?: TGenericLogger);
|
|
20
|
+
get db(): mongodb.Db;
|
|
21
|
+
protected collectionsList?: Promise<Set<string>>;
|
|
22
|
+
protected getCollectionsList(): Promise<Set<string>>;
|
|
23
|
+
collectionExists(name: string): Promise<boolean>;
|
|
24
|
+
getCollection<T extends TAtscriptAnnotatedType & (new (...args: any[]) => any)>(type: T, logger?: TGenericLogger): AsCollection<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TPlainIndex = {
|
|
28
|
+
type: 'plain' | 'unique' | 'text';
|
|
29
|
+
fields: Record<string, 1 | 'text'>;
|
|
30
|
+
weights: Record<string, number>;
|
|
31
|
+
};
|
|
32
|
+
type TSearchIndex = {
|
|
33
|
+
type: 'dynamic_text' | 'search_text' | 'vector';
|
|
34
|
+
definition: TMongoSearchIndexDefinition;
|
|
35
|
+
};
|
|
36
|
+
type TIndex = TPlainIndex | TSearchIndex;
|
|
37
|
+
declare class AsCollection<T extends TAtscriptAnnotatedType & (new (...args: any[]) => any)> {
|
|
38
|
+
protected readonly asMongo: AsMongo;
|
|
39
|
+
protected readonly _type: T;
|
|
40
|
+
protected readonly logger: TGenericLogger;
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly collection: Collection<InstanceType<T>>;
|
|
43
|
+
constructor(asMongo: AsMongo, _type: T, logger?: TGenericLogger);
|
|
44
|
+
exists(): Promise<boolean>;
|
|
45
|
+
ensureExists(): Promise<void>;
|
|
46
|
+
get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
|
|
47
|
+
protected _indexes: Map<string, TIndex>;
|
|
48
|
+
protected _vectorFilters: Map<string, string>;
|
|
49
|
+
get indexes(): Map<string, TIndex>;
|
|
50
|
+
protected _addIndexField(type: TPlainIndex['type'], name: string, field: string, weight?: number): void;
|
|
51
|
+
protected _setSearchIndex(type: TSearchIndex['type'], name: string | undefined, definition: TMongoSearchIndexDefinition): void;
|
|
52
|
+
protected _addFieldToSearchIndex(type: TSearchIndex['type'], _name: string | undefined, fieldName: string, analyzer?: string): void;
|
|
53
|
+
protected _flatMap?: Map<string, TAtscriptAnnotatedType>;
|
|
54
|
+
protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string): void;
|
|
55
|
+
protected _prepareIndexesForCollection(): void;
|
|
56
|
+
protected _finalizeIndexesForCollection(): void;
|
|
57
|
+
protected _prepareIndexesForField(fieldName: string, metadata: TMetadataMap<AtscriptMetadata>): void;
|
|
58
|
+
protected _flatten(): void;
|
|
59
|
+
get flatMap(): Map<string, TAtscriptAnnotatedType> | undefined;
|
|
60
|
+
syncIndexes(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
type TVectorSimilarity = 'cosine' | 'euclidean' | 'dotProduct';
|
|
63
|
+
type TMongoSearchIndexDefinition = {
|
|
64
|
+
mappings?: {
|
|
65
|
+
dynamic?: boolean;
|
|
66
|
+
fields?: Record<string, {
|
|
67
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
|
|
68
|
+
analyzer?: string;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
fields?: {
|
|
72
|
+
path: string;
|
|
73
|
+
type: 'filter' | 'vector';
|
|
74
|
+
similarity?: TVectorSimilarity;
|
|
75
|
+
numDimensions?: number;
|
|
76
|
+
}[];
|
|
77
|
+
analyzer?: string;
|
|
78
|
+
text?: {
|
|
79
|
+
fuzzy?: {
|
|
80
|
+
maxEdits: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export { AsCollection, AsMongo, MongoPlugin };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { AnnotationSpec, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
|
|
2
|
+
import { isAnnotatedType } from "@atscript/typescript";
|
|
3
|
+
import { MongoClient } from "mongodb";
|
|
4
|
+
|
|
5
|
+
//#region packages/mongo/src/plugin/primitives.ts
|
|
6
|
+
const primitives = { mongo: { extensions: {
|
|
7
|
+
objectId: {
|
|
8
|
+
type: "string",
|
|
9
|
+
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"
|
|
10
|
+
},
|
|
11
|
+
vector: {
|
|
12
|
+
type: {
|
|
13
|
+
kind: "array",
|
|
14
|
+
of: "number"
|
|
15
|
+
},
|
|
16
|
+
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"
|
|
17
|
+
}
|
|
18
|
+
} } };
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region packages/mongo/src/plugin/annotations.ts
|
|
22
|
+
const analyzers = [
|
|
23
|
+
"lucene.standard",
|
|
24
|
+
"lucene.simple",
|
|
25
|
+
"lucene.whitespace",
|
|
26
|
+
"lucene.english",
|
|
27
|
+
"lucene.french",
|
|
28
|
+
"lucene.german",
|
|
29
|
+
"lucene.italian",
|
|
30
|
+
"lucene.portuguese",
|
|
31
|
+
"lucene.spanish",
|
|
32
|
+
"lucene.chinese",
|
|
33
|
+
"lucene.hindi",
|
|
34
|
+
"lucene.bengali",
|
|
35
|
+
"lucene.russian",
|
|
36
|
+
"lucene.arabic"
|
|
37
|
+
];
|
|
38
|
+
const annotations = { mongo: {
|
|
39
|
+
collection: new AnnotationSpec({
|
|
40
|
+
description: "Defines a **MongoDB collection**. This annotation is required to mark an interface as a collection.\n\n- Automatically enforces a **non-optional** `_id` field.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n- Ensures that `_id` is included if not explicitly defined.\n\n**Example:**\n```atscript\n@mongo.collection \"users\"\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
|
|
41
|
+
nodeType: ["interface"],
|
|
42
|
+
argument: {
|
|
43
|
+
name: "name",
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "The **name of the MongoDB collection**."
|
|
46
|
+
},
|
|
47
|
+
validate(token, args, doc) {
|
|
48
|
+
const parent = token.parentNode;
|
|
49
|
+
const struc = parent?.getDefinition();
|
|
50
|
+
const errors = [];
|
|
51
|
+
if (isInterface(parent) && parent.props.has("_id") && isStructure(struc)) {
|
|
52
|
+
const _id = parent.props.get("_id");
|
|
53
|
+
const isOptional = !!_id.token("optional");
|
|
54
|
+
if (isOptional) errors.push({
|
|
55
|
+
message: `[mongo] _id can't be optional in Mongo Collection`,
|
|
56
|
+
severity: 1,
|
|
57
|
+
range: _id.token("identifier").range
|
|
58
|
+
});
|
|
59
|
+
const definition = _id.getDefinition();
|
|
60
|
+
if (!definition) return errors;
|
|
61
|
+
let wrongType = false;
|
|
62
|
+
if (isRef(definition)) {
|
|
63
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
64
|
+
if (isPrimitive(def) && !["string", "number"].includes(def.config.type)) wrongType = true;
|
|
65
|
+
} else wrongType = true;
|
|
66
|
+
if (wrongType) errors.push({
|
|
67
|
+
message: `[mongo] _id must be of type string, number or mongo.objectId`,
|
|
68
|
+
severity: 1,
|
|
69
|
+
range: _id.token("identifier").range
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
},
|
|
74
|
+
modify(token, args, doc) {
|
|
75
|
+
const parent = token.parentNode;
|
|
76
|
+
const struc = parent?.getDefinition();
|
|
77
|
+
if (isInterface(parent) && !parent.props.has("_id") && isStructure(struc)) struc.addVirtualProp({
|
|
78
|
+
name: "_id",
|
|
79
|
+
type: "mongo.objectId",
|
|
80
|
+
documentation: "Mongodb Primary Key ObjectId"
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
index: {
|
|
85
|
+
plain: new AnnotationSpec({
|
|
86
|
+
description: "Defines a **standard MongoDB index** on a field.\n\n- Improves query performance on indexed fields.\n- Can be used for **single-field** or **compound** indexes.\n\n**Example:**\n```atscript\n@mongo.index.plain \"departmentIndex\"\ndepartment: string\n```\n",
|
|
87
|
+
multiple: true,
|
|
88
|
+
nodeType: ["prop"],
|
|
89
|
+
argument: {
|
|
90
|
+
optional: true,
|
|
91
|
+
name: "indexName",
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "The **name of the index** (optional). If omitted, property name is used."
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
unique: new AnnotationSpec({
|
|
97
|
+
description: "Creates a **unique index** on a field to ensure no duplicate values exist.\n\n- Enforces uniqueness at the database level.\n- Automatically prevents duplicate entries.\n- Typically used for **emails, usernames, and IDs**.\n\n**Example:**\n```atscript\n@mongo.index.unique \"uniqueEmailIndex\"\nemail: string.email\n```\n",
|
|
98
|
+
multiple: true,
|
|
99
|
+
nodeType: ["prop"],
|
|
100
|
+
argument: {
|
|
101
|
+
optional: true,
|
|
102
|
+
name: "indexName",
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "The **name of the unique index** (optional). If omitted, property name is used."
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
text: new AnnotationSpec({
|
|
108
|
+
description: "Creates a **legacy MongoDB text index** for full-text search.\n\n**⚠ WARNING:** *Text indexes slow down database operations. Use `@mongo.defineTextSearch` instead for better performance.*\n\n- Allows **basic full-text search** on a field.\n- Does **not support fuzzy matching or ranking**.\n- **Replaced by MongoDB Atlas Search Indexes (`@mongo.searchIndex.text`).**\n\n**Example:**\n```atscript\n@mongo.index.text 5\nbio: string\n```\n",
|
|
109
|
+
nodeType: ["prop"],
|
|
110
|
+
argument: {
|
|
111
|
+
optional: true,
|
|
112
|
+
name: "weight",
|
|
113
|
+
type: "number",
|
|
114
|
+
description: "Field importance in search results (higher = more relevant). Defaults to `1`."
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
search: {
|
|
119
|
+
dynamic: new AnnotationSpec({
|
|
120
|
+
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@mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
|
|
121
|
+
nodeType: ["interface"],
|
|
122
|
+
multiple: false,
|
|
123
|
+
argument: [{
|
|
124
|
+
optional: true,
|
|
125
|
+
name: "analyzer",
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "The **text analyzer** for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, etc.",
|
|
128
|
+
values: analyzers
|
|
129
|
+
}, {
|
|
130
|
+
optional: true,
|
|
131
|
+
name: "fuzzy",
|
|
132
|
+
type: "number",
|
|
133
|
+
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\"`)."
|
|
134
|
+
}]
|
|
135
|
+
}),
|
|
136
|
+
static: new AnnotationSpec({
|
|
137
|
+
description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@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 `@mongo.useTextSearch`** to be included in this search index.\n\n**Example:**\n```atscript\n@mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
|
|
138
|
+
nodeType: ["interface"],
|
|
139
|
+
multiple: true,
|
|
140
|
+
argument: [
|
|
141
|
+
{
|
|
142
|
+
optional: true,
|
|
143
|
+
name: "analyzer",
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
146
|
+
values: analyzers
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
optional: true,
|
|
150
|
+
name: "fuzzy",
|
|
151
|
+
type: "number",
|
|
152
|
+
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\")."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
optional: true,
|
|
156
|
+
name: "indexName",
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "The name of the search index. Fields must reference this name using `@mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}),
|
|
162
|
+
text: new AnnotationSpec({
|
|
163
|
+
description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@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@mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
|
|
164
|
+
nodeType: ["prop"],
|
|
165
|
+
multiple: true,
|
|
166
|
+
argument: [{
|
|
167
|
+
optional: true,
|
|
168
|
+
name: "analyzer",
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "The text analyzer for tokenization. Defaults to `\"lucene.standard\"`.\n\n**Available options:** `\"lucene.standard\"`, `\"lucene.english\"`, `\"lucene.spanish\"`, `\"lucene.german\"`, etc.",
|
|
171
|
+
values: analyzers
|
|
172
|
+
}, {
|
|
173
|
+
optional: true,
|
|
174
|
+
name: "indexName",
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "The **name of the search index** defined in `@mongo.defineTextSearch`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
|
|
177
|
+
}]
|
|
178
|
+
}),
|
|
179
|
+
vector: new AnnotationSpec({
|
|
180
|
+
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@mongo.search.vector 512, \"cosine\"\nembedding: mongo.vector\n```\n",
|
|
181
|
+
nodeType: ["prop"],
|
|
182
|
+
multiple: false,
|
|
183
|
+
argument: [
|
|
184
|
+
{
|
|
185
|
+
optional: false,
|
|
186
|
+
name: "dimensions",
|
|
187
|
+
type: "number",
|
|
188
|
+
description: "The **number of dimensions in the vector** (e.g., 512 for OpenAI embeddings).",
|
|
189
|
+
values: [
|
|
190
|
+
"512",
|
|
191
|
+
"768",
|
|
192
|
+
"1024",
|
|
193
|
+
"1536",
|
|
194
|
+
"3072",
|
|
195
|
+
"4096"
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
optional: true,
|
|
200
|
+
name: "similarity",
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "The **similarity metric** used for vector search. Defaults to `\"cosine\"`.\n\n**Available options:** `\"cosine\"`, `\"euclidean\"`, `\"dotProduct\"`.",
|
|
203
|
+
values: [
|
|
204
|
+
"cosine",
|
|
205
|
+
"euclidean",
|
|
206
|
+
"dotProduct"
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
optional: true,
|
|
211
|
+
name: "indexName",
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "The **name of the vector search index** (optional, defaults to property name)."
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}),
|
|
217
|
+
filter: new AnnotationSpec({
|
|
218
|
+
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 `@mongo.search.vector`.\n\n**Example:**\n```atscript\n@mongo.search.vector 512, \"cosine\"\nembedding: number[]\n\n@mongo.search.filter \"embedding\"\ncategory: string\n```\n",
|
|
219
|
+
nodeType: ["prop"],
|
|
220
|
+
multiple: true,
|
|
221
|
+
argument: [{
|
|
222
|
+
optional: false,
|
|
223
|
+
name: "indexName",
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "The **name of the vector search index** this field should be used as a filter for."
|
|
226
|
+
}]
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
} };
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region packages/mongo/src/plugin/index.ts
|
|
233
|
+
const MongoPlugin = () => {
|
|
234
|
+
return {
|
|
235
|
+
name: "mongo",
|
|
236
|
+
config() {
|
|
237
|
+
return {
|
|
238
|
+
primitives,
|
|
239
|
+
annotations
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region packages/mongo/src/lib/logger.ts
|
|
247
|
+
const NoopLogger = {
|
|
248
|
+
error: () => {},
|
|
249
|
+
warn: () => {},
|
|
250
|
+
log: () => {},
|
|
251
|
+
info: () => {},
|
|
252
|
+
debug: () => {}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region packages/mongo/src/lib/as-collection.ts
|
|
257
|
+
function _define_property$1(obj, key, value) {
|
|
258
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
259
|
+
value,
|
|
260
|
+
enumerable: true,
|
|
261
|
+
configurable: true,
|
|
262
|
+
writable: true
|
|
263
|
+
});
|
|
264
|
+
else obj[key] = value;
|
|
265
|
+
return obj;
|
|
266
|
+
}
|
|
267
|
+
const INDEX_PREFIX = "anscript__";
|
|
268
|
+
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
269
|
+
function indexKey(type, name) {
|
|
270
|
+
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
271
|
+
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
272
|
+
}
|
|
273
|
+
var AsCollection = class {
|
|
274
|
+
async exists() {
|
|
275
|
+
return this.asMongo.collectionExists(this.name);
|
|
276
|
+
}
|
|
277
|
+
async ensureExists() {
|
|
278
|
+
const exists = await this.exists();
|
|
279
|
+
if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
|
|
280
|
+
}
|
|
281
|
+
get type() {
|
|
282
|
+
return this._type;
|
|
283
|
+
}
|
|
284
|
+
get indexes() {
|
|
285
|
+
this._flatten();
|
|
286
|
+
return this._indexes;
|
|
287
|
+
}
|
|
288
|
+
_addIndexField(type, name, field, weight) {
|
|
289
|
+
const key = indexKey(type, name);
|
|
290
|
+
let index = this._indexes.get(key);
|
|
291
|
+
const value = type === "text" ? "text" : 1;
|
|
292
|
+
if (index) index.fields[field] = value;
|
|
293
|
+
else {
|
|
294
|
+
const weights = {};
|
|
295
|
+
index = {
|
|
296
|
+
type,
|
|
297
|
+
fields: { [field]: value },
|
|
298
|
+
weights
|
|
299
|
+
};
|
|
300
|
+
this._indexes.set(key, index);
|
|
301
|
+
}
|
|
302
|
+
if (weight) index.weights[field] = weight;
|
|
303
|
+
}
|
|
304
|
+
_setSearchIndex(type, name, definition) {
|
|
305
|
+
this._indexes.set(indexKey(type, name || DEFAULT_INDEX_NAME), {
|
|
306
|
+
type,
|
|
307
|
+
definition
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
_addFieldToSearchIndex(type, _name, fieldName, analyzer) {
|
|
311
|
+
const name = _name || DEFAULT_INDEX_NAME;
|
|
312
|
+
let index = this._indexes.get(indexKey(type, name));
|
|
313
|
+
if (!index && type === "search_text") {
|
|
314
|
+
this._setSearchIndex(type, name, {
|
|
315
|
+
mappings: { fields: {} },
|
|
316
|
+
text: { fuzzy: { maxEdits: 0 } }
|
|
317
|
+
});
|
|
318
|
+
index = this._indexes.get(indexKey(type, name));
|
|
319
|
+
}
|
|
320
|
+
if (index) {
|
|
321
|
+
index.definition.mappings.fields[fieldName] = { type: "string" };
|
|
322
|
+
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
_flattenType(type, prefix) {
|
|
326
|
+
switch (type.type.kind) {
|
|
327
|
+
case "object":
|
|
328
|
+
const items = Array.from(type.type.props.entries());
|
|
329
|
+
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
|
|
330
|
+
break;
|
|
331
|
+
case "array":
|
|
332
|
+
this._flattenType(type.type.of, prefix);
|
|
333
|
+
break;
|
|
334
|
+
case "intersection":
|
|
335
|
+
case "tuple":
|
|
336
|
+
case "union": for (const item of type.type.items) this._flattenType(item, prefix);
|
|
337
|
+
default:
|
|
338
|
+
this._flatMap?.set(prefix || "", type);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
if (prefix) this._prepareIndexesForField(prefix, type.metadata);
|
|
342
|
+
}
|
|
343
|
+
_prepareIndexesForCollection() {
|
|
344
|
+
const typeMeta = this.type.metadata;
|
|
345
|
+
const dynamicText = typeMeta.get("mongo.search.dynamic");
|
|
346
|
+
if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
|
|
347
|
+
mappings: { dynamic: true },
|
|
348
|
+
analyzer: dynamicText.analyzer,
|
|
349
|
+
text: { fuzzy: { maxEdits: dynamicText.fuzzy || 0 } }
|
|
350
|
+
});
|
|
351
|
+
for (const textSearch of typeMeta.get("mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
|
|
352
|
+
mappings: { fields: {} },
|
|
353
|
+
analyzer: textSearch.analyzer,
|
|
354
|
+
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
_finalizeIndexesForCollection() {
|
|
358
|
+
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
359
|
+
const index = this._indexes.get(key);
|
|
360
|
+
if (index && index.type === "vector") index.definition.fields?.push({
|
|
361
|
+
type: "filter",
|
|
362
|
+
path: value
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
_prepareIndexesForField(fieldName, metadata) {
|
|
367
|
+
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
368
|
+
for (const index of metadata.get("mongo.index.unique") || []) this._addIndexField("unique", index === true ? fieldName : index, fieldName);
|
|
369
|
+
const textWeight = metadata.get("mongo.index.text");
|
|
370
|
+
if (textWeight) this._addIndexField("text", "", fieldName, textWeight === true ? 1 : textWeight);
|
|
371
|
+
for (const index of metadata.get("mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, fieldName, index.analyzer);
|
|
372
|
+
const vectorIndex = metadata.get("mongo.search.vector");
|
|
373
|
+
if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || fieldName, { fields: [{
|
|
374
|
+
type: "vector",
|
|
375
|
+
path: fieldName,
|
|
376
|
+
similarity: vectorIndex.similarity || "dotProduct",
|
|
377
|
+
numDimensions: vectorIndex.dimensions
|
|
378
|
+
}] });
|
|
379
|
+
for (const index of metadata.get("mongo.search.filter") || []) this._vectorFilters.set(indexKey("vector", index.indexName), fieldName);
|
|
380
|
+
}
|
|
381
|
+
_flatten() {
|
|
382
|
+
if (!this._flatMap) {
|
|
383
|
+
this._flatMap = new Map();
|
|
384
|
+
this._prepareIndexesForCollection();
|
|
385
|
+
this._flattenType(this.type);
|
|
386
|
+
this._finalizeIndexesForCollection();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
get flatMap() {
|
|
390
|
+
this._flatten();
|
|
391
|
+
return this._flatMap;
|
|
392
|
+
}
|
|
393
|
+
async syncIndexes() {
|
|
394
|
+
await this.ensureExists();
|
|
395
|
+
const existingIndexes = await this.collection.listIndexes().toArray();
|
|
396
|
+
const indexesToCreate = new Map(this.indexes);
|
|
397
|
+
for (const remote of existingIndexes) {
|
|
398
|
+
if (!remote.name.startsWith(INDEX_PREFIX)) continue;
|
|
399
|
+
if (indexesToCreate.has(remote.name)) {
|
|
400
|
+
const local = indexesToCreate.get(remote.name);
|
|
401
|
+
switch (local.type) {
|
|
402
|
+
case "plain":
|
|
403
|
+
case "unique":
|
|
404
|
+
case "text":
|
|
405
|
+
if ((local.type === "text" || objMatch(local.fields, remote.key)) && objMatch(local.weights || {}, remote.weights || {})) indexesToCreate.delete(remote.name);
|
|
406
|
+
else {
|
|
407
|
+
this.logger.debug(`dropping index "${remote.name}"`);
|
|
408
|
+
await this.collection.dropIndex(remote.name);
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
default:
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
this.logger.debug(`dropping index "${remote.name}"`);
|
|
415
|
+
await this.collection.dropIndex(remote.name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const toUpdate = new Set();
|
|
419
|
+
const existingSearchIndexes = await this.collection.listSearchIndexes().toArray();
|
|
420
|
+
for (const remote of existingSearchIndexes) {
|
|
421
|
+
if (!remote.name.startsWith(INDEX_PREFIX)) continue;
|
|
422
|
+
if (indexesToCreate.has(remote.name)) {
|
|
423
|
+
const local = indexesToCreate.get(remote.name);
|
|
424
|
+
const right = remote.latestDefinition;
|
|
425
|
+
switch (local.type) {
|
|
426
|
+
case "dynamic_text":
|
|
427
|
+
case "search_text":
|
|
428
|
+
let left = local.definition;
|
|
429
|
+
if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
|
|
430
|
+
else toUpdate.add(remote.name);
|
|
431
|
+
break;
|
|
432
|
+
case "vector":
|
|
433
|
+
if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
|
|
434
|
+
else toUpdate.add(remote.name);
|
|
435
|
+
break;
|
|
436
|
+
default:
|
|
437
|
+
}
|
|
438
|
+
} else if (remote.status !== "DELETING") {
|
|
439
|
+
this.logger.debug(`dropping search index "${remote.name}"`);
|
|
440
|
+
await this.collection.dropSearchIndex(remote.name);
|
|
441
|
+
} else this.logger.debug(`search index "${remote.name}" is in deleting status`);
|
|
442
|
+
}
|
|
443
|
+
for (const [key, value] of Array.from(indexesToCreate.entries())) switch (value.type) {
|
|
444
|
+
case "plain":
|
|
445
|
+
this.logger.debug(`creating index "${key}"`);
|
|
446
|
+
await this.collection.createIndex(value.fields, { name: key });
|
|
447
|
+
break;
|
|
448
|
+
case "unique":
|
|
449
|
+
this.logger.debug(`creating index "${key}"`);
|
|
450
|
+
await this.collection.createIndex(value.fields, {
|
|
451
|
+
name: key,
|
|
452
|
+
unique: true
|
|
453
|
+
});
|
|
454
|
+
break;
|
|
455
|
+
case "text":
|
|
456
|
+
this.logger.debug(`creating index "${key}"`);
|
|
457
|
+
await this.collection.createIndex(value.fields, {
|
|
458
|
+
weights: value.weights,
|
|
459
|
+
name: key
|
|
460
|
+
});
|
|
461
|
+
break;
|
|
462
|
+
case "dynamic_text":
|
|
463
|
+
case "search_text":
|
|
464
|
+
case "vector":
|
|
465
|
+
if (toUpdate.has(key)) {
|
|
466
|
+
this.logger.debug(`updating search index "${key}"`);
|
|
467
|
+
await this.collection.updateSearchIndex(key, value.definition);
|
|
468
|
+
} else {
|
|
469
|
+
this.logger.debug(`creating search index "${key}"`);
|
|
470
|
+
await this.collection.createSearchIndex({
|
|
471
|
+
name: key,
|
|
472
|
+
type: value.type === "vector" ? "vectorSearch" : "search",
|
|
473
|
+
definition: value.definition
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
default:
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
constructor(asMongo, _type, logger = NoopLogger) {
|
|
481
|
+
_define_property$1(this, "asMongo", void 0);
|
|
482
|
+
_define_property$1(this, "_type", void 0);
|
|
483
|
+
_define_property$1(this, "logger", void 0);
|
|
484
|
+
_define_property$1(this, "name", void 0);
|
|
485
|
+
_define_property$1(this, "collection", void 0);
|
|
486
|
+
_define_property$1(this, "_indexes", void 0);
|
|
487
|
+
_define_property$1(this, "_vectorFilters", void 0);
|
|
488
|
+
_define_property$1(this, "_flatMap", void 0);
|
|
489
|
+
this.asMongo = asMongo;
|
|
490
|
+
this._type = _type;
|
|
491
|
+
this.logger = logger;
|
|
492
|
+
this._indexes = new Map();
|
|
493
|
+
this._vectorFilters = new Map();
|
|
494
|
+
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
495
|
+
const name = _type.metadata.get("mongo.collection");
|
|
496
|
+
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
|
497
|
+
if (_type.type.kind !== "object") throw new Error("Mongo collection must be an object type");
|
|
498
|
+
this.name = name;
|
|
499
|
+
this.collection = asMongo.db.collection(name);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* Vector Index fields matching
|
|
504
|
+
*/ function vectorFieldsMatch(left, right) {
|
|
505
|
+
const leftMap = new Map();
|
|
506
|
+
left.forEach((f) => leftMap.set(f.path, f));
|
|
507
|
+
const rightMap = new Map();
|
|
508
|
+
(right || []).forEach((f) => rightMap.set(f.path, f));
|
|
509
|
+
if (leftMap.size === rightMap.size) {
|
|
510
|
+
let match = true;
|
|
511
|
+
for (const [key, left$1] of leftMap.entries()) {
|
|
512
|
+
const right$1 = rightMap.get(key);
|
|
513
|
+
if (!right$1) {
|
|
514
|
+
match = false;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
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;
|
|
518
|
+
match = false;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
return match;
|
|
522
|
+
} else return false;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Search Index fields matching
|
|
526
|
+
*/ function fieldsMatch(left, right) {
|
|
527
|
+
if (!left || !right) return left === right;
|
|
528
|
+
const leftKeys = Object.keys(left);
|
|
529
|
+
const rightKeys = Object.keys(right);
|
|
530
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
531
|
+
return leftKeys.every((key) => {
|
|
532
|
+
if (!(key in right)) return false;
|
|
533
|
+
const leftField = left[key];
|
|
534
|
+
const rightField = right[key];
|
|
535
|
+
return leftField.type === rightField.type && leftField.analyzer === rightField.analyzer;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Shallow object matching
|
|
540
|
+
*/ function objMatch(o1, o2) {
|
|
541
|
+
const keys1 = Object.keys(o1);
|
|
542
|
+
const keys2 = Object.keys(o2);
|
|
543
|
+
if (keys1.length !== keys2.length) return false;
|
|
544
|
+
return keys1.every((key) => o1[key] === o2[key]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region packages/mongo/src/lib/as-mongo.ts
|
|
549
|
+
function _define_property(obj, key, value) {
|
|
550
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
551
|
+
value,
|
|
552
|
+
enumerable: true,
|
|
553
|
+
configurable: true,
|
|
554
|
+
writable: true
|
|
555
|
+
});
|
|
556
|
+
else obj[key] = value;
|
|
557
|
+
return obj;
|
|
558
|
+
}
|
|
559
|
+
var AsMongo = class {
|
|
560
|
+
get db() {
|
|
561
|
+
return this.client.db();
|
|
562
|
+
}
|
|
563
|
+
getCollectionsList() {
|
|
564
|
+
if (!this.collectionsList) this.collectionsList = this.db.listCollections().toArray().then((c) => new Set(c.map((c$1) => c$1.name)));
|
|
565
|
+
return this.collectionsList;
|
|
566
|
+
}
|
|
567
|
+
async collectionExists(name) {
|
|
568
|
+
const list = await this.getCollectionsList();
|
|
569
|
+
return list.has(name);
|
|
570
|
+
}
|
|
571
|
+
getCollection(type, logger) {
|
|
572
|
+
return new AsCollection(this, type, logger || this.logger);
|
|
573
|
+
}
|
|
574
|
+
constructor(client, logger = NoopLogger) {
|
|
575
|
+
_define_property(this, "logger", void 0);
|
|
576
|
+
_define_property(this, "client", void 0);
|
|
577
|
+
_define_property(this, "collectionsList", void 0);
|
|
578
|
+
this.logger = logger;
|
|
579
|
+
if (typeof client === "string") this.client = new MongoClient(client);
|
|
580
|
+
else this.client = client;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
export { AsCollection, AsMongo, MongoPlugin };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atscript/mongo",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Mongodb plugin for atscript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"cli.cjs"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"atscript",
|
|
22
|
+
"plugin",
|
|
23
|
+
"mongodb"
|
|
24
|
+
],
|
|
25
|
+
"author": "Artem Maltsev",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/moostjs/atscript.git",
|
|
29
|
+
"directory": "packages/mongo"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/moostjs/atscript/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/moostjs/atscript/tree/main/packages/mongo#readme",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"mongodb": "^6.13.0",
|
|
38
|
+
"@atscript/core": "^0.0.1",
|
|
39
|
+
"@atscript/typescript": "^0.0.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"pub": "pnpm publish --access public",
|
|
46
|
+
"test": "vitest"
|
|
47
|
+
}
|
|
48
|
+
}
|