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