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