@atscript/db-mongo 0.1.164 → 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);
@@ -2791,7 +2806,7 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2791
2806
  weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
2792
2807
  };
2793
2808
  if (index === defaultFulltext) defaultIndex = converted;
2794
- 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);
2795
2810
  }
2796
2811
  for (const index of this._mongoIndexes.values()) switch (index.type) {
2797
2812
  case "text":
@@ -3027,10 +3042,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
3027
3042
  }
3028
3043
  /** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
3029
3044
  _mapConstraintError(error) {
3030
- if (error instanceof mongodb.MongoServerError && error.code === 11e3) throw new _atscript_db.DbError("CONFLICT", [{
3031
- path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
3032
- message: error.message
3033
- }]);
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);
3034
3049
  throw error;
3035
3050
  }
3036
3051
  /** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
@@ -3226,11 +3241,13 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
3226
3241
  };
3227
3242
  const versionColumn = this._table.versionColumnPhysical;
3228
3243
  const counters = this._table.onUpdateIncPhysical;
3244
+ const keep = this._table.insertOnlyPhysical;
3229
3245
  this._log("replaceOne", mongoFilter, data);
3230
- if (versionColumn !== void 0 || counters.length > 0) {
3246
+ if (versionColumn !== void 0 || counters.length > 0 || keep.length > 0) {
3231
3247
  const replacement = {};
3232
3248
  for (const [key, value] of Object.entries(data)) replacement[key] = { $literal: value };
3233
3249
  for (const col of counters) replacement[col] = incExpr(col, 1);
3250
+ for (const col of keep) replacement[col] = `$${col}`;
3234
3251
  if (versionColumn !== void 0) replacement[versionColumn] = { $add: [`$${versionColumn}`, 1] };
3235
3252
  const pipeline = [{ $replaceWith: replacement }];
3236
3253
  return this._wrapUpdate(() => this.collection.updateOne(mongoFilter, pipeline, this._getSessionOpts()));
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);
@@ -2790,7 +2805,7 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2790
2805
  weights: Object.fromEntries(index.fields.filter((f) => f.weight && !f.integer).map((f) => [f.name, f.weight]))
2791
2806
  };
2792
2807
  if (index === defaultFulltext) defaultIndex = converted;
2793
- 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);
2794
2809
  }
2795
2810
  for (const index of this._mongoIndexes.values()) switch (index.type) {
2796
2811
  case "text":
@@ -3026,10 +3041,10 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
3026
3041
  }
3027
3042
  /** Rethrows a duplicate-key (11000) server error as `CONFLICT`, anything else as is. */
3028
3043
  _mapConstraintError(error) {
3029
- if (error instanceof MongoServerError && error.code === 11e3) throw new DbError("CONFLICT", [{
3030
- path: error.keyPattern ? Object.keys(error.keyPattern)[0] ?? "" : "",
3031
- message: error.message
3032
- }]);
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);
3033
3048
  throw error;
3034
3049
  }
3035
3050
  /** An update-shaped write under {@link _wrapDuplicateKeyError}, reduced to `TDbUpdateResult`. */
@@ -3225,11 +3240,13 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
3225
3240
  };
3226
3241
  const versionColumn = this._table.versionColumnPhysical;
3227
3242
  const counters = this._table.onUpdateIncPhysical;
3243
+ const keep = this._table.insertOnlyPhysical;
3228
3244
  this._log("replaceOne", mongoFilter, data);
3229
- if (versionColumn !== void 0 || counters.length > 0) {
3245
+ if (versionColumn !== void 0 || counters.length > 0 || keep.length > 0) {
3230
3246
  const replacement = {};
3231
3247
  for (const [key, value] of Object.entries(data)) replacement[key] = { $literal: value };
3232
3248
  for (const col of counters) replacement[col] = incExpr(col, 1);
3249
+ for (const col of keep) replacement[col] = `$${col}`;
3233
3250
  if (versionColumn !== void 0) replacement[versionColumn] = { $add: [`$${versionColumn}`, 1] };
3234
3251
  const pipeline = [{ $replaceWith: replacement }];
3235
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.164",
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.164"
61
+ "@atscript/db": "^0.1.165"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "vp pack",