@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 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. The text
1029
- * default is the `DEFAULT` entry (an alias of the index `search()` answers
1030
- * with when none is named); the vector default is the first vector index.
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 host.getMongoSearchIndexes()) {
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 : name === DEFAULT_INDEX_NAME
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({ $sort: { _score: -1 } });
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({ $sort: { _score: -1 } });
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 on a topology without
2319
- * transactions (standalone), where `fn` would run without one.
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 — this topology (standalone) has none`);
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 (this._txDisabled || !this._client) return passThrough();
2329
- try {
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
- else if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
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 new _atscript_db.DbError("CONFLICT", [{
3018
- path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
3019
- message: error.message
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 on a topology without
337
- * transactions (standalone), where `fn` would run without one.
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 on a topology without
337
- * transactions (standalone), where `fn` would run without one.
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. The text
1028
- * default is the `DEFAULT` entry (an alias of the index `search()` answers
1029
- * with when none is named); the vector default is the first vector index.
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 host.getMongoSearchIndexes()) {
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 : name === DEFAULT_INDEX_NAME
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({ $sort: { _score: -1 } });
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({ $sort: { _score: -1 } });
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 on a topology without
2318
- * transactions (standalone), where `fn` would run without one.
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 — this topology (standalone) has none`);
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 (this._txDisabled || !this._client) return passThrough();
2328
- try {
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
- else if (!this._searchIndexesMap.has(index.name)) this._searchIndexesMap.set(index.name, converted);
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 new DbError("CONFLICT", [{
3017
- path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
3018
- message: error.message
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.163",
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.109",
50
- "@atscript/typescript": "^0.1.109",
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.109"
54
+ "unplugin-atscript": "^0.2.1"
55
55
  },
56
56
  "peerDependencies": {
57
- "@atscript/core": "^0.1.109",
58
- "@atscript/typescript": "^0.1.109",
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.163"
61
+ "@atscript/db": "^0.1.165"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "vp pack",