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