@atscript/db-mongo 0.1.163 → 0.1.165
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +72 -42
- package/dist/index.d.cts +7 -9
- package/dist/index.d.mts +7 -9
- package/dist/index.mjs +73 -43
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -1025,21 +1025,36 @@ function filterOptionsOf(host) {
|
|
|
1025
1025
|
return { collation: require_mongo_accumulator.collationOfAdapter(host) };
|
|
1026
1026
|
}
|
|
1027
1027
|
/**
|
|
1028
|
-
* Returns available search indexes as generic metadata for UI.
|
|
1029
|
-
*
|
|
1030
|
-
* with when none is named)
|
|
1028
|
+
* Returns available search indexes as generic metadata for UI. Each index is
|
|
1029
|
+
* listed once, by its declared name; the text default (the index `search()`
|
|
1030
|
+
* answers with when none is named) carries `isDefault` and lists `DEFAULT` —
|
|
1031
|
+
* which also resolves — as an alias (since 0.1.165). Only an index without a
|
|
1032
|
+
* name of its own (a dynamic Atlas mapping) is listed as `DEFAULT`. The vector
|
|
1033
|
+
* default is the first vector index.
|
|
1031
1034
|
*/
|
|
1035
|
+
/** Classic `$text` relevance order: `textScore` descending, `_id` breaking ties (since 0.1.165). */
|
|
1036
|
+
const RELEVANCE_SORT = { $sort: {
|
|
1037
|
+
_score: -1,
|
|
1038
|
+
_id: 1
|
|
1039
|
+
} };
|
|
1032
1040
|
function getSearchIndexesImpl(host) {
|
|
1033
1041
|
const result = [];
|
|
1042
|
+
const indexes = host.getMongoSearchIndexes();
|
|
1043
|
+
const textDefault = indexes.get(DEFAULT_INDEX_NAME);
|
|
1044
|
+
let defaultListedByName = false;
|
|
1045
|
+
for (const [name, index] of indexes) if (index === textDefault && name !== "DEFAULT") defaultListedByName = true;
|
|
1034
1046
|
let firstVector = true;
|
|
1035
|
-
for (const [name, index] of
|
|
1047
|
+
for (const [name, index] of indexes) {
|
|
1036
1048
|
const isVector = index.type === "vector";
|
|
1049
|
+
const aliased = !isVector && index === textDefault && name !== "DEFAULT";
|
|
1050
|
+
if (name === "DEFAULT" && defaultListedByName) continue;
|
|
1037
1051
|
result.push({
|
|
1038
1052
|
name,
|
|
1053
|
+
...aliased ? { aliases: [DEFAULT_INDEX_NAME] } : {},
|
|
1039
1054
|
description: `${index.type} index`,
|
|
1040
1055
|
type: isVector ? "vector" : "text",
|
|
1041
1056
|
fields: searchIndexPaths(host, index),
|
|
1042
|
-
isDefault: isVector ? firstVector :
|
|
1057
|
+
isDefault: isVector ? firstVector : index === textDefault
|
|
1043
1058
|
});
|
|
1044
1059
|
if (isVector) firstVector = false;
|
|
1045
1060
|
}
|
|
@@ -1514,7 +1529,7 @@ async function runSearchPipeline(host, stage, query, label, { threshold, classic
|
|
|
1514
1529
|
pipeline.push(...filterStages ?? require_mongo_accumulator.mongoFilterStages(query.filter, filterOptionsOf(host)));
|
|
1515
1530
|
const { stages: sorted, cleanup } = require_mongo_accumulator.sortStages(controls);
|
|
1516
1531
|
if (sorted.length > 0) pipeline.push(...sorted);
|
|
1517
|
-
else if (classicText) pipeline.push(
|
|
1532
|
+
else if (classicText) pipeline.push(RELEVANCE_SORT);
|
|
1518
1533
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
1519
1534
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
1520
1535
|
else pipeline.push({ $limit: 1e3 });
|
|
@@ -1544,7 +1559,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, { threshold
|
|
|
1544
1559
|
preStages.push({ $match: { _score: { $gte: threshold } } });
|
|
1545
1560
|
} else if (classicText) preStages.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
1546
1561
|
const { stages: dataStages, cleanup } = require_mongo_accumulator.sortStages(controls);
|
|
1547
|
-
if (dataStages.length === 0 && classicText) dataStages.push(
|
|
1562
|
+
if (dataStages.length === 0 && classicText) dataStages.push(RELEVANCE_SORT);
|
|
1548
1563
|
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
1549
1564
|
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
1550
1565
|
if (cleanup) dataStages.push(cleanup);
|
|
@@ -2212,6 +2227,37 @@ const validateMongoIdPlugin = (ctx, def, value) => {
|
|
|
2212
2227
|
}
|
|
2213
2228
|
};
|
|
2214
2229
|
//#endregion
|
|
2230
|
+
//#region src/lib/mongo-transaction-support.ts
|
|
2231
|
+
const verdicts = /* @__PURE__ */ new WeakMap();
|
|
2232
|
+
/** Whether the client's server runs transactions, asked once per client; a failed probe is retried. */
|
|
2233
|
+
function transactionSupport(client) {
|
|
2234
|
+
let verdict = verdicts.get(client);
|
|
2235
|
+
if (!verdict) {
|
|
2236
|
+
verdict = probe(client).catch((error) => {
|
|
2237
|
+
verdicts.delete(client);
|
|
2238
|
+
throw error;
|
|
2239
|
+
});
|
|
2240
|
+
verdicts.set(client, verdict);
|
|
2241
|
+
}
|
|
2242
|
+
return verdict;
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* A replica-set member reports `setName` (an uninitiated `--replSet` member
|
|
2246
|
+
* `isreplicaset: true`), a mongos — also behind a load balancer —
|
|
2247
|
+
* `msg: 'isdbgrid'`. Anything else is a standalone `mongod`.
|
|
2248
|
+
*/
|
|
2249
|
+
async function probe(client) {
|
|
2250
|
+
const admin = client.db("admin");
|
|
2251
|
+
let reply;
|
|
2252
|
+
try {
|
|
2253
|
+
reply = await admin.command({ hello: 1 });
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
if (!(error instanceof mongodb.MongoServerError) || error.code !== 59) throw error;
|
|
2256
|
+
reply = await admin.command({ isMaster: 1 });
|
|
2257
|
+
}
|
|
2258
|
+
return reply.setName != null || reply.isreplicaset === true || reply.msg === "isdbgrid";
|
|
2259
|
+
}
|
|
2260
|
+
//#endregion
|
|
2215
2261
|
//#region src/lib/mongo-adapter.ts
|
|
2216
2262
|
/** Mirrors the `mongo.objectId` primitive's pattern — only definite hex ids convert. */
|
|
2217
2263
|
const OBJECT_ID_HEX = /^[a-fA-F0-9]{24}$/;
|
|
@@ -2297,48 +2343,30 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2297
2343
|
return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
|
|
2298
2344
|
}
|
|
2299
2345
|
/**
|
|
2300
|
-
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
2301
|
-
* Shared across all adapter instances for the same client so topology is probed once.
|
|
2302
|
-
*/
|
|
2303
|
-
static _txDisabledClients = /* @__PURE__ */ new WeakSet();
|
|
2304
|
-
get _txDisabled() {
|
|
2305
|
-
return this.client ? MongoAdapter._txDisabledClients.has(this.client) : true;
|
|
2306
|
-
}
|
|
2307
|
-
set _txDisabled(value) {
|
|
2308
|
-
if (value && this.client) MongoAdapter._txDisabledClients.add(this.client);
|
|
2309
|
-
}
|
|
2310
|
-
/**
|
|
2311
2346
|
* Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
|
|
2312
2347
|
* This handles txnNumber management and automatic retry for
|
|
2313
2348
|
* `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
2314
2349
|
*
|
|
2350
|
+
* Transaction support is read from the server, not the connection mode: a
|
|
2351
|
+
* `directConnection=true` client to a replica-set primary runs real
|
|
2352
|
+
* transactions; a standalone `mongod` (or an adapter without a client)
|
|
2353
|
+
* runs `fn` without one.
|
|
2354
|
+
*
|
|
2315
2355
|
* `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
|
|
2316
2356
|
* snapshot, which satisfies `'read committed'` and `'repeatable read'`;
|
|
2317
2357
|
* `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
|
|
2318
|
-
* allows write skew), and so does any level
|
|
2319
|
-
*
|
|
2358
|
+
* allows write skew), and so does any level where `fn` would run without
|
|
2359
|
+
* a transaction.
|
|
2320
2360
|
*/
|
|
2321
2361
|
async withTransaction(fn, opts) {
|
|
2322
2362
|
const isolation = this._checkIsolation(opts);
|
|
2323
2363
|
if (this._getTransactionState()) return fn();
|
|
2324
|
-
const passThrough = () => {
|
|
2325
|
-
if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction —
|
|
2364
|
+
const passThrough = (reason) => {
|
|
2365
|
+
if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — ${reason}`);
|
|
2326
2366
|
return fn();
|
|
2327
2367
|
};
|
|
2328
|
-
if (
|
|
2329
|
-
|
|
2330
|
-
const topology = this._client.topology;
|
|
2331
|
-
if (topology) {
|
|
2332
|
-
const type = (topology.description ?? topology.s?.description)?.type;
|
|
2333
|
-
if (type === "Single" || type === "Unknown") {
|
|
2334
|
-
this._txDisabled = true;
|
|
2335
|
-
return passThrough();
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
} catch {
|
|
2339
|
-
this._txDisabled = true;
|
|
2340
|
-
return passThrough();
|
|
2341
|
-
}
|
|
2368
|
+
if (!this._client) return passThrough("the adapter was created without a MongoClient");
|
|
2369
|
+
if (!await transactionSupport(this._client)) return passThrough("the server is a standalone mongod (transactions need a replica set or mongos)");
|
|
2342
2370
|
const session = this._client.startSession();
|
|
2343
2371
|
try {
|
|
2344
2372
|
let result;
|
|
@@ -2778,7 +2806,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2778
2806
|
weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
|
|
2779
2807
|
};
|
|
2780
2808
|
if (index === defaultFulltext) defaultIndex = converted;
|
|
2781
|
-
|
|
2809
|
+
if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
|
|
2782
2810
|
}
|
|
2783
2811
|
for (const index of this._mongoIndexes.values()) switch (index.type) {
|
|
2784
2812
|
case "text":
|
|
@@ -3014,10 +3042,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
3014
3042
|
}
|
|
3015
3043
|
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
3016
3044
|
_mapConstraintError(error) {
|
|
3017
|
-
if (error instanceof mongodb.MongoServerError && error.code === 11e3) throw
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}
|
|
3045
|
+
if (error instanceof mongodb.MongoServerError && error.code === 11e3) throw (0, _atscript_db.constraintViolation)("CONFLICT", {
|
|
3046
|
+
columns: error.keyPattern ? Object.keys(error.keyPattern) : void 0,
|
|
3047
|
+
name: error.message.match(/index: (\S+) dup key/)?.[1]
|
|
3048
|
+
}, error.message);
|
|
3021
3049
|
throw error;
|
|
3022
3050
|
}
|
|
3023
3051
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
@@ -3213,11 +3241,13 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
3213
3241
|
};
|
|
3214
3242
|
const versionColumn = this._table.versionColumnPhysical;
|
|
3215
3243
|
const counters = this._table.onUpdateIncPhysical;
|
|
3244
|
+
const keep = this._table.insertOnlyPhysical;
|
|
3216
3245
|
this._log("replaceOne", mongoFilter, data);
|
|
3217
|
-
if (versionColumn !== void 0 || counters.length > 0) {
|
|
3246
|
+
if (versionColumn !== void 0 || counters.length > 0 || keep.length > 0) {
|
|
3218
3247
|
const replacement = {};
|
|
3219
3248
|
for (const [key, value] of Object.entries(data)) replacement[key] = { $literal: value };
|
|
3220
3249
|
for (const col of counters) replacement[col] = incExpr(col, 1);
|
|
3250
|
+
for (const col of keep) replacement[col] = `$${col}`;
|
|
3221
3251
|
if (versionColumn !== void 0) replacement[versionColumn] = { $add: [`$${versionColumn}`, 1] };
|
|
3222
3252
|
const pipeline = [{ $replaceWith: replacement }];
|
|
3223
3253
|
return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, pipeline, this._getSessionOpts()));
|
package/dist/index.d.cts
CHANGED
|
@@ -318,23 +318,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
318
318
|
* client may serve several (since 0.1.147).
|
|
319
319
|
*/
|
|
320
320
|
sharesStoreWith(other: BaseDbAdapter): boolean;
|
|
321
|
-
/**
|
|
322
|
-
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
323
|
-
* Shared across all adapter instances for the same client so topology is probed once.
|
|
324
|
-
*/
|
|
325
|
-
private static readonly _txDisabledClients;
|
|
326
|
-
private get _txDisabled();
|
|
327
|
-
private set _txDisabled(value);
|
|
328
321
|
/**
|
|
329
322
|
* Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
|
|
330
323
|
* This handles txnNumber management and automatic retry for
|
|
331
324
|
* `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
332
325
|
*
|
|
326
|
+
* Transaction support is read from the server, not the connection mode: a
|
|
327
|
+
* `directConnection=true` client to a replica-set primary runs real
|
|
328
|
+
* transactions; a standalone `mongod` (or an adapter without a client)
|
|
329
|
+
* runs `fn` without one.
|
|
330
|
+
*
|
|
333
331
|
* `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
|
|
334
332
|
* snapshot, which satisfies `'read committed'` and `'repeatable read'`;
|
|
335
333
|
* `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
|
|
336
|
-
* allows write skew), and so does any level
|
|
337
|
-
*
|
|
334
|
+
* allows write skew), and so does any level where `fn` would run without
|
|
335
|
+
* a transaction.
|
|
338
336
|
*/
|
|
339
337
|
withTransaction<T>(fn: () => Promise<T>, opts?: TTransactionOptions): Promise<T>;
|
|
340
338
|
private static readonly _ISOLATION_LEVELS;
|
package/dist/index.d.mts
CHANGED
|
@@ -318,23 +318,21 @@ declare class MongoAdapter extends BaseDbAdapter {
|
|
|
318
318
|
* client may serve several (since 0.1.147).
|
|
319
319
|
*/
|
|
320
320
|
sharesStoreWith(other: BaseDbAdapter): boolean;
|
|
321
|
-
/**
|
|
322
|
-
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
323
|
-
* Shared across all adapter instances for the same client so topology is probed once.
|
|
324
|
-
*/
|
|
325
|
-
private static readonly _txDisabledClients;
|
|
326
|
-
private get _txDisabled();
|
|
327
|
-
private set _txDisabled(value);
|
|
328
321
|
/**
|
|
329
322
|
* Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
|
|
330
323
|
* This handles txnNumber management and automatic retry for
|
|
331
324
|
* `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
332
325
|
*
|
|
326
|
+
* Transaction support is read from the server, not the connection mode: a
|
|
327
|
+
* `directConnection=true` client to a replica-set primary runs real
|
|
328
|
+
* transactions; a standalone `mongod` (or an adapter without a client)
|
|
329
|
+
* runs `fn` without one.
|
|
330
|
+
*
|
|
333
331
|
* `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
|
|
334
332
|
* snapshot, which satisfies `'read committed'` and `'repeatable read'`;
|
|
335
333
|
* `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
|
|
336
|
-
* allows write skew), and so does any level
|
|
337
|
-
*
|
|
334
|
+
* allows write skew), and so does any level where `fn` would run without
|
|
335
|
+
* a transaction.
|
|
338
336
|
*/
|
|
339
337
|
withTransaction<T>(fn: () => Promise<T>, opts?: TTransactionOptions): Promise<T>;
|
|
340
338
|
private static readonly _ISOLATION_LEVELS;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as buildMongoQuery, c as planStages, f as sortStages, g as queryNodeToExpr, h as orNull, i as buildMongoFilter, l as correlate, n as distinctCountExpr, o as collationOfAdapter, p as exprToMongo, r as REL_FILTER_TEMP_PREFIX, s as mongoFilterStages, t as buildAccumulator, u as hasNullsPlacement } from "./mongo-accumulator-DMPFOscc.mjs";
|
|
2
2
|
import { t as dynamicSearchEncryptedMessage } from "./search-messages-mQSpJ4GD.mjs";
|
|
3
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, containsRelationFilter, createChangeRecorder, createFailureCollector, defaultFulltextIndex, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, isAtscriptDbView, isFieldRef, isPlainObject, isViewType, queryReadColumns, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, searchTermInteger, tableNameOf, translateQueryTree, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
3
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, andFilters, bucketTimeZoneUnavailable, computeInsights, constraintViolation, containsRelationFilter, createChangeRecorder, createFailureCollector, defaultFulltextIndex, geoIndexNotFoundMessage, getDbFieldOp, getKeyProps, getPath, isAtscriptDbView, isFieldRef, isPlainObject, isViewType, queryReadColumns, relationStaticFilter, resolveDesignType, searchIndexNotFoundMessage, searchTermInteger, tableNameOf, translateQueryTree, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
4
4
|
import { MongoBulkWriteError, MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
5
5
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
6
6
|
//#region src/lib/path-utils.ts
|
|
@@ -1024,21 +1024,36 @@ function filterOptionsOf(host) {
|
|
|
1024
1024
|
return { collation: collationOfAdapter(host) };
|
|
1025
1025
|
}
|
|
1026
1026
|
/**
|
|
1027
|
-
* Returns available search indexes as generic metadata for UI.
|
|
1028
|
-
*
|
|
1029
|
-
* with when none is named)
|
|
1027
|
+
* Returns available search indexes as generic metadata for UI. Each index is
|
|
1028
|
+
* listed once, by its declared name; the text default (the index `search()`
|
|
1029
|
+
* answers with when none is named) carries `isDefault` and lists `DEFAULT` —
|
|
1030
|
+
* which also resolves — as an alias (since 0.1.165). Only an index without a
|
|
1031
|
+
* name of its own (a dynamic Atlas mapping) is listed as `DEFAULT`. The vector
|
|
1032
|
+
* default is the first vector index.
|
|
1030
1033
|
*/
|
|
1034
|
+
/** Classic `$text` relevance order: `textScore` descending, `_id` breaking ties (since 0.1.165). */
|
|
1035
|
+
const RELEVANCE_SORT = { $sort: {
|
|
1036
|
+
_score: -1,
|
|
1037
|
+
_id: 1
|
|
1038
|
+
} };
|
|
1031
1039
|
function getSearchIndexesImpl(host) {
|
|
1032
1040
|
const result = [];
|
|
1041
|
+
const indexes = host.getMongoSearchIndexes();
|
|
1042
|
+
const textDefault = indexes.get(DEFAULT_INDEX_NAME);
|
|
1043
|
+
let defaultListedByName = false;
|
|
1044
|
+
for (const [name, index] of indexes) if (index === textDefault && name !== "DEFAULT") defaultListedByName = true;
|
|
1033
1045
|
let firstVector = true;
|
|
1034
|
-
for (const [name, index] of
|
|
1046
|
+
for (const [name, index] of indexes) {
|
|
1035
1047
|
const isVector = index.type === "vector";
|
|
1048
|
+
const aliased = !isVector && index === textDefault && name !== "DEFAULT";
|
|
1049
|
+
if (name === "DEFAULT" && defaultListedByName) continue;
|
|
1036
1050
|
result.push({
|
|
1037
1051
|
name,
|
|
1052
|
+
...aliased ? { aliases: [DEFAULT_INDEX_NAME] } : {},
|
|
1038
1053
|
description: `${index.type} index`,
|
|
1039
1054
|
type: isVector ? "vector" : "text",
|
|
1040
1055
|
fields: searchIndexPaths(host, index),
|
|
1041
|
-
isDefault: isVector ? firstVector :
|
|
1056
|
+
isDefault: isVector ? firstVector : index === textDefault
|
|
1042
1057
|
});
|
|
1043
1058
|
if (isVector) firstVector = false;
|
|
1044
1059
|
}
|
|
@@ -1513,7 +1528,7 @@ async function runSearchPipeline(host, stage, query, label, { threshold, classic
|
|
|
1513
1528
|
pipeline.push(...filterStages ?? mongoFilterStages(query.filter, filterOptionsOf(host)));
|
|
1514
1529
|
const { stages: sorted, cleanup } = sortStages(controls);
|
|
1515
1530
|
if (sorted.length > 0) pipeline.push(...sorted);
|
|
1516
|
-
else if (classicText) pipeline.push(
|
|
1531
|
+
else if (classicText) pipeline.push(RELEVANCE_SORT);
|
|
1517
1532
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
1518
1533
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
1519
1534
|
else pipeline.push({ $limit: 1e3 });
|
|
@@ -1543,7 +1558,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, { threshold
|
|
|
1543
1558
|
preStages.push({ $match: { _score: { $gte: threshold } } });
|
|
1544
1559
|
} else if (classicText) preStages.push({ $addFields: { _score: { $meta: "textScore" } } });
|
|
1545
1560
|
const { stages: dataStages, cleanup } = sortStages(controls);
|
|
1546
|
-
if (dataStages.length === 0 && classicText) dataStages.push(
|
|
1561
|
+
if (dataStages.length === 0 && classicText) dataStages.push(RELEVANCE_SORT);
|
|
1547
1562
|
if (controls.$skip) dataStages.push({ $skip: controls.$skip });
|
|
1548
1563
|
if (controls.$limit) dataStages.push({ $limit: controls.$limit });
|
|
1549
1564
|
if (cleanup) dataStages.push(cleanup);
|
|
@@ -2211,6 +2226,37 @@ const validateMongoIdPlugin = (ctx, def, value) => {
|
|
|
2211
2226
|
}
|
|
2212
2227
|
};
|
|
2213
2228
|
//#endregion
|
|
2229
|
+
//#region src/lib/mongo-transaction-support.ts
|
|
2230
|
+
const verdicts = /* @__PURE__ */ new WeakMap();
|
|
2231
|
+
/** Whether the client's server runs transactions, asked once per client; a failed probe is retried. */
|
|
2232
|
+
function transactionSupport(client) {
|
|
2233
|
+
let verdict = verdicts.get(client);
|
|
2234
|
+
if (!verdict) {
|
|
2235
|
+
verdict = probe(client).catch((error) => {
|
|
2236
|
+
verdicts.delete(client);
|
|
2237
|
+
throw error;
|
|
2238
|
+
});
|
|
2239
|
+
verdicts.set(client, verdict);
|
|
2240
|
+
}
|
|
2241
|
+
return verdict;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2244
|
+
* A replica-set member reports `setName` (an uninitiated `--replSet` member
|
|
2245
|
+
* `isreplicaset: true`), a mongos — also behind a load balancer —
|
|
2246
|
+
* `msg: 'isdbgrid'`. Anything else is a standalone `mongod`.
|
|
2247
|
+
*/
|
|
2248
|
+
async function probe(client) {
|
|
2249
|
+
const admin = client.db("admin");
|
|
2250
|
+
let reply;
|
|
2251
|
+
try {
|
|
2252
|
+
reply = await admin.command({ hello: 1 });
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
if (!(error instanceof MongoServerError) || error.code !== 59) throw error;
|
|
2255
|
+
reply = await admin.command({ isMaster: 1 });
|
|
2256
|
+
}
|
|
2257
|
+
return reply.setName != null || reply.isreplicaset === true || reply.msg === "isdbgrid";
|
|
2258
|
+
}
|
|
2259
|
+
//#endregion
|
|
2214
2260
|
//#region src/lib/mongo-adapter.ts
|
|
2215
2261
|
/** Mirrors the `mongo.objectId` primitive's pattern — only definite hex ids convert. */
|
|
2216
2262
|
const OBJECT_ID_HEX = /^[a-fA-F0-9]{24}$/;
|
|
@@ -2296,48 +2342,30 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2296
2342
|
return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
|
|
2297
2343
|
}
|
|
2298
2344
|
/**
|
|
2299
|
-
* Per-client cache: whether transactions are unavailable (standalone MongoDB).
|
|
2300
|
-
* Shared across all adapter instances for the same client so topology is probed once.
|
|
2301
|
-
*/
|
|
2302
|
-
static _txDisabledClients = /* @__PURE__ */ new WeakSet();
|
|
2303
|
-
get _txDisabled() {
|
|
2304
|
-
return this.client ? MongoAdapter._txDisabledClients.has(this.client) : true;
|
|
2305
|
-
}
|
|
2306
|
-
set _txDisabled(value) {
|
|
2307
|
-
if (value && this.client) MongoAdapter._txDisabledClients.add(this.client);
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
2345
|
* Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
|
|
2311
2346
|
* This handles txnNumber management and automatic retry for
|
|
2312
2347
|
* `TransientTransactionError` / `UnknownTransactionCommitResult`.
|
|
2313
2348
|
*
|
|
2349
|
+
* Transaction support is read from the server, not the connection mode: a
|
|
2350
|
+
* `directConnection=true` client to a replica-set primary runs real
|
|
2351
|
+
* transactions; a standalone `mongod` (or an adapter without a client)
|
|
2352
|
+
* runs `fn` without one.
|
|
2353
|
+
*
|
|
2314
2354
|
* `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
|
|
2315
2355
|
* snapshot, which satisfies `'read committed'` and `'repeatable read'`;
|
|
2316
2356
|
* `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
|
|
2317
|
-
* allows write skew), and so does any level
|
|
2318
|
-
*
|
|
2357
|
+
* allows write skew), and so does any level where `fn` would run without
|
|
2358
|
+
* a transaction.
|
|
2319
2359
|
*/
|
|
2320
2360
|
async withTransaction(fn, opts) {
|
|
2321
2361
|
const isolation = this._checkIsolation(opts);
|
|
2322
2362
|
if (this._getTransactionState()) return fn();
|
|
2323
|
-
const passThrough = () => {
|
|
2324
|
-
if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction —
|
|
2363
|
+
const passThrough = (reason) => {
|
|
2364
|
+
if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — ${reason}`);
|
|
2325
2365
|
return fn();
|
|
2326
2366
|
};
|
|
2327
|
-
if (
|
|
2328
|
-
|
|
2329
|
-
const topology = this._client.topology;
|
|
2330
|
-
if (topology) {
|
|
2331
|
-
const type = (topology.description ?? topology.s?.description)?.type;
|
|
2332
|
-
if (type === "Single" || type === "Unknown") {
|
|
2333
|
-
this._txDisabled = true;
|
|
2334
|
-
return passThrough();
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
} catch {
|
|
2338
|
-
this._txDisabled = true;
|
|
2339
|
-
return passThrough();
|
|
2340
|
-
}
|
|
2367
|
+
if (!this._client) return passThrough("the adapter was created without a MongoClient");
|
|
2368
|
+
if (!await transactionSupport(this._client)) return passThrough("the server is a standalone mongod (transactions need a replica set or mongos)");
|
|
2341
2369
|
const session = this._client.startSession();
|
|
2342
2370
|
try {
|
|
2343
2371
|
let result;
|
|
@@ -2777,7 +2805,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
2777
2805
|
weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
|
|
2778
2806
|
};
|
|
2779
2807
|
if (index === defaultFulltext) defaultIndex = converted;
|
|
2780
|
-
|
|
2808
|
+
if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
|
|
2781
2809
|
}
|
|
2782
2810
|
for (const index of this._mongoIndexes.values()) switch (index.type) {
|
|
2783
2811
|
case "text":
|
|
@@ -3013,10 +3041,10 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
3013
3041
|
}
|
|
3014
3042
|
/** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
|
|
3015
3043
|
_mapConstraintError(error) {
|
|
3016
|
-
if (error instanceof MongoServerError && error.code === 11e3) throw
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
}
|
|
3044
|
+
if (error instanceof MongoServerError && error.code === 11e3) throw constraintViolation("CONFLICT", {
|
|
3045
|
+
columns: error.keyPattern ? Object.keys(error.keyPattern) : void 0,
|
|
3046
|
+
name: error.message.match(/index: (\S+) dup key/)?.[1]
|
|
3047
|
+
}, error.message);
|
|
3020
3048
|
throw error;
|
|
3021
3049
|
}
|
|
3022
3050
|
/** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
|
|
@@ -3212,11 +3240,13 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
3212
3240
|
};
|
|
3213
3241
|
const versionColumn = this._table.versionColumnPhysical;
|
|
3214
3242
|
const counters = this._table.onUpdateIncPhysical;
|
|
3243
|
+
const keep = this._table.insertOnlyPhysical;
|
|
3215
3244
|
this._log("replaceOne", mongoFilter, data);
|
|
3216
|
-
if (versionColumn !== void 0 || counters.length > 0) {
|
|
3245
|
+
if (versionColumn !== void 0 || counters.length > 0 || keep.length > 0) {
|
|
3217
3246
|
const replacement = {};
|
|
3218
3247
|
for (const [key, value] of Object.entries(data)) replacement[key] = { $literal: value };
|
|
3219
3248
|
for (const col of counters) replacement[col] = incExpr(col, 1);
|
|
3249
|
+
for (const col of keep) replacement[col] = `$${col}`;
|
|
3220
3250
|
if (versionColumn !== void 0) replacement[versionColumn] = { $add: [`$${versionColumn}`, 1] };
|
|
3221
3251
|
const pipeline = [{ $replaceWith: replacement }];
|
|
3222
3252
|
return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, pipeline, this._getSessionOpts()));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.165",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -46,19 +46,19 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@atscript/core": "^0.1
|
|
50
|
-
"@atscript/typescript": "^0.1
|
|
49
|
+
"@atscript/core": "^0.2.1",
|
|
50
|
+
"@atscript/typescript": "^0.2.1",
|
|
51
51
|
"@uniqu/core": "^0.1.15",
|
|
52
52
|
"mongodb": "^6.17.0",
|
|
53
53
|
"mongodb-memory-server-core": "^10.0.0",
|
|
54
|
-
"unplugin-atscript": "^0.1
|
|
54
|
+
"unplugin-atscript": "^0.2.1"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@atscript/core": "^0.1
|
|
58
|
-
"@atscript/typescript": "^0.1
|
|
57
|
+
"@atscript/core": "^0.2.1",
|
|
58
|
+
"@atscript/typescript": "^0.2.1",
|
|
59
59
|
"@uniqu/core": "^0.1.15",
|
|
60
60
|
"mongodb": "^6.17.0",
|
|
61
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.165"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"build": "vp pack",
|