@atscript/db-mongo 0.1.39 → 0.1.41
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/README.md +11 -11
- package/dist/agg.cjs +26 -65
- package/dist/{agg.d.ts → agg.d.cts} +5 -12
- package/dist/agg.d.mts +18 -0
- package/dist/agg.mjs +19 -35
- package/dist/index.cjs +374 -313
- package/dist/index.d.cts +344 -0
- package/dist/index.d.mts +344 -0
- package/dist/index.mjs +364 -301
- package/dist/mongo-filter-B5ZINZPF.cjs +40 -0
- package/dist/{mongo-filter-CL69Yhcm.mjs → mongo-filter-CcQO-sEh.mjs} +9 -4
- package/dist/plugin.cjs +37 -47
- package/dist/plugin.d.cts +6 -0
- package/dist/plugin.d.mts +6 -0
- package/dist/plugin.mjs +22 -11
- package/package.json +25 -44
- package/scripts/setup-skills.js +53 -43
- package/skills/atscript-db-mongo/SKILL.md +11 -11
- package/skills/atscript-db-mongo/collections.md +29 -28
- package/skills/atscript-db-mongo/core.md +6 -5
- package/skills/atscript-db-mongo/patches.md +17 -16
- package/LICENSE +0 -21
- package/dist/agg-CUX5Jb_A.mjs +0 -75
- package/dist/agg-FBVtOv9k.cjs +0 -77
- package/dist/index.d.ts +0 -336
- package/dist/mongo-filter-C8w5by9H.cjs +0 -65
- package/dist/plugin.d.ts +0 -5
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
|
-
import { buildMongoFilter } from "./mongo-filter-
|
|
2
|
-
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getKeyProps } from "@atscript/db";
|
|
1
|
+
import { t as buildMongoFilter } from "./mongo-filter-CcQO-sEh.mjs";
|
|
2
|
+
import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps } from "@atscript/db";
|
|
3
3
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
//#region src/lib/collection-patcher.ts
|
|
5
|
+
/**
|
|
6
|
+
* CollectionPatcher is a small helper that converts a *patch payload* produced
|
|
7
|
+
* by Atscript into a shape that the official MongoDB driver understands – a
|
|
8
|
+
* triple of `(filter, update, options)` to be fed to `collection.updateOne()`.
|
|
9
|
+
*
|
|
10
|
+
* Supported high‑level operations for *top‑level arrays* (see the attached
|
|
11
|
+
* spreadsheet in the chat):
|
|
12
|
+
*
|
|
13
|
+
* | Payload field | MongoDB operator | Purpose |
|
|
14
|
+
* |-------------- |-------------------------|----------------------------------------|
|
|
15
|
+
* | `$replace` | full `$set` | Replace the whole array. |
|
|
16
|
+
* | `$insert` | `$push` | Append new items (duplicates allowed). |
|
|
17
|
+
* | `$upsert` | custom | Insert or update by *key* (see TODO). |
|
|
18
|
+
* | `$update` | `$set` + `arrayFilters` | Update array elements matched by *key* |
|
|
19
|
+
* | `$remove` | `$pullAll` / `$pull` | Remove by value or by *key*. |
|
|
20
|
+
*
|
|
21
|
+
* The class walks through the incoming payload, detects which of the above
|
|
22
|
+
* operations applies to each top‑level array and builds the corresponding
|
|
23
|
+
* MongoDB update document. Primitive fields are flattened into a regular
|
|
24
|
+
* `$set` map.
|
|
25
|
+
*/
|
|
16
26
|
var CollectionPatcher = class {
|
|
27
|
+
constructor(collection, payload, ops) {
|
|
28
|
+
this.collection = collection;
|
|
29
|
+
this.payload = payload;
|
|
30
|
+
this.ops = ops;
|
|
31
|
+
}
|
|
32
|
+
static getKeyProps = getKeyProps;
|
|
33
|
+
/**
|
|
34
|
+
* Internal accumulator: filter passed to `updateOne()`.
|
|
35
|
+
* Filled only with the `_id` field right now.
|
|
36
|
+
*/
|
|
37
|
+
filterObj = {};
|
|
38
|
+
/** MongoDB *update* document being built. */
|
|
39
|
+
updatePipeline = [];
|
|
40
|
+
/** Current `$set` stage being populated. */
|
|
41
|
+
currentSetStage = null;
|
|
42
|
+
/** Additional *options* (mainly `arrayFilters`). */
|
|
43
|
+
optionsObj = {};
|
|
17
44
|
/**
|
|
18
45
|
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
19
46
|
*
|
|
20
47
|
* @returns Helper object exposing both individual parts and
|
|
21
48
|
* a `.toArgs()` convenience callback.
|
|
22
|
-
*/
|
|
49
|
+
*/
|
|
50
|
+
preparePatch() {
|
|
23
51
|
this.filterObj = { _id: this.collection.prepareId(this.payload._id) };
|
|
24
52
|
this.flattenPayload(this.payload);
|
|
53
|
+
if (this.ops?.inc) for (const key in this.ops.inc) this._set(key, this._fieldOpExpr(key, "inc", this.ops.inc[key]));
|
|
54
|
+
if (this.ops?.mul) for (const key in this.ops.mul) this._set(key, this._fieldOpExpr(key, "mul", this.ops.mul[key]));
|
|
25
55
|
const updateFilter = this.updatePipeline;
|
|
26
56
|
return {
|
|
27
57
|
toArgs: () => [
|
|
@@ -34,13 +64,18 @@ var CollectionPatcher = class {
|
|
|
34
64
|
updateOptions: this.optionsObj
|
|
35
65
|
};
|
|
36
66
|
}
|
|
67
|
+
/** Builds a MongoDB aggregation expression for an $inc or $mul field op. */
|
|
68
|
+
_fieldOpExpr(key, op, value) {
|
|
69
|
+
return op === "inc" ? { $add: [`$${key}`, value] } : { $multiply: [`$${key}`, value] };
|
|
70
|
+
}
|
|
37
71
|
/**
|
|
38
72
|
* Helper – lazily create `$set` section and assign *key* → *value*.
|
|
39
73
|
*
|
|
40
74
|
* @param key Fully‑qualified dotted path
|
|
41
75
|
* @param val Value to be written
|
|
42
76
|
* @private
|
|
43
|
-
*/
|
|
77
|
+
*/
|
|
78
|
+
_set(key, val) {
|
|
44
79
|
if (this.currentSetStage && !(key in this.currentSetStage.$set)) {
|
|
45
80
|
this.currentSetStage.$set[key] = val;
|
|
46
81
|
return;
|
|
@@ -55,15 +90,20 @@ var CollectionPatcher = class {
|
|
|
55
90
|
* @param payload Current payload chunk
|
|
56
91
|
* @param prefix Dotted path accumulated so far
|
|
57
92
|
* @private
|
|
58
|
-
*/
|
|
93
|
+
*/
|
|
94
|
+
flattenPayload(payload, prefix = "") {
|
|
59
95
|
const evalKey = (k) => prefix ? `${prefix}.${k}` : k;
|
|
60
96
|
for (const [_key, value] of Object.entries(payload)) {
|
|
61
97
|
const key = evalKey(_key);
|
|
62
98
|
const flatType = this.collection.flatMap.get(key);
|
|
63
99
|
const topLevelArray = flatType?.metadata?.get("db.__topLevelArray");
|
|
64
100
|
if (typeof value === "object" && !Array.isArray(value) && topLevelArray && !flatType?.metadata?.has("db.json")) this.parseArrayPatch(key, value, flatType);
|
|
65
|
-
else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
66
|
-
else if (key !== "_id")
|
|
101
|
+
else if (typeof value === "object" && flatType?.metadata?.get("db.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
102
|
+
else if (key !== "_id") {
|
|
103
|
+
const fieldOp = getDbFieldOp(value);
|
|
104
|
+
if (fieldOp) this._set(key, this._fieldOpExpr(key, fieldOp.op, fieldOp.value));
|
|
105
|
+
else this._set(key, value);
|
|
106
|
+
}
|
|
67
107
|
}
|
|
68
108
|
return this.updatePipeline;
|
|
69
109
|
}
|
|
@@ -74,13 +114,14 @@ else if (key !== "_id") this._set(key, value);
|
|
|
74
114
|
* @param key Dotted path to the array field
|
|
75
115
|
* @param value Payload slice for that field
|
|
76
116
|
* @private
|
|
77
|
-
*/
|
|
117
|
+
*/
|
|
118
|
+
parseArrayPatch(key, value, flatType) {
|
|
78
119
|
const toRemove = value.$remove;
|
|
79
120
|
const toReplace = value.$replace;
|
|
80
121
|
const toInsert = value.$insert;
|
|
81
122
|
const toUpsert = value.$upsert;
|
|
82
123
|
const toUpdate = value.$update;
|
|
83
|
-
const keyProps = flatType.type.kind === "array" ? getKeyProps(flatType) : new Set();
|
|
124
|
+
const keyProps = flatType.type.kind === "array" ? getKeyProps(flatType) : /* @__PURE__ */ new Set();
|
|
84
125
|
const keys = keyProps.size > 0 ? [...keyProps] : [];
|
|
85
126
|
this._remove(key, toRemove, keys, flatType);
|
|
86
127
|
this._replace(key, toReplace);
|
|
@@ -98,7 +139,8 @@ else if (key !== "_id") this._set(key, value);
|
|
|
98
139
|
* @param keys Ordered list of key property names
|
|
99
140
|
* @param left Base token for *left* expression (e.g. `"$$el"`)
|
|
100
141
|
* @param right Base token for *right* expression (e.g. `"$$this"`)
|
|
101
|
-
*/
|
|
142
|
+
*/
|
|
143
|
+
_keysEqual(keys, left, right) {
|
|
102
144
|
const eqs = keys.map((k) => ({ $eq: [`${left}.${k}`, `${right}.${k}`] }));
|
|
103
145
|
return eqs.length === 1 ? eqs[0] : { $and: eqs };
|
|
104
146
|
}
|
|
@@ -108,24 +150,26 @@ else if (key !== "_id") this._set(key, value);
|
|
|
108
150
|
* @param key Dotted path to the array
|
|
109
151
|
* @param input New array value (may be `undefined`)
|
|
110
152
|
* @private
|
|
111
|
-
*/
|
|
153
|
+
*/
|
|
154
|
+
_replace(key, input) {
|
|
112
155
|
if (input) this._set(key, input);
|
|
113
156
|
}
|
|
114
157
|
/**
|
|
115
158
|
* `$insert`
|
|
116
159
|
* - plain append → $concatArrays
|
|
117
160
|
* - unique / keyed → delegate to _upsert (insert-or-update)
|
|
118
|
-
*/
|
|
161
|
+
*/
|
|
162
|
+
_insert(key, input, keys, flatType) {
|
|
119
163
|
if (!input?.length) return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
164
|
+
if (flatType.metadata?.has("expect.array.uniqueItems") || keys.length > 0) this._upsert(key, input, keys, flatType);
|
|
165
|
+
else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
123
166
|
}
|
|
124
167
|
/**
|
|
125
168
|
* `$upsert`
|
|
126
169
|
* - keyed → remove existing matching by key(s) then append candidate
|
|
127
170
|
* - unique → $setUnion (deep equality)
|
|
128
|
-
*/
|
|
171
|
+
*/
|
|
172
|
+
_upsert(key, input, keys, flatType) {
|
|
129
173
|
if (!input?.length) return;
|
|
130
174
|
if (keys.length > 0) {
|
|
131
175
|
const mergeStrategy = flatType.metadata?.get("db.patch.strategy") === "merge";
|
|
@@ -166,7 +210,8 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
166
210
|
* `$update`
|
|
167
211
|
* - keyed → map array and merge / replace matching element(s)
|
|
168
212
|
* - non-keyed → behave like `$addToSet` (insert only when not present)
|
|
169
|
-
*/
|
|
213
|
+
*/
|
|
214
|
+
_update(key, input, keys, flatType) {
|
|
170
215
|
if (!input?.length) return;
|
|
171
216
|
if (keys.length > 0) {
|
|
172
217
|
const mergeStrategy = flatType.metadata?.get("db.patch.strategy") === "merge";
|
|
@@ -189,7 +234,8 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
189
234
|
* `$remove`
|
|
190
235
|
* - keyed → filter out any element whose key set matches a payload item
|
|
191
236
|
* - non-keyed → deep equality remove (`$setDifference`)
|
|
192
|
-
*/
|
|
237
|
+
*/
|
|
238
|
+
_remove(key, input, keys, _flatType) {
|
|
193
239
|
if (!input?.length) return;
|
|
194
240
|
if (keys.length > 0) this._set(key, { $let: {
|
|
195
241
|
vars: { rem: input },
|
|
@@ -203,46 +249,29 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
|
203
249
|
} } } }
|
|
204
250
|
} }
|
|
205
251
|
} });
|
|
206
|
-
else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
207
|
-
}
|
|
208
|
-
constructor(collection, payload) {
|
|
209
|
-
_define_property$1(this, "collection", void 0);
|
|
210
|
-
_define_property$1(this, "payload", void 0);
|
|
211
|
-
/**
|
|
212
|
-
* Internal accumulator: filter passed to `updateOne()`.
|
|
213
|
-
* Filled only with the `_id` field right now.
|
|
214
|
-
*/ _define_property$1(this, "filterObj", void 0);
|
|
215
|
-
/** MongoDB *update* document being built. */ _define_property$1(this, "updatePipeline", void 0);
|
|
216
|
-
/** Current `$set` stage being populated. */ _define_property$1(this, "currentSetStage", void 0);
|
|
217
|
-
/** Additional *options* (mainly `arrayFilters`). */ _define_property$1(this, "optionsObj", void 0);
|
|
218
|
-
this.collection = collection;
|
|
219
|
-
this.payload = payload;
|
|
220
|
-
this.filterObj = {};
|
|
221
|
-
this.updatePipeline = [];
|
|
222
|
-
this.currentSetStage = null;
|
|
223
|
-
this.optionsObj = {};
|
|
252
|
+
else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
224
253
|
}
|
|
225
254
|
};
|
|
226
|
-
_define_property$1(CollectionPatcher, "getKeyProps", getKeyProps);
|
|
227
|
-
|
|
228
255
|
//#endregion
|
|
229
|
-
//#region
|
|
256
|
+
//#region src/lib/mongo-types.ts
|
|
230
257
|
const INDEX_PREFIX = "atscript__";
|
|
231
258
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
232
259
|
const JOINED_PREFIX = "__joined_";
|
|
233
260
|
function mongoIndexKey(type, name) {
|
|
234
|
-
|
|
235
|
-
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
261
|
+
return `${INDEX_PREFIX}${type}__${name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 117 - type.length - 2)}`;
|
|
236
262
|
}
|
|
237
|
-
|
|
238
263
|
//#endregion
|
|
239
|
-
//#region
|
|
264
|
+
//#region src/lib/mongo-relations.ts
|
|
240
265
|
function buildPKKey(primaryKeys, doc) {
|
|
241
|
-
if (primaryKeys.length === 1)
|
|
266
|
+
if (primaryKeys.length === 1) {
|
|
267
|
+
const val = doc[primaryKeys[0]];
|
|
268
|
+
return val == null ? "" : `${val}`;
|
|
269
|
+
}
|
|
242
270
|
let key = "";
|
|
243
271
|
for (let i = 0; i < primaryKeys.length; i++) {
|
|
244
272
|
if (i > 0) key += "\0";
|
|
245
|
-
|
|
273
|
+
const val = doc[primaryKeys[i]];
|
|
274
|
+
key += val == null ? "" : `${val}`;
|
|
246
275
|
}
|
|
247
276
|
return key;
|
|
248
277
|
}
|
|
@@ -269,23 +298,23 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
269
298
|
if (pkMatchFilter) {
|
|
270
299
|
const pipeline = [{ $match: pkMatchFilter }];
|
|
271
300
|
for (const meta of relMeta) pipeline.push(...meta.stages);
|
|
272
|
-
|
|
273
|
-
mergeRelationResults(rows, results, primaryKeys, relMeta);
|
|
301
|
+
mergeRelationResults(rows, await host.collection.aggregate(pipeline, host._getSessionOpts()).toArray(), primaryKeys, relMeta);
|
|
274
302
|
} else for (const row of rows) for (const meta of relMeta) row[meta.name] = meta.isArray ? [] : null;
|
|
275
303
|
await loadNestedRelations(rows, relMeta, tableResolver);
|
|
276
304
|
}
|
|
277
|
-
/** Builds a $match filter to re-select source rows by PK. */
|
|
305
|
+
/** Builds a $match filter to re-select source rows by PK. */
|
|
306
|
+
function buildPKMatchFilter(rows, primaryKeys) {
|
|
278
307
|
if (primaryKeys.length === 1) {
|
|
279
308
|
const pk = primaryKeys[0];
|
|
280
|
-
const values = new Set();
|
|
309
|
+
const values = /* @__PURE__ */ new Set();
|
|
281
310
|
for (const row of rows) {
|
|
282
311
|
const v = row[pk];
|
|
283
|
-
if (v !== null && v !==
|
|
312
|
+
if (v !== null && v !== void 0) values.add(v);
|
|
284
313
|
}
|
|
285
|
-
if (values.size === 0) return
|
|
314
|
+
if (values.size === 0) return;
|
|
286
315
|
return { [pk]: { $in: [...values] } };
|
|
287
316
|
}
|
|
288
|
-
const seen = new Set();
|
|
317
|
+
const seen = /* @__PURE__ */ new Set();
|
|
289
318
|
const orFilters = [];
|
|
290
319
|
for (const row of rows) {
|
|
291
320
|
const key = buildPKKey(primaryKeys, row);
|
|
@@ -295,7 +324,7 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
295
324
|
let valid = true;
|
|
296
325
|
for (const pk of primaryKeys) {
|
|
297
326
|
const val = row[pk];
|
|
298
|
-
if (val === null || val ===
|
|
327
|
+
if (val === null || val === void 0) {
|
|
299
328
|
valid = false;
|
|
300
329
|
break;
|
|
301
330
|
}
|
|
@@ -303,18 +332,20 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
303
332
|
}
|
|
304
333
|
if (valid) orFilters.push(condition);
|
|
305
334
|
}
|
|
306
|
-
if (orFilters.length === 0) return
|
|
335
|
+
if (orFilters.length === 0) return;
|
|
307
336
|
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
308
337
|
}
|
|
309
|
-
/** Dispatches to the correct $lookup builder based on relation direction. */
|
|
338
|
+
/** Dispatches to the correct $lookup builder based on relation direction. */
|
|
339
|
+
function buildRelationLookup(host, withRel, relation, foreignKeys, tableResolver) {
|
|
310
340
|
switch (relation.direction) {
|
|
311
341
|
case "to": return buildToLookup(withRel, relation, foreignKeys);
|
|
312
342
|
case "from": return buildFromLookup(host, withRel, relation, tableResolver);
|
|
313
343
|
case "via": return buildViaLookup(host, withRel, relation, tableResolver);
|
|
314
|
-
default: return
|
|
344
|
+
default: return;
|
|
315
345
|
}
|
|
316
346
|
}
|
|
317
|
-
/** Builds `let` variable bindings and the corresponding `$expr` match for `$lookup`. */
|
|
347
|
+
/** Builds `let` variable bindings and the corresponding `$expr` match for `$lookup`. */
|
|
348
|
+
function buildLookupJoin(localFields, remoteFields, varPrefix) {
|
|
318
349
|
const letVars = {};
|
|
319
350
|
for (let i = 0; i < localFields.length; i++) letVars[`${varPrefix}${i}`] = `$${localFields[i]}`;
|
|
320
351
|
if (remoteFields.length === 1) return {
|
|
@@ -328,32 +359,33 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
328
359
|
exprMatch: { $and: andClauses }
|
|
329
360
|
};
|
|
330
361
|
}
|
|
331
|
-
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */
|
|
362
|
+
/** $lookup for TO relations (FK is on this table → target). Always single-valued. */
|
|
363
|
+
function buildToLookup(withRel, relation, foreignKeys) {
|
|
332
364
|
const fk = findFKForRelation(relation, foreignKeys);
|
|
333
|
-
if (!fk) return
|
|
365
|
+
if (!fk) return;
|
|
334
366
|
const innerPipeline = buildLookupInnerPipeline(withRel, fk.targetFields);
|
|
335
367
|
const { letVars, exprMatch } = buildLookupJoin(fk.localFields, fk.targetFields, "fk_");
|
|
336
|
-
const stages = [{ $lookup: {
|
|
337
|
-
from: fk.targetTable,
|
|
338
|
-
let: letVars,
|
|
339
|
-
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
340
|
-
as: withRel.name
|
|
341
|
-
} }, { $unwind: {
|
|
342
|
-
path: `$${withRel.name}`,
|
|
343
|
-
preserveNullAndEmptyArrays: true
|
|
344
|
-
} }];
|
|
345
368
|
return {
|
|
346
|
-
stages
|
|
369
|
+
stages: [{ $lookup: {
|
|
370
|
+
from: fk.targetTable,
|
|
371
|
+
let: letVars,
|
|
372
|
+
pipeline: [{ $match: { $expr: exprMatch } }, ...innerPipeline],
|
|
373
|
+
as: withRel.name
|
|
374
|
+
} }, { $unwind: {
|
|
375
|
+
path: `$${withRel.name}`,
|
|
376
|
+
preserveNullAndEmptyArrays: true
|
|
377
|
+
} }],
|
|
347
378
|
isArray: false
|
|
348
379
|
};
|
|
349
380
|
}
|
|
350
|
-
/** $lookup for FROM relations (FK is on target → this table). */
|
|
381
|
+
/** $lookup for FROM relations (FK is on target → this table). */
|
|
382
|
+
function buildFromLookup(host, withRel, relation, tableResolver) {
|
|
351
383
|
const targetType = relation.targetType();
|
|
352
|
-
if (!targetType || !tableResolver) return
|
|
384
|
+
if (!targetType || !tableResolver) return;
|
|
353
385
|
const targetMeta = tableResolver(targetType);
|
|
354
|
-
if (!targetMeta) return
|
|
386
|
+
if (!targetMeta) return;
|
|
355
387
|
const remoteFK = findRemoteFK(targetMeta, host._table.tableName, relation.alias);
|
|
356
|
-
if (!remoteFK) return
|
|
388
|
+
if (!remoteFK) return;
|
|
357
389
|
const targetTableName = resolveRelTargetTableName(relation);
|
|
358
390
|
const innerPipeline = buildLookupInnerPipeline(withRel, remoteFK.fields);
|
|
359
391
|
const { letVars, exprMatch } = buildLookupJoin(remoteFK.targetFields, remoteFK.fields, "pk_");
|
|
@@ -372,46 +404,47 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
372
404
|
isArray: relation.isArray
|
|
373
405
|
};
|
|
374
406
|
}
|
|
375
|
-
/** $lookup for VIA relations (M:N through junction table). Always array. */
|
|
376
|
-
|
|
407
|
+
/** $lookup for VIA relations (M:N through junction table). Always array. */
|
|
408
|
+
function buildViaLookup(host, withRel, relation, tableResolver) {
|
|
409
|
+
if (!relation.viaType || !tableResolver) return;
|
|
377
410
|
const junctionType = relation.viaType();
|
|
378
|
-
if (!junctionType) return
|
|
411
|
+
if (!junctionType) return;
|
|
379
412
|
const junctionMeta = tableResolver(junctionType);
|
|
380
|
-
if (!junctionMeta) return
|
|
413
|
+
if (!junctionMeta) return;
|
|
381
414
|
const junctionTableName = junctionType.metadata?.get("db.table") || junctionType.id || "";
|
|
382
415
|
const targetTableName = resolveRelTargetTableName(relation);
|
|
383
416
|
const fkToThis = findRemoteFK(junctionMeta, host._table.tableName);
|
|
384
|
-
if (!fkToThis) return
|
|
417
|
+
if (!fkToThis) return;
|
|
385
418
|
const fkToTarget = findRemoteFK(junctionMeta, targetTableName);
|
|
386
|
-
if (!fkToTarget) return
|
|
419
|
+
if (!fkToTarget) return;
|
|
387
420
|
const innerPipeline = buildLookupInnerPipeline(withRel, fkToTarget.targetFields);
|
|
388
421
|
const { letVars, exprMatch } = buildLookupJoin(fkToThis.targetFields, fkToThis.fields, "pk_");
|
|
389
|
-
const stages = [{ $lookup: {
|
|
390
|
-
from: junctionTableName,
|
|
391
|
-
let: letVars,
|
|
392
|
-
pipeline: [
|
|
393
|
-
{ $match: { $expr: exprMatch } },
|
|
394
|
-
{ $lookup: {
|
|
395
|
-
from: targetTableName,
|
|
396
|
-
localField: fkToTarget.fields[0],
|
|
397
|
-
foreignField: fkToTarget.targetFields[0],
|
|
398
|
-
pipeline: innerPipeline,
|
|
399
|
-
as: "__target"
|
|
400
|
-
} },
|
|
401
|
-
{ $unwind: {
|
|
402
|
-
path: "$__target",
|
|
403
|
-
preserveNullAndEmptyArrays: false
|
|
404
|
-
} },
|
|
405
|
-
{ $replaceRoot: { newRoot: "$__target" } }
|
|
406
|
-
],
|
|
407
|
-
as: withRel.name
|
|
408
|
-
} }];
|
|
409
422
|
return {
|
|
410
|
-
stages
|
|
423
|
+
stages: [{ $lookup: {
|
|
424
|
+
from: junctionTableName,
|
|
425
|
+
let: letVars,
|
|
426
|
+
pipeline: [
|
|
427
|
+
{ $match: { $expr: exprMatch } },
|
|
428
|
+
{ $lookup: {
|
|
429
|
+
from: targetTableName,
|
|
430
|
+
localField: fkToTarget.fields[0],
|
|
431
|
+
foreignField: fkToTarget.targetFields[0],
|
|
432
|
+
pipeline: innerPipeline,
|
|
433
|
+
as: "__target"
|
|
434
|
+
} },
|
|
435
|
+
{ $unwind: {
|
|
436
|
+
path: "$__target",
|
|
437
|
+
preserveNullAndEmptyArrays: false
|
|
438
|
+
} },
|
|
439
|
+
{ $replaceRoot: { newRoot: "$__target" } }
|
|
440
|
+
],
|
|
441
|
+
as: withRel.name
|
|
442
|
+
} }],
|
|
411
443
|
isArray: true
|
|
412
444
|
};
|
|
413
445
|
}
|
|
414
|
-
/** Builds inner pipeline stages for relation controls ($sort, $limit, $skip, $select, filter). */
|
|
446
|
+
/** Builds inner pipeline stages for relation controls ($sort, $limit, $skip, $select, filter). */
|
|
447
|
+
function buildLookupInnerPipeline(withRel, requiredFields) {
|
|
415
448
|
const pipeline = [];
|
|
416
449
|
const flatRel = withRel;
|
|
417
450
|
const nested = withRel.controls || {};
|
|
@@ -423,7 +456,7 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
423
456
|
if (filter && Object.keys(filter).length > 0) pipeline.push({ $match: buildMongoFilter(filter) });
|
|
424
457
|
if (sort) pipeline.push({ $sort: sort });
|
|
425
458
|
if (skip) pipeline.push({ $skip: skip });
|
|
426
|
-
if (limit !== null && limit !==
|
|
459
|
+
if (limit !== null && limit !== void 0) pipeline.push({ $limit: limit });
|
|
427
460
|
if (select) {
|
|
428
461
|
const projection = {};
|
|
429
462
|
for (const f of select) projection[f] = 1;
|
|
@@ -433,13 +466,14 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
433
466
|
}
|
|
434
467
|
return pipeline;
|
|
435
468
|
}
|
|
436
|
-
/** Extracts nested $with from a WithRelation's controls. */
|
|
469
|
+
/** Extracts nested $with from a WithRelation's controls. */
|
|
470
|
+
function extractNestedWith(withRel) {
|
|
437
471
|
const flatRel = withRel;
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
return nestedWith && nestedWith.length > 0 ? nestedWith : undefined;
|
|
472
|
+
const nestedWith = (withRel.controls || {}).$with || flatRel.$with;
|
|
473
|
+
return nestedWith && nestedWith.length > 0 ? nestedWith : void 0;
|
|
441
474
|
}
|
|
442
|
-
/** Post-processes nested $with by delegating to the target table's own relation loading. */
|
|
475
|
+
/** Post-processes nested $with by delegating to the target table's own relation loading. */
|
|
476
|
+
async function loadNestedRelations(rows, relMeta, tableResolver) {
|
|
443
477
|
if (!tableResolver) return;
|
|
444
478
|
const tasks = [];
|
|
445
479
|
for (const meta of relMeta) {
|
|
@@ -452,26 +486,28 @@ async function loadRelationsImpl(host, rows, withRelations, relations, foreignKe
|
|
|
452
486
|
for (const row of rows) {
|
|
453
487
|
const val = row[meta.name];
|
|
454
488
|
if (meta.isArray && Array.isArray(val)) for (const item of val) subRows.push(item);
|
|
455
|
-
else if (val && typeof val === "object") subRows.push(val);
|
|
489
|
+
else if (val && typeof val === "object") subRows.push(val);
|
|
456
490
|
}
|
|
457
491
|
if (subRows.length === 0) continue;
|
|
458
492
|
tasks.push(targetTable.loadRelations(subRows, meta.nestedWith));
|
|
459
493
|
}
|
|
460
494
|
await Promise.all(tasks);
|
|
461
495
|
}
|
|
462
|
-
/** Merges aggregation results back onto the original rows by PK. */
|
|
463
|
-
|
|
496
|
+
/** Merges aggregation results back onto the original rows by PK. */
|
|
497
|
+
function mergeRelationResults(rows, results, primaryKeys, relMeta) {
|
|
498
|
+
const resultIndex = /* @__PURE__ */ new Map();
|
|
464
499
|
for (const doc of results) resultIndex.set(buildPKKey(primaryKeys, doc), doc);
|
|
465
500
|
for (const row of rows) {
|
|
466
501
|
const enriched = resultIndex.get(buildPKKey(primaryKeys, row));
|
|
467
502
|
for (const meta of relMeta) if (enriched) {
|
|
468
503
|
const value = enriched[meta.name];
|
|
469
504
|
if (!meta.isArray && Array.isArray(value)) row[meta.name] = value[0] ?? null;
|
|
470
|
-
else row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
505
|
+
else row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
471
506
|
} else row[meta.name] = meta.isArray ? [] : null;
|
|
472
507
|
}
|
|
473
508
|
}
|
|
474
|
-
/** Finds FK entry for a TO relation from this table's foreignKeys map. */
|
|
509
|
+
/** Finds FK entry for a TO relation from this table's foreignKeys map. */
|
|
510
|
+
function findFKForRelation(relation, foreignKeys) {
|
|
475
511
|
const targetTableName = resolveRelTargetTableName(relation);
|
|
476
512
|
for (const fk of foreignKeys.values()) if (relation.alias) {
|
|
477
513
|
if (fk.alias === relation.alias) return {
|
|
@@ -484,22 +520,22 @@ else row[meta.name] = value ?? (meta.isArray ? [] : null);
|
|
|
484
520
|
targetFields: fk.targetFields,
|
|
485
521
|
targetTable: fk.targetTable
|
|
486
522
|
};
|
|
487
|
-
return undefined;
|
|
488
523
|
}
|
|
489
|
-
/** Finds a FK on a remote table that points back to the given table name. */
|
|
524
|
+
/** Finds a FK on a remote table that points back to the given table name. */
|
|
525
|
+
function findRemoteFK(target, thisTableName, alias) {
|
|
490
526
|
for (const fk of target.foreignKeys.values()) {
|
|
491
527
|
if (alias && fk.alias === alias && fk.targetTable === thisTableName) return fk;
|
|
492
528
|
if (!alias && fk.targetTable === thisTableName) return fk;
|
|
493
529
|
}
|
|
494
|
-
return undefined;
|
|
495
530
|
}
|
|
496
|
-
/** Resolves the target table/collection name from a relation's target type. */
|
|
531
|
+
/** Resolves the target table/collection name from a relation's target type. */
|
|
532
|
+
function resolveRelTargetTableName(relation) {
|
|
497
533
|
const targetType = relation.targetType();
|
|
498
534
|
return targetType?.metadata?.get("db.table") || targetType?.id || "";
|
|
499
535
|
}
|
|
500
|
-
|
|
501
536
|
//#endregion
|
|
502
|
-
//#region
|
|
537
|
+
//#region src/lib/mongo-search.ts
|
|
538
|
+
/** Returns available search indexes as generic metadata for UI. */
|
|
503
539
|
function getSearchIndexesImpl(host) {
|
|
504
540
|
const result = [];
|
|
505
541
|
for (const [name, index] of host.getMongoSearchIndexes()) result.push({
|
|
@@ -509,40 +545,43 @@ function getSearchIndexesImpl(host) {
|
|
|
509
545
|
});
|
|
510
546
|
return result;
|
|
511
547
|
}
|
|
548
|
+
/** Checks if any vector search index is available. */
|
|
512
549
|
function isVectorSearchableImpl(host) {
|
|
513
550
|
for (const index of host.getMongoSearchIndexes().values()) if (index.type === "vector") return true;
|
|
514
551
|
return false;
|
|
515
552
|
}
|
|
553
|
+
/** Text search via $search aggregation stage. */
|
|
516
554
|
async function searchImpl(host, text, query, indexName) {
|
|
517
555
|
const stage = buildSearchStage(host, text, indexName);
|
|
518
556
|
if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
519
557
|
return runSearchPipeline(host, stage, query, "search");
|
|
520
558
|
}
|
|
559
|
+
/** Text search with faceted count. */
|
|
521
560
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
522
561
|
const stage = buildSearchStage(host, text, indexName);
|
|
523
562
|
if (!stage) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
524
563
|
return runSearchWithCountPipeline(host, stage, query, "searchWithCount");
|
|
525
564
|
}
|
|
565
|
+
/** Vector search via $vectorSearch aggregation stage. */
|
|
526
566
|
async function vectorSearchImpl(host, vector, query, indexName) {
|
|
527
567
|
const controls = query.controls || {};
|
|
528
|
-
|
|
529
|
-
const threshold = resolveThreshold(host, controls, indexName);
|
|
530
|
-
return runSearchPipeline(host, stage, query, "vectorSearch", threshold);
|
|
568
|
+
return runSearchPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearch", resolveThreshold(host, controls, indexName));
|
|
531
569
|
}
|
|
570
|
+
/** Vector search with faceted count. */
|
|
532
571
|
async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
533
572
|
const controls = query.controls || {};
|
|
534
|
-
|
|
535
|
-
const threshold = resolveThreshold(host, controls, indexName);
|
|
536
|
-
return runSearchWithCountPipeline(host, stage, query, "vectorSearchWithCount", threshold);
|
|
573
|
+
return runSearchWithCountPipeline(host, buildVectorSearchStage(host, vector, indexName, controls.$limit), query, "vectorSearchWithCount", resolveThreshold(host, controls, indexName));
|
|
537
574
|
}
|
|
538
|
-
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
575
|
+
/** Resolves the effective threshold: query-time $threshold > schema-level @db.search.vector.threshold. */
|
|
576
|
+
function resolveThreshold(host, controls, indexName) {
|
|
539
577
|
const queryThreshold = controls.$threshold;
|
|
540
|
-
if (queryThreshold !==
|
|
578
|
+
if (queryThreshold !== void 0) return queryThreshold;
|
|
541
579
|
return host.getVectorThreshold(indexName);
|
|
542
580
|
}
|
|
543
|
-
/** Builds a MongoDB $search pipeline stage for text search. */
|
|
581
|
+
/** Builds a MongoDB $search pipeline stage for text search. */
|
|
582
|
+
function buildSearchStage(host, text, indexName) {
|
|
544
583
|
const index = host.getMongoSearchIndex(indexName);
|
|
545
|
-
if (!index) return
|
|
584
|
+
if (!index) return;
|
|
546
585
|
if (index.type === "vector") throw new Error("Vector indexes cannot be used with text search. Use vectorSearch() instead.");
|
|
547
586
|
return { $search: {
|
|
548
587
|
index: index.key,
|
|
@@ -552,7 +591,8 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
552
591
|
}
|
|
553
592
|
} };
|
|
554
593
|
}
|
|
555
|
-
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
594
|
+
/** Builds a $vectorSearch aggregation stage from a pre-computed vector. */
|
|
595
|
+
function buildVectorSearchStage(host, vector, indexName, limit) {
|
|
556
596
|
let index;
|
|
557
597
|
if (indexName) {
|
|
558
598
|
const found = host.getMongoSearchIndex(indexName);
|
|
@@ -579,11 +619,12 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
579
619
|
limit: limit || 20
|
|
580
620
|
} };
|
|
581
621
|
}
|
|
582
|
-
/** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
|
|
622
|
+
/** Runs a search/vector pipeline and returns results. Shared by search + vectorSearch. */
|
|
623
|
+
async function runSearchPipeline(host, stage, query, label, threshold) {
|
|
583
624
|
const filter = buildMongoFilter(query.filter);
|
|
584
625
|
const controls = query.controls || {};
|
|
585
626
|
const pipeline = [stage];
|
|
586
|
-
if (threshold !==
|
|
627
|
+
if (threshold !== void 0) {
|
|
587
628
|
pipeline.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
|
|
588
629
|
pipeline.push({ $match: { _score: { $gte: threshold } } });
|
|
589
630
|
}
|
|
@@ -591,16 +632,17 @@ async function vectorSearchWithCountImpl(host, vector, query, indexName) {
|
|
|
591
632
|
if (controls.$sort) pipeline.push({ $sort: controls.$sort });
|
|
592
633
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
593
634
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
594
|
-
else pipeline.push({ $limit: 1e3 });
|
|
635
|
+
else pipeline.push({ $limit: 1e3 });
|
|
595
636
|
if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
|
|
596
637
|
host._log(`aggregate (${label})`, pipeline);
|
|
597
638
|
return host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
|
|
598
639
|
}
|
|
599
|
-
/** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
|
|
640
|
+
/** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
|
|
641
|
+
async function runSearchWithCountPipeline(host, stage, query, label, threshold) {
|
|
600
642
|
const filter = buildMongoFilter(query.filter);
|
|
601
643
|
const controls = query.controls || {};
|
|
602
644
|
const preStages = [];
|
|
603
|
-
if (threshold !==
|
|
645
|
+
if (threshold !== void 0) {
|
|
604
646
|
preStages.push({ $addFields: { _score: { $meta: "vectorSearchScore" } } });
|
|
605
647
|
preStages.push({ $match: { _score: { $gte: threshold } } });
|
|
606
648
|
}
|
|
@@ -625,9 +667,8 @@ else pipeline.push({ $limit: 1e3 });
|
|
|
625
667
|
count: result[0]?.meta[0]?.count || 0
|
|
626
668
|
};
|
|
627
669
|
}
|
|
628
|
-
|
|
629
670
|
//#endregion
|
|
630
|
-
//#region
|
|
671
|
+
//#region src/lib/mongo-schema-sync.ts
|
|
631
672
|
const DESTRUCTIVE_OPTION_KEYS = new Set([
|
|
632
673
|
"capped",
|
|
633
674
|
"capped.size",
|
|
@@ -645,7 +686,7 @@ function getDesiredTableOptionsImpl(cappedOptions) {
|
|
|
645
686
|
key: "capped.size",
|
|
646
687
|
value: String(cappedOptions.size)
|
|
647
688
|
}];
|
|
648
|
-
if (cappedOptions.max !==
|
|
689
|
+
if (cappedOptions.max !== void 0) opts.push({
|
|
649
690
|
key: "capped.max",
|
|
650
691
|
value: String(cappedOptions.max)
|
|
651
692
|
});
|
|
@@ -660,11 +701,11 @@ async function getExistingTableOptionsImpl(host) {
|
|
|
660
701
|
key: "capped",
|
|
661
702
|
value: "true"
|
|
662
703
|
}];
|
|
663
|
-
if (collOpts.size !==
|
|
704
|
+
if (collOpts.size !== void 0) opts.push({
|
|
664
705
|
key: "capped.size",
|
|
665
706
|
value: String(collOpts.size)
|
|
666
707
|
});
|
|
667
|
-
if (collOpts.max !==
|
|
708
|
+
if (collOpts.max !== void 0) opts.push({
|
|
668
709
|
key: "capped.max",
|
|
669
710
|
value: String(collOpts.max)
|
|
670
711
|
});
|
|
@@ -674,9 +715,9 @@ async function ensureTableImpl(host, table) {
|
|
|
674
715
|
if (table instanceof AtscriptDbView && !table.isExternal) return ensureView(host, table);
|
|
675
716
|
return host.ensureCollectionExists();
|
|
676
717
|
}
|
|
677
|
-
/** Creates a MongoDB view from the AtscriptDbView's view plan. */
|
|
678
|
-
|
|
679
|
-
if (
|
|
718
|
+
/** Creates a MongoDB view from the AtscriptDbView's view plan. */
|
|
719
|
+
async function ensureView(host, view) {
|
|
720
|
+
if (await host.collectionExists()) return;
|
|
680
721
|
const plan = view.viewPlan;
|
|
681
722
|
const columns = view.getViewColumnMappings();
|
|
682
723
|
const pipeline = [];
|
|
@@ -698,13 +739,14 @@ async function ensureTableImpl(host, table) {
|
|
|
698
739
|
pipeline.push({ $match: matchExpr });
|
|
699
740
|
}
|
|
700
741
|
const hasAggregates = columns.some((c) => c.aggFn);
|
|
701
|
-
/** Resolves a column to its MongoDB source field path. */
|
|
742
|
+
/** Resolves a column to its MongoDB source field path. */
|
|
743
|
+
const colSourceField = (col) => col.sourceTable === plan.entryTable ? `$${col.sourceColumn}` : `$${JOINED_PREFIX}${col.sourceTable}.${col.sourceColumn}`;
|
|
702
744
|
if (hasAggregates) {
|
|
703
745
|
const group = { _id: {} };
|
|
704
746
|
const project = { _id: 0 };
|
|
705
747
|
for (const col of columns) if (col.aggFn) {
|
|
706
748
|
if (col.aggFn === "count" && col.aggField === "*") group[col.viewColumn] = { $sum: 1 };
|
|
707
|
-
else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
749
|
+
else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
708
750
|
project[col.viewColumn] = `$${col.viewColumn}`;
|
|
709
751
|
} else {
|
|
710
752
|
group._id[col.viewColumn] = colSourceField(col);
|
|
@@ -727,11 +769,10 @@ else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
|
727
769
|
pipeline
|
|
728
770
|
});
|
|
729
771
|
}
|
|
730
|
-
/** Extracts localField/foreignField from a join condition. */
|
|
731
|
-
|
|
732
|
-
const c =
|
|
733
|
-
|
|
734
|
-
if (leftTable === joinTable) return {
|
|
772
|
+
/** Extracts localField/foreignField from a join condition. */
|
|
773
|
+
function resolveJoinFields(condition, entryTable, joinTable) {
|
|
774
|
+
const c = "$and" in condition ? condition.$and[0] : condition;
|
|
775
|
+
if ((c.left.type ? c.left.type()?.metadata?.get("db.table") || "" : entryTable) === joinTable) return {
|
|
735
776
|
localField: c.right.field,
|
|
736
777
|
foreignField: c.left.field
|
|
737
778
|
};
|
|
@@ -740,7 +781,8 @@ else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
|
740
781
|
foreignField: c.right.field
|
|
741
782
|
};
|
|
742
783
|
}
|
|
743
|
-
/** Translates an AtscriptQueryNode to a MongoDB $match expression. */
|
|
784
|
+
/** Translates an AtscriptQueryNode to a MongoDB $match expression. */
|
|
785
|
+
function queryNodeToMatch(node, entryTable) {
|
|
744
786
|
if ("$and" in node) {
|
|
745
787
|
const items = node.$and;
|
|
746
788
|
const result = [];
|
|
@@ -764,7 +806,8 @@ else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
|
764
806
|
if (comp.op === "$ne") return { [fieldPath]: { $ne: comp.right } };
|
|
765
807
|
return { [fieldPath]: { [comp.op]: comp.right } };
|
|
766
808
|
}
|
|
767
|
-
/** Resolves a field ref to a MongoDB dot path for view pipeline expressions. */
|
|
809
|
+
/** Resolves a field ref to a MongoDB dot path for view pipeline expressions. */
|
|
810
|
+
function resolveViewFieldPath(ref, entryTable) {
|
|
768
811
|
if (!ref.type) return ref.field;
|
|
769
812
|
const table = ref.type()?.metadata?.get("db.table") || "";
|
|
770
813
|
if (table === entryTable) return ref.field;
|
|
@@ -773,7 +816,8 @@ else group[col.viewColumn] = { [`$${col.aggFn}`]: colSourceField(col) };
|
|
|
773
816
|
/**
|
|
774
817
|
* Translates an AtscriptQueryNode to a MongoDB $match for use after $group (HAVING).
|
|
775
818
|
* After $group, aggregate fields are top-level and dimension fields are under _id.
|
|
776
|
-
*/
|
|
819
|
+
*/
|
|
820
|
+
function queryNodeToHaving(node, columns) {
|
|
777
821
|
const colMap = new Map(columns.map((c) => [c.viewColumn, c]));
|
|
778
822
|
const resolveHavingField = (ref) => {
|
|
779
823
|
if (!ref.type) {
|
|
@@ -854,7 +898,7 @@ async function syncColumnsImpl(host, diff) {
|
|
|
854
898
|
const setSpec = {};
|
|
855
899
|
for (const field of diff.added) {
|
|
856
900
|
const defaultVal = resolveSyncDefault(field);
|
|
857
|
-
if (defaultVal !==
|
|
901
|
+
if (defaultVal !== void 0) setSpec[field.physicalName] = defaultVal;
|
|
858
902
|
added.push(field.physicalName);
|
|
859
903
|
}
|
|
860
904
|
if (Object.keys(setSpec).length > 0) update.$set = setSpec;
|
|
@@ -871,14 +915,14 @@ async function dropColumnsImpl(host, columns) {
|
|
|
871
915
|
for (const col of columns) unsetSpec[col] = "";
|
|
872
916
|
await host.collection.updateMany({}, { $unset: unsetSpec }, host._getSessionOpts());
|
|
873
917
|
}
|
|
874
|
-
/** Resolves a field's default value for bulk $set during column sync. */
|
|
875
|
-
|
|
918
|
+
/** Resolves a field's default value for bulk $set during column sync. */
|
|
919
|
+
function resolveSyncDefault(field) {
|
|
920
|
+
if (!field.defaultValue) return field.optional ? null : void 0;
|
|
876
921
|
if (field.defaultValue.kind === "value") return field.defaultValue.value;
|
|
877
|
-
return undefined;
|
|
878
922
|
}
|
|
879
923
|
async function syncIndexesImpl(host) {
|
|
880
924
|
await host.ensureCollectionExists();
|
|
881
|
-
const allIndexes = new Map();
|
|
925
|
+
const allIndexes = /* @__PURE__ */ new Map();
|
|
882
926
|
for (const [key, index] of host._table.indexes.entries()) {
|
|
883
927
|
const fields = {};
|
|
884
928
|
const weights = {};
|
|
@@ -911,20 +955,19 @@ async function syncIndexesImpl(host) {
|
|
|
911
955
|
const existingIndexes = await host.collection.listIndexes().toArray();
|
|
912
956
|
const indexesToCreate = new Map(allIndexes);
|
|
913
957
|
for (const remote of existingIndexes) {
|
|
914
|
-
if (!remote.name.startsWith(
|
|
958
|
+
if (!remote.name.startsWith("atscript__")) continue;
|
|
915
959
|
if (indexesToCreate.has(remote.name)) {
|
|
916
960
|
const local = indexesToCreate.get(remote.name);
|
|
917
961
|
switch (local.type) {
|
|
918
962
|
case "plain":
|
|
919
963
|
case "unique":
|
|
920
|
-
case "text":
|
|
964
|
+
case "text":
|
|
921
965
|
if ((local.type === "text" || objMatch(local.fields, remote.key)) && objMatch(local.weights || {}, remote.weights || {})) indexesToCreate.delete(remote.name);
|
|
922
|
-
else {
|
|
966
|
+
else {
|
|
923
967
|
host._log("dropIndex", remote.name);
|
|
924
968
|
await host.collection.dropIndex(remote.name);
|
|
925
969
|
}
|
|
926
970
|
break;
|
|
927
|
-
}
|
|
928
971
|
default:
|
|
929
972
|
}
|
|
930
973
|
} else {
|
|
@@ -933,13 +976,12 @@ else {
|
|
|
933
976
|
}
|
|
934
977
|
}
|
|
935
978
|
for (const [key, value] of allIndexes.entries()) switch (value.type) {
|
|
936
|
-
case "plain":
|
|
979
|
+
case "plain":
|
|
937
980
|
if (!indexesToCreate.has(key)) continue;
|
|
938
981
|
host._log("createIndex", key, value.fields);
|
|
939
982
|
await host.collection.createIndex(value.fields, { name: key });
|
|
940
983
|
break;
|
|
941
|
-
|
|
942
|
-
case "unique": {
|
|
984
|
+
case "unique":
|
|
943
985
|
if (!indexesToCreate.has(key)) continue;
|
|
944
986
|
host._log("createIndex (unique)", key, value.fields);
|
|
945
987
|
await host.collection.createIndex(value.fields, {
|
|
@@ -947,8 +989,7 @@ else {
|
|
|
947
989
|
unique: true
|
|
948
990
|
});
|
|
949
991
|
break;
|
|
950
|
-
|
|
951
|
-
case "text": {
|
|
992
|
+
case "text":
|
|
952
993
|
if (!indexesToCreate.has(key)) continue;
|
|
953
994
|
host._log("createIndex (text)", key, value.fields);
|
|
954
995
|
await host.collection.createIndex(value.fields, {
|
|
@@ -956,14 +997,13 @@ else {
|
|
|
956
997
|
name: key
|
|
957
998
|
});
|
|
958
999
|
break;
|
|
959
|
-
}
|
|
960
1000
|
default:
|
|
961
1001
|
}
|
|
962
1002
|
try {
|
|
963
|
-
const toUpdate = new Set();
|
|
1003
|
+
const toUpdate = /* @__PURE__ */ new Set();
|
|
964
1004
|
const existingSearchIndexes = await host.collection.listSearchIndexes().toArray();
|
|
965
1005
|
for (const remote of existingSearchIndexes) {
|
|
966
|
-
if (!remote.name.startsWith(
|
|
1006
|
+
if (!remote.name.startsWith("atscript__")) continue;
|
|
967
1007
|
if (indexesToCreate.has(remote.name)) {
|
|
968
1008
|
const local = indexesToCreate.get(remote.name);
|
|
969
1009
|
const right = remote.latestDefinition;
|
|
@@ -972,14 +1012,13 @@ else {
|
|
|
972
1012
|
case "search_text": {
|
|
973
1013
|
const left = local.definition;
|
|
974
1014
|
if (left.analyzer === right.analyzer && fieldsMatch(left.mappings.fields || {}, right.mappings.fields || {})) indexesToCreate.delete(remote.name);
|
|
975
|
-
else toUpdate.add(remote.name);
|
|
1015
|
+
else toUpdate.add(remote.name);
|
|
976
1016
|
break;
|
|
977
1017
|
}
|
|
978
|
-
case "vector":
|
|
1018
|
+
case "vector":
|
|
979
1019
|
if (vectorFieldsMatch(local.definition.fields || [], right.fields || [])) indexesToCreate.delete(remote.name);
|
|
980
|
-
else toUpdate.add(remote.name);
|
|
1020
|
+
else toUpdate.add(remote.name);
|
|
981
1021
|
break;
|
|
982
|
-
}
|
|
983
1022
|
default:
|
|
984
1023
|
}
|
|
985
1024
|
} else if (remote.status !== "DELETING") {
|
|
@@ -990,7 +1029,7 @@ else toUpdate.add(remote.name);
|
|
|
990
1029
|
for (const [key, value] of indexesToCreate.entries()) switch (value.type) {
|
|
991
1030
|
case "dynamic_text":
|
|
992
1031
|
case "search_text":
|
|
993
|
-
case "vector":
|
|
1032
|
+
case "vector":
|
|
994
1033
|
if (toUpdate.has(key)) {
|
|
995
1034
|
host._log("updateSearchIndex", key, value.definition);
|
|
996
1035
|
await host.collection.updateSearchIndex(key, value.definition);
|
|
@@ -1003,7 +1042,6 @@ else toUpdate.add(remote.name);
|
|
|
1003
1042
|
});
|
|
1004
1043
|
}
|
|
1005
1044
|
break;
|
|
1006
|
-
}
|
|
1007
1045
|
default:
|
|
1008
1046
|
}
|
|
1009
1047
|
} catch {}
|
|
@@ -1028,7 +1066,7 @@ function fieldsMatch(left, right) {
|
|
|
1028
1066
|
}
|
|
1029
1067
|
function vectorFieldsMatch(left, right) {
|
|
1030
1068
|
if (left.length !== (right || []).length) return false;
|
|
1031
|
-
const rightMap = new Map();
|
|
1069
|
+
const rightMap = /* @__PURE__ */ new Map();
|
|
1032
1070
|
for (const f of right || []) rightMap.set(f.path, f);
|
|
1033
1071
|
for (const l of left) {
|
|
1034
1072
|
const r = rightMap.get(l.path);
|
|
@@ -1037,35 +1075,52 @@ function vectorFieldsMatch(left, right) {
|
|
|
1037
1075
|
}
|
|
1038
1076
|
return true;
|
|
1039
1077
|
}
|
|
1040
|
-
|
|
1041
1078
|
//#endregion
|
|
1042
|
-
//#region
|
|
1079
|
+
//#region src/lib/validate-plugins.ts
|
|
1043
1080
|
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
1044
1081
|
if (def.type.tags?.has("objectId")) {
|
|
1045
|
-
if (ctx.path === "_id" && (value ===
|
|
1082
|
+
if (ctx.path === "_id" && (value === void 0 || value === null)) {
|
|
1046
1083
|
const dbCtx = ctx.context;
|
|
1047
1084
|
if (dbCtx && (dbCtx.mode === "insert" || dbCtx.mode === "replace")) return true;
|
|
1048
1085
|
}
|
|
1049
1086
|
return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
|
|
1050
1087
|
}
|
|
1051
1088
|
};
|
|
1052
|
-
|
|
1053
1089
|
//#endregion
|
|
1054
|
-
//#region
|
|
1055
|
-
function _define_property(obj, key, value) {
|
|
1056
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
1057
|
-
value,
|
|
1058
|
-
enumerable: true,
|
|
1059
|
-
configurable: true,
|
|
1060
|
-
writable: true
|
|
1061
|
-
});
|
|
1062
|
-
else obj[key] = value;
|
|
1063
|
-
return obj;
|
|
1064
|
-
}
|
|
1090
|
+
//#region src/lib/mongo-adapter.ts
|
|
1065
1091
|
var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
1092
|
+
_collection;
|
|
1093
|
+
/** MongoDB-specific indexes (search, vector) — separate from table.indexes. */
|
|
1094
|
+
_mongoIndexes = /* @__PURE__ */ new Map();
|
|
1095
|
+
/** Vector search filter associations built during flattening. */
|
|
1096
|
+
_vectorFilters = /* @__PURE__ */ new Map();
|
|
1097
|
+
/** Default similarity thresholds per vector index (from @db.search.vector.threshold). */
|
|
1098
|
+
_vectorThresholds = /* @__PURE__ */ new Map();
|
|
1099
|
+
/** Cached search index lookup. */
|
|
1100
|
+
_searchIndexesMap;
|
|
1101
|
+
/** Physical field names with @db.default.increment → optional start value. */
|
|
1102
|
+
_incrementFields = /* @__PURE__ */ new Map();
|
|
1103
|
+
/** Physical field names that have a non-binary collation (nocase/unicode). */
|
|
1104
|
+
_collateFields;
|
|
1105
|
+
/** Capped collection options from @db.mongo.capped. */
|
|
1106
|
+
_cappedOptions;
|
|
1107
|
+
/** Whether the schema explicitly defines _id (via @db.mongo.collection or manual _id field). */
|
|
1108
|
+
_hasExplicitId = false;
|
|
1109
|
+
/** Unique fields accumulated during onFieldScanned, returned via getMetadataOverrides. */
|
|
1110
|
+
_pendingUniqueFields = [];
|
|
1111
|
+
constructor(db, client) {
|
|
1112
|
+
super();
|
|
1113
|
+
this.db = db;
|
|
1114
|
+
this.client = client;
|
|
1115
|
+
}
|
|
1066
1116
|
get _client() {
|
|
1067
1117
|
return this.client;
|
|
1068
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
1121
|
+
* Shared across all adapter instances for the same client so topology is probed once.
|
|
1122
|
+
*/
|
|
1123
|
+
static _txDisabledClients = /* @__PURE__ */ new WeakSet();
|
|
1069
1124
|
get _txDisabled() {
|
|
1070
1125
|
return this.client ? MongoAdapter._txDisabledClients.has(this.client) : true;
|
|
1071
1126
|
}
|
|
@@ -1076,14 +1131,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1076
1131
|
* Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
|
|
1077
1132
|
* This handles txnNumber management and automatic retry for
|
|
1078
1133
|
* `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
1079
|
-
*/
|
|
1134
|
+
*/
|
|
1135
|
+
async withTransaction(fn) {
|
|
1080
1136
|
if (this._getTransactionState()) return fn();
|
|
1081
1137
|
if (this._txDisabled || !this._client) return fn();
|
|
1082
1138
|
try {
|
|
1083
1139
|
const topology = this._client.topology;
|
|
1084
1140
|
if (topology) {
|
|
1085
|
-
const
|
|
1086
|
-
const type = desc?.type;
|
|
1141
|
+
const type = (topology.description ?? topology.s?.description)?.type;
|
|
1087
1142
|
if (type === "Single" || type === "Unknown") {
|
|
1088
1143
|
this._txDisabled = true;
|
|
1089
1144
|
return fn();
|
|
@@ -1106,7 +1161,9 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1106
1161
|
} catch {}
|
|
1107
1162
|
}
|
|
1108
1163
|
}
|
|
1109
|
-
|
|
1164
|
+
static _noSession = Object.freeze({});
|
|
1165
|
+
/** Returns `{ session }` opts if inside a transaction, empty object otherwise. */
|
|
1166
|
+
_getSessionOpts() {
|
|
1110
1167
|
const session = this._getTransactionState();
|
|
1111
1168
|
return session ? { session } : MongoAdapter._noSession;
|
|
1112
1169
|
}
|
|
@@ -1118,11 +1175,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1118
1175
|
return this.collection.aggregate(pipeline, this._getSessionOpts());
|
|
1119
1176
|
}
|
|
1120
1177
|
async aggregate(query) {
|
|
1121
|
-
const { buildAggregatePipeline, buildCountPipeline } = await import("./agg
|
|
1178
|
+
const { buildAggregatePipeline, buildCountPipeline } = await import("./agg.mjs");
|
|
1122
1179
|
if (query.controls?.$count) {
|
|
1123
|
-
const pipeline
|
|
1124
|
-
this._log("aggregate (count)", pipeline
|
|
1125
|
-
const result = await this.aggregatePipeline(pipeline
|
|
1180
|
+
const pipeline = buildCountPipeline(query);
|
|
1181
|
+
this._log("aggregate (count)", pipeline);
|
|
1182
|
+
const result = await this.aggregatePipeline(pipeline).toArray();
|
|
1126
1183
|
return result.length > 0 ? result : [{ count: 0 }];
|
|
1127
1184
|
}
|
|
1128
1185
|
const pipeline = buildAggregatePipeline(query);
|
|
@@ -1136,19 +1193,20 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1136
1193
|
if (idProp?.type.kind === "") return idProp.type.designType;
|
|
1137
1194
|
return "objectId";
|
|
1138
1195
|
}
|
|
1139
|
-
prepareId(id,
|
|
1196
|
+
prepareId(id, _fieldType) {
|
|
1197
|
+
const fieldType = _fieldType;
|
|
1140
1198
|
const tags = fieldType.type.tags;
|
|
1141
1199
|
if (tags?.has("objectId") && tags?.has("mongo")) return id instanceof ObjectId ? id : new ObjectId(id);
|
|
1142
1200
|
if (fieldType.type.kind === "") {
|
|
1143
|
-
|
|
1144
|
-
if (dt === "number") return Number(id);
|
|
1201
|
+
if (fieldType.type.designType === "number") return Number(id);
|
|
1145
1202
|
}
|
|
1146
1203
|
return String(id);
|
|
1147
1204
|
}
|
|
1148
1205
|
/**
|
|
1149
1206
|
* Convenience method that uses `idType` to transform an ID value.
|
|
1150
1207
|
* For use in controllers that don't have access to the field type.
|
|
1151
|
-
*/
|
|
1208
|
+
*/
|
|
1209
|
+
prepareIdFromIdType(id) {
|
|
1152
1210
|
switch (this.idType) {
|
|
1153
1211
|
case "objectId": return id instanceof ObjectId ? id : new ObjectId(id);
|
|
1154
1212
|
case "number": return Number(id);
|
|
@@ -1165,26 +1223,24 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1165
1223
|
getValidatorPlugins() {
|
|
1166
1224
|
return [validateMongoIdPlugin];
|
|
1167
1225
|
}
|
|
1168
|
-
getAdapterTableName(
|
|
1169
|
-
return undefined;
|
|
1170
|
-
}
|
|
1226
|
+
getAdapterTableName(_type) {}
|
|
1171
1227
|
supportsNativeRelations() {
|
|
1172
1228
|
return true;
|
|
1173
1229
|
}
|
|
1174
1230
|
async loadRelations(rows, withRelations, relations, foreignKeys, tableResolver) {
|
|
1175
1231
|
return loadRelationsImpl(this, rows, withRelations, relations, foreignKeys, tableResolver);
|
|
1176
1232
|
}
|
|
1177
|
-
/** Returns the context object used by CollectionPatcher. */
|
|
1233
|
+
/** Returns the context object used by CollectionPatcher. */
|
|
1234
|
+
getPatcherContext() {
|
|
1178
1235
|
return {
|
|
1179
1236
|
flatMap: this._table.flatMap,
|
|
1180
1237
|
prepareId: (id) => this.prepareIdFromIdType(id),
|
|
1181
1238
|
createValidator: (opts) => this._table.createValidator(opts)
|
|
1182
1239
|
};
|
|
1183
1240
|
}
|
|
1184
|
-
async nativePatch(filter, patch) {
|
|
1241
|
+
async nativePatch(filter, patch, ops) {
|
|
1185
1242
|
const mongoFilter = buildMongoFilter(filter);
|
|
1186
|
-
const
|
|
1187
|
-
const { updateFilter, updateOptions } = patcher.preparePatch();
|
|
1243
|
+
const { updateFilter, updateOptions } = new CollectionPatcher(this.getPatcherContext(), patch, ops).preparePatch();
|
|
1188
1244
|
this._log("updateOne (patch)", mongoFilter, updateFilter);
|
|
1189
1245
|
const result = await this.collection.updateOne(mongoFilter, updateFilter, {
|
|
1190
1246
|
...updateOptions,
|
|
@@ -1195,8 +1251,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1195
1251
|
modifiedCount: result.modifiedCount
|
|
1196
1252
|
};
|
|
1197
1253
|
}
|
|
1198
|
-
onBeforeFlatten(
|
|
1199
|
-
const typeMeta =
|
|
1254
|
+
onBeforeFlatten(_type) {
|
|
1255
|
+
const typeMeta = _type.metadata;
|
|
1200
1256
|
const capped = typeMeta.get("db.mongo.capped");
|
|
1201
1257
|
if (capped) this._cappedOptions = {
|
|
1202
1258
|
size: capped.size,
|
|
@@ -1214,7 +1270,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1214
1270
|
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
1215
1271
|
});
|
|
1216
1272
|
}
|
|
1217
|
-
onFieldScanned(field,
|
|
1273
|
+
onFieldScanned(field, _type, metadata) {
|
|
1218
1274
|
if (field === "_id") this._hasExplicitId = true;
|
|
1219
1275
|
if (field !== "_id" && metadata.has("meta.id")) {
|
|
1220
1276
|
this._addMongoIndexField("unique", "__pk", field);
|
|
@@ -1223,7 +1279,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1223
1279
|
if (metadata.has("db.default.increment")) {
|
|
1224
1280
|
const physicalName = metadata.get("db.column") ?? field;
|
|
1225
1281
|
const startValue = metadata.get("db.default.increment");
|
|
1226
|
-
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue :
|
|
1282
|
+
this._incrementFields.set(physicalName, typeof startValue === "number" ? startValue : void 0);
|
|
1227
1283
|
}
|
|
1228
1284
|
for (const index of metadata.get("db.index.fulltext") || []) {
|
|
1229
1285
|
const name = typeof index === "object" ? index.name || "" : "";
|
|
@@ -1241,7 +1297,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1241
1297
|
numDimensions: vectorIndex.dimensions
|
|
1242
1298
|
}] });
|
|
1243
1299
|
const threshold = metadata.get("db.search.vector.threshold");
|
|
1244
|
-
if (threshold !==
|
|
1300
|
+
if (threshold !== void 0) this._vectorThresholds.set(mongoIndexKey("vector", indexName), threshold);
|
|
1245
1301
|
}
|
|
1246
1302
|
for (const indexName of metadata.get("db.search.filter") || []) this._vectorFilters.set(mongoIndexKey("vector", indexName), field);
|
|
1247
1303
|
}
|
|
@@ -1250,7 +1306,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1250
1306
|
if (this._hasExplicitId) return {
|
|
1251
1307
|
addPrimaryKeys: ["_id"],
|
|
1252
1308
|
removePrimaryKeys: meta.originalMetaIdFields.filter((f) => f !== "_id"),
|
|
1253
|
-
addUniqueFields: uniqueFields.length > 0 ? uniqueFields :
|
|
1309
|
+
addUniqueFields: uniqueFields.length > 0 ? uniqueFields : void 0
|
|
1254
1310
|
};
|
|
1255
1311
|
uniqueFields.push("_id");
|
|
1256
1312
|
return {
|
|
@@ -1263,7 +1319,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1263
1319
|
designType: "string",
|
|
1264
1320
|
tags: new Set(["objectId", "mongo"])
|
|
1265
1321
|
},
|
|
1266
|
-
metadata: new Map()
|
|
1322
|
+
metadata: /* @__PURE__ */ new Map()
|
|
1267
1323
|
}
|
|
1268
1324
|
}],
|
|
1269
1325
|
addUniqueFields: uniqueFields
|
|
@@ -1278,14 +1334,15 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1278
1334
|
});
|
|
1279
1335
|
}
|
|
1280
1336
|
for (const fd of this._table.fieldDescriptors) if (fd.collate && fd.collate !== "binary") {
|
|
1281
|
-
if (!this._collateFields) this._collateFields = new Map();
|
|
1337
|
+
if (!this._collateFields) this._collateFields = /* @__PURE__ */ new Map();
|
|
1282
1338
|
this._collateFields.set(fd.physicalName, fd.collate);
|
|
1283
1339
|
}
|
|
1284
1340
|
}
|
|
1285
|
-
/** Returns MongoDB-specific search index map (internal). */
|
|
1341
|
+
/** Returns MongoDB-specific search index map (internal). */
|
|
1342
|
+
getMongoSearchIndexes() {
|
|
1286
1343
|
if (!this._searchIndexesMap) {
|
|
1287
1344
|
this._table.flatMap;
|
|
1288
|
-
this._searchIndexesMap = new Map();
|
|
1345
|
+
this._searchIndexesMap = /* @__PURE__ */ new Map();
|
|
1289
1346
|
let defaultIndex;
|
|
1290
1347
|
for (const index of this._table.indexes.values()) if (index.type === "fulltext" && !defaultIndex) defaultIndex = {
|
|
1291
1348
|
key: index.key,
|
|
@@ -1295,34 +1352,32 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1295
1352
|
weights: Object.fromEntries(index.fields.filter((f) => f.weight).map((f) => [f.name, f.weight]))
|
|
1296
1353
|
};
|
|
1297
1354
|
for (const index of this._mongoIndexes.values()) switch (index.type) {
|
|
1298
|
-
case "text":
|
|
1355
|
+
case "text":
|
|
1299
1356
|
if (!defaultIndex) defaultIndex = index;
|
|
1300
1357
|
break;
|
|
1301
|
-
|
|
1302
|
-
case "dynamic_text": {
|
|
1358
|
+
case "dynamic_text":
|
|
1303
1359
|
defaultIndex = index;
|
|
1304
1360
|
break;
|
|
1305
|
-
|
|
1306
|
-
case "search_text": {
|
|
1361
|
+
case "search_text":
|
|
1307
1362
|
if (!defaultIndex || defaultIndex.type === "text") defaultIndex = index;
|
|
1308
1363
|
this._searchIndexesMap.set(index.name, index);
|
|
1309
1364
|
break;
|
|
1310
|
-
|
|
1311
|
-
case "vector": {
|
|
1365
|
+
case "vector":
|
|
1312
1366
|
this._searchIndexesMap.set(index.name, index);
|
|
1313
1367
|
break;
|
|
1314
|
-
}
|
|
1315
1368
|
default:
|
|
1316
1369
|
}
|
|
1317
|
-
if (defaultIndex && !this._searchIndexesMap.has(
|
|
1370
|
+
if (defaultIndex && !this._searchIndexesMap.has("DEFAULT")) this._searchIndexesMap.set(DEFAULT_INDEX_NAME, defaultIndex);
|
|
1318
1371
|
}
|
|
1319
1372
|
return this._searchIndexesMap;
|
|
1320
1373
|
}
|
|
1321
|
-
/** Returns a specific MongoDB search index by name. */
|
|
1374
|
+
/** Returns a specific MongoDB search index by name. */
|
|
1375
|
+
getMongoSearchIndex(name = DEFAULT_INDEX_NAME) {
|
|
1322
1376
|
return this.getMongoSearchIndexes().get(name);
|
|
1323
1377
|
}
|
|
1324
|
-
/** Returns the default similarity threshold for a vector index (from @db.search.vector.threshold). */
|
|
1325
|
-
|
|
1378
|
+
/** Returns the default similarity threshold for a vector index (from @db.search.vector.threshold). */
|
|
1379
|
+
getVectorThreshold(indexName) {
|
|
1380
|
+
const key = mongoIndexKey("vector", indexName || "DEFAULT");
|
|
1326
1381
|
return this._vectorThresholds.get(key);
|
|
1327
1382
|
}
|
|
1328
1383
|
getSearchIndexes() {
|
|
@@ -1366,18 +1421,16 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1366
1421
|
};
|
|
1367
1422
|
}
|
|
1368
1423
|
async collectionExists() {
|
|
1369
|
-
|
|
1370
|
-
return cols.length > 0;
|
|
1424
|
+
return (await this.db.listCollections({ name: this._table.tableName }).toArray()).length > 0;
|
|
1371
1425
|
}
|
|
1372
1426
|
async ensureCollectionExists() {
|
|
1373
|
-
|
|
1374
|
-
if (!exists) {
|
|
1427
|
+
if (!await this.collectionExists()) {
|
|
1375
1428
|
this._log("createCollection", this._table.tableName);
|
|
1376
1429
|
const opts = { comment: "Created by Atscript Mongo Adapter" };
|
|
1377
1430
|
if (this._cappedOptions) {
|
|
1378
1431
|
opts.capped = true;
|
|
1379
1432
|
opts.size = this._cappedOptions.size;
|
|
1380
|
-
if (this._cappedOptions.max !== null && this._cappedOptions.max !==
|
|
1433
|
+
if (this._cappedOptions.max !== null && this._cappedOptions.max !== void 0) opts.max = this._cappedOptions.max;
|
|
1381
1434
|
}
|
|
1382
1435
|
await this.db.createCollection(this._table.tableName, opts);
|
|
1383
1436
|
}
|
|
@@ -1385,17 +1438,15 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1385
1438
|
/**
|
|
1386
1439
|
* Wraps an async operation to catch MongoDB duplicate key errors
|
|
1387
1440
|
* (code 11000) and rethrow as structured `DbError`.
|
|
1388
|
-
*/
|
|
1441
|
+
*/
|
|
1442
|
+
async _wrapDuplicateKeyError(fn) {
|
|
1389
1443
|
try {
|
|
1390
1444
|
return await fn();
|
|
1391
1445
|
} catch (error) {
|
|
1392
|
-
if (error instanceof MongoServerError && error.code === 11e3) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
message: error.message
|
|
1397
|
-
}]);
|
|
1398
|
-
}
|
|
1446
|
+
if (error instanceof MongoServerError && error.code === 11e3) throw new DbError("CONFLICT", [{
|
|
1447
|
+
path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
|
|
1448
|
+
message: error.message
|
|
1449
|
+
}]);
|
|
1399
1450
|
throw error;
|
|
1400
1451
|
}
|
|
1401
1452
|
}
|
|
@@ -1413,7 +1464,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1413
1464
|
}
|
|
1414
1465
|
async insertMany(data) {
|
|
1415
1466
|
if (this._incrementFields.size > 0) {
|
|
1416
|
-
const allFields = new Set();
|
|
1467
|
+
const allFields = /* @__PURE__ */ new Set();
|
|
1417
1468
|
for (const item of data) for (const f of this._fieldsNeedingIncrement(item)) allFields.add(f);
|
|
1418
1469
|
if (allFields.size > 0) await this._assignBatchIncrements(data, allFields);
|
|
1419
1470
|
}
|
|
@@ -1452,10 +1503,11 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1452
1503
|
...this._getSessionOpts()
|
|
1453
1504
|
});
|
|
1454
1505
|
}
|
|
1455
|
-
async updateOne(filter, data) {
|
|
1506
|
+
async updateOne(filter, data, ops) {
|
|
1456
1507
|
const mongoFilter = buildMongoFilter(filter);
|
|
1457
|
-
|
|
1458
|
-
|
|
1508
|
+
const updateDoc = buildMongoUpdateDoc(data, ops);
|
|
1509
|
+
this._log("updateOne", mongoFilter, updateDoc);
|
|
1510
|
+
const result = await this.collection.updateOne(mongoFilter, updateDoc, this._getSessionOpts());
|
|
1459
1511
|
return {
|
|
1460
1512
|
matchedCount: result.matchedCount,
|
|
1461
1513
|
modifiedCount: result.modifiedCount
|
|
@@ -1473,13 +1525,13 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1473
1525
|
async deleteOne(filter) {
|
|
1474
1526
|
const mongoFilter = buildMongoFilter(filter);
|
|
1475
1527
|
this._log("deleteOne", mongoFilter);
|
|
1476
|
-
|
|
1477
|
-
return { deletedCount: result.deletedCount };
|
|
1528
|
+
return { deletedCount: (await this.collection.deleteOne(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
1478
1529
|
}
|
|
1479
|
-
async updateMany(filter, data) {
|
|
1530
|
+
async updateMany(filter, data, ops) {
|
|
1480
1531
|
const mongoFilter = buildMongoFilter(filter);
|
|
1481
|
-
|
|
1482
|
-
|
|
1532
|
+
const updateDoc = buildMongoUpdateDoc(data, ops);
|
|
1533
|
+
this._log("updateMany", mongoFilter, updateDoc);
|
|
1534
|
+
const result = await this.collection.updateMany(mongoFilter, updateDoc, this._getSessionOpts());
|
|
1483
1535
|
return {
|
|
1484
1536
|
matchedCount: result.matchedCount,
|
|
1485
1537
|
modifiedCount: result.modifiedCount
|
|
@@ -1497,11 +1549,10 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1497
1549
|
async deleteMany(filter) {
|
|
1498
1550
|
const mongoFilter = buildMongoFilter(filter);
|
|
1499
1551
|
this._log("deleteMany", mongoFilter);
|
|
1500
|
-
|
|
1501
|
-
return { deletedCount: result.deletedCount };
|
|
1552
|
+
return { deletedCount: (await this.collection.deleteMany(mongoFilter, this._getSessionOpts())).deletedCount };
|
|
1502
1553
|
}
|
|
1503
1554
|
clearCollectionCache() {
|
|
1504
|
-
this._collection =
|
|
1555
|
+
this._collection = void 0;
|
|
1505
1556
|
}
|
|
1506
1557
|
async tableExists() {
|
|
1507
1558
|
return tableExistsImpl(this);
|
|
@@ -1542,29 +1593,31 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1542
1593
|
destructiveOptionKeys() {
|
|
1543
1594
|
return DESTRUCTIVE_OPTION_KEYS;
|
|
1544
1595
|
}
|
|
1545
|
-
/** Returns the counters collection used for atomic auto-increment. */
|
|
1596
|
+
/** Returns the counters collection used for atomic auto-increment. */
|
|
1597
|
+
get _countersCollection() {
|
|
1546
1598
|
return this.db.collection("__atscript_counters");
|
|
1547
1599
|
}
|
|
1548
|
-
/** Returns physical field names of increment fields that are undefined in the data. */
|
|
1600
|
+
/** Returns physical field names of increment fields that are undefined in the data. */
|
|
1601
|
+
_fieldsNeedingIncrement(data) {
|
|
1549
1602
|
const result = [];
|
|
1550
|
-
for (const physical of this._incrementFields.keys()) if (data[physical] ===
|
|
1603
|
+
for (const physical of this._incrementFields.keys()) if (data[physical] === void 0 || data[physical] === null) result.push(physical);
|
|
1551
1604
|
return result;
|
|
1552
1605
|
}
|
|
1553
1606
|
/**
|
|
1554
1607
|
* Atomically allocates `count` sequential values for each increment field
|
|
1555
1608
|
* using a counter collection. Returns a map of field → first allocated value.
|
|
1556
|
-
*/
|
|
1609
|
+
*/
|
|
1610
|
+
async _allocateIncrementValues(physicalFields, count) {
|
|
1557
1611
|
const counters = this._countersCollection;
|
|
1558
1612
|
const collectionName = this._table.tableName;
|
|
1559
|
-
const result = new Map();
|
|
1613
|
+
const result = /* @__PURE__ */ new Map();
|
|
1560
1614
|
for (const field of physicalFields) {
|
|
1561
1615
|
const counterId = `${collectionName}.${field}`;
|
|
1562
1616
|
const startValue = this._incrementFields.get(field);
|
|
1563
|
-
const
|
|
1617
|
+
const seq = (await counters.findOneAndUpdate({ _id: counterId }, { $inc: { seq: count } }, {
|
|
1564
1618
|
upsert: true,
|
|
1565
1619
|
returnDocument: "after"
|
|
1566
|
-
});
|
|
1567
|
-
const seq = doc?.seq ?? count;
|
|
1620
|
+
}))?.seq ?? count;
|
|
1568
1621
|
if (seq === count) {
|
|
1569
1622
|
const currentMax = await this._getCurrentFieldMax(field);
|
|
1570
1623
|
const minStart = typeof startValue === "number" ? startValue : 1;
|
|
@@ -1580,7 +1633,8 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1580
1633
|
}
|
|
1581
1634
|
return result;
|
|
1582
1635
|
}
|
|
1583
|
-
/** Reads current max value for a single field via $group aggregation. */
|
|
1636
|
+
/** Reads current max value for a single field via $group aggregation. */
|
|
1637
|
+
async _getCurrentFieldMax(field) {
|
|
1584
1638
|
const alias = `max__${field.replace(/\./g, "__")}`;
|
|
1585
1639
|
const agg = await this.collection.aggregate([{ $group: {
|
|
1586
1640
|
_id: null,
|
|
@@ -1592,19 +1646,20 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1592
1646
|
}
|
|
1593
1647
|
return 0;
|
|
1594
1648
|
}
|
|
1595
|
-
/** Allocates increment values for a batch of items, assigning in order. */
|
|
1596
|
-
|
|
1649
|
+
/** Allocates increment values for a batch of items, assigning in order. */
|
|
1650
|
+
async _assignBatchIncrements(data, allFields) {
|
|
1651
|
+
const fieldCounts = /* @__PURE__ */ new Map();
|
|
1597
1652
|
for (const physical of allFields) {
|
|
1598
1653
|
let count = 0;
|
|
1599
|
-
for (const item of data) if (item[physical] ===
|
|
1654
|
+
for (const item of data) if (item[physical] === void 0 || item[physical] === null) count++;
|
|
1600
1655
|
if (count > 0) fieldCounts.set(physical, count);
|
|
1601
1656
|
}
|
|
1602
|
-
const fieldCounters = new Map();
|
|
1657
|
+
const fieldCounters = /* @__PURE__ */ new Map();
|
|
1603
1658
|
for (const [physical, count] of fieldCounts) {
|
|
1604
1659
|
const allocated = await this._allocateIncrementValues([physical], count);
|
|
1605
1660
|
fieldCounters.set(physical, allocated.get(physical) ?? 1);
|
|
1606
1661
|
}
|
|
1607
|
-
for (const item of data) for (const physical of allFields) if (item[physical] ===
|
|
1662
|
+
for (const item of data) for (const physical of allFields) if (item[physical] === void 0 || item[physical] === null) {
|
|
1608
1663
|
const next = fieldCounters.get(physical) ?? 1;
|
|
1609
1664
|
item[physical] = next;
|
|
1610
1665
|
fieldCounters.set(physical, next + 1);
|
|
@@ -1623,8 +1678,9 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1623
1678
|
* Returns MongoDB collation options if any filter field has a non-binary collation.
|
|
1624
1679
|
* Uses pre-computed insights when available, falls back to computing them on demand.
|
|
1625
1680
|
* Maps: nocase → strength 2 (case-insensitive), unicode → strength 1 (case+accent-insensitive).
|
|
1626
|
-
*/
|
|
1627
|
-
|
|
1681
|
+
*/
|
|
1682
|
+
_getCollationOpts(query) {
|
|
1683
|
+
if (!this._collateFields) return;
|
|
1628
1684
|
const insights = query.insights ?? computeInsights(query.filter);
|
|
1629
1685
|
let strength;
|
|
1630
1686
|
for (const field of insights.keys()) {
|
|
@@ -1638,14 +1694,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1638
1694
|
return strength ? { collation: {
|
|
1639
1695
|
locale: "en",
|
|
1640
1696
|
strength
|
|
1641
|
-
} } :
|
|
1697
|
+
} } : void 0;
|
|
1642
1698
|
}
|
|
1643
1699
|
_addMongoIndexField(type, name, field, weight) {
|
|
1644
1700
|
const key = mongoIndexKey(type, name);
|
|
1645
1701
|
let index = this._mongoIndexes.get(key);
|
|
1646
1702
|
const value = type === "text" ? "text" : 1;
|
|
1647
1703
|
if (index) index.fields[field] = value;
|
|
1648
|
-
else {
|
|
1704
|
+
else {
|
|
1649
1705
|
index = {
|
|
1650
1706
|
key,
|
|
1651
1707
|
name,
|
|
@@ -1658,16 +1714,16 @@ else {
|
|
|
1658
1714
|
if (weight) index.weights[field] = weight;
|
|
1659
1715
|
}
|
|
1660
1716
|
_setSearchIndex(type, name, definition) {
|
|
1661
|
-
const key = mongoIndexKey(type, name ||
|
|
1717
|
+
const key = mongoIndexKey(type, name || "DEFAULT");
|
|
1662
1718
|
this._mongoIndexes.set(key, {
|
|
1663
1719
|
key,
|
|
1664
|
-
name: name ||
|
|
1720
|
+
name: name || "DEFAULT",
|
|
1665
1721
|
type,
|
|
1666
1722
|
definition
|
|
1667
1723
|
});
|
|
1668
1724
|
}
|
|
1669
1725
|
_addFieldToSearchIndex(type, _name, fieldName, analyzer) {
|
|
1670
|
-
const name = _name ||
|
|
1726
|
+
const name = _name || "DEFAULT";
|
|
1671
1727
|
let index = this._mongoIndexes.get(mongoIndexKey(type, name));
|
|
1672
1728
|
if (!index && type === "search_text") {
|
|
1673
1729
|
this._setSearchIndex(type, name, {
|
|
@@ -1681,23 +1737,30 @@ else {
|
|
|
1681
1737
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
1682
1738
|
}
|
|
1683
1739
|
}
|
|
1684
|
-
constructor(db, client) {
|
|
1685
|
-
super(), _define_property(this, "db", void 0), _define_property(this, "client", void 0), _define_property(this, "_collection", void 0), _define_property(this, "_mongoIndexes", void 0), _define_property(this, "_vectorFilters", void 0), _define_property(this, "_vectorThresholds", void 0), _define_property(this, "_searchIndexesMap", void 0), _define_property(this, "_incrementFields", void 0), _define_property(this, "_collateFields", void 0), _define_property(this, "_cappedOptions", void 0), _define_property(this, "_hasExplicitId", void 0), _define_property(this, "_pendingUniqueFields", void 0), this.db = db, this.client = client, this._mongoIndexes = new Map(), this._vectorFilters = new Map(), this._vectorThresholds = new Map(), this._incrementFields = new Map(), this._hasExplicitId = false, this._pendingUniqueFields = [];
|
|
1686
|
-
}
|
|
1687
1740
|
};
|
|
1688
1741
|
/**
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1742
|
+
* Builds a MongoDB update document from a data object that may contain
|
|
1743
|
+
* field ops (`{ $inc: N }`, `{ $dec: N }`, `{ $mul: N }`).
|
|
1744
|
+
* Regular fields go into `$set`, ops go into `$inc` / `$mul`.
|
|
1745
|
+
*/
|
|
1746
|
+
function buildMongoUpdateDoc(data, ops) {
|
|
1747
|
+
const updateDoc = {};
|
|
1748
|
+
let hasData = false;
|
|
1749
|
+
for (const _ in data) {
|
|
1750
|
+
hasData = true;
|
|
1751
|
+
break;
|
|
1752
|
+
}
|
|
1753
|
+
if (hasData) updateDoc.$set = data;
|
|
1754
|
+
if (ops?.inc) updateDoc.$inc = ops.inc;
|
|
1755
|
+
if (ops?.mul) updateDoc.$mul = ops.mul;
|
|
1756
|
+
return updateDoc;
|
|
1757
|
+
}
|
|
1694
1758
|
//#endregion
|
|
1695
|
-
//#region
|
|
1759
|
+
//#region src/lib/index.ts
|
|
1696
1760
|
function createAdapter(connection, _options) {
|
|
1697
1761
|
const client = new MongoClient(connection);
|
|
1698
1762
|
const db = client.db();
|
|
1699
1763
|
return new DbSpace(() => new MongoAdapter(db, client));
|
|
1700
1764
|
}
|
|
1701
|
-
|
|
1702
1765
|
//#endregion
|
|
1703
|
-
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|
|
1766
|
+
export { CollectionPatcher, MongoAdapter, buildMongoFilter, createAdapter, validateMongoIdPlugin };
|