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