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