@atscript/db-mongo 0.1.89 → 0.1.91

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
@@ -2,8 +2,55 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_mongo_filter = require("./mongo-filter-AihWWQXp.cjs");
3
3
  let _atscript_db = require("@atscript/db");
4
4
  let mongodb = require("mongodb");
5
+ //#region src/lib/projection-dedupe.ts
6
+ function dedupeProjection(projection) {
7
+ const includeKeys = Object.keys(projection).filter((k) => projection[k] === 1);
8
+ if (includeKeys.length < 2) return projection;
9
+ const toRemove = /* @__PURE__ */ new Set();
10
+ for (const parent of includeKeys) {
11
+ const prefix = parent + ".";
12
+ for (const other of includeKeys) if (other !== parent && other.startsWith(prefix)) toRemove.add(other);
13
+ }
14
+ if (toRemove.size === 0) return projection;
15
+ const result = {};
16
+ for (const k of Object.keys(projection)) if (!toRemove.has(k)) result[k] = projection[k];
17
+ return result;
18
+ }
19
+ //#endregion
20
+ //#region src/lib/mongo-errors.ts
21
+ /**
22
+ * Maps MongoDB projection-validation errors (31249, 31254) to `DbError("INVALID_QUERY")`
23
+ * so moost-db's validation interceptor returns HTTP 400 instead of an opaque 500.
24
+ * These codes always indicate malformed client `$select`, not a server fault.
25
+ */
26
+ async function wrapInvalidQuery(fn) {
27
+ try {
28
+ return await fn();
29
+ } catch (error) {
30
+ if (error instanceof mongodb.MongoServerError && (error.code === 31249 || error.code === 31254)) throw new _atscript_db.DbError("INVALID_QUERY", [{
31
+ path: "$select",
32
+ message: error.message
33
+ }]);
34
+ throw error;
35
+ }
36
+ }
37
+ //#endregion
5
38
  //#region src/lib/collection-patcher.ts
6
39
  /**
40
+ * True when a user value contains a `$`-prefixed string or object key — the
41
+ * shapes aggregation `$set` would evaluate as field paths / operators instead
42
+ * of treating as literal data. Caller must wrap such values in `{ $literal }`.
43
+ */
44
+ function containsAggregationExpr(v) {
45
+ if (typeof v === "string") return v.startsWith("$");
46
+ if (Array.isArray(v)) return v.some(containsAggregationExpr);
47
+ if (v !== null && typeof v === "object") for (const k of Object.keys(v)) {
48
+ if (k.startsWith("$")) return true;
49
+ if (containsAggregationExpr(v[k])) return true;
50
+ }
51
+ return false;
52
+ }
53
+ /**
7
54
  * CollectionPatcher is a small helper that converts a *patch payload* produced
8
55
  * by Atscript into a shape that the official MongoDB driver understands – a
9
56
  * triple of `(filter, update, options)` to be fed to `collection.updateOne()`.
@@ -88,6 +135,7 @@ var CollectionPatcher = class {
88
135
  _setLeaf(key, value) {
89
136
  const fieldOp = (0, _atscript_db.getDbFieldOp)(value);
90
137
  if (fieldOp) this._set(key, this._fieldOpExpr(key, fieldOp.op, fieldOp.value));
138
+ else if (containsAggregationExpr(value)) this._set(key, { $literal: value });
91
139
  else this._set(key, value);
92
140
  }
93
141
  /**
@@ -492,7 +540,7 @@ function buildLookupInnerPipeline(withRel, requiredFields) {
492
540
  for (const f of select) projection[f] = 1;
493
541
  for (const f of requiredFields) projection[f] = 1;
494
542
  if (!select.includes("_id") && !requiredFields.includes("_id")) projection["_id"] = 0;
495
- pipeline.push({ $project: projection });
543
+ pipeline.push({ $project: dedupeProjection(projection) });
496
544
  }
497
545
  return pipeline;
498
546
  }
@@ -663,9 +711,12 @@ async function runSearchPipeline(host, stage, query, label, threshold) {
663
711
  if (controls.$skip) pipeline.push({ $skip: controls.$skip });
664
712
  if (controls.$limit) pipeline.push({ $limit: controls.$limit });
665
713
  else pipeline.push({ $limit: 1e3 });
666
- if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
714
+ if (controls.$select) {
715
+ const projection = controls.$select.asProjection;
716
+ if (projection) pipeline.push({ $project: dedupeProjection(projection) });
717
+ }
667
718
  host._log(`aggregate (${label})`, pipeline);
668
- return host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
719
+ return wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
669
720
  }
670
721
  /** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
671
722
  async function runSearchWithCountPipeline(host, stage, query, label, threshold) {
@@ -680,7 +731,10 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold)
680
731
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
681
732
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
682
733
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
683
- if (controls.$select) dataStages.push({ $project: controls.$select.asProjection });
734
+ if (controls.$select) {
735
+ const projection = controls.$select.asProjection;
736
+ if (projection) dataStages.push({ $project: dedupeProjection(projection) });
737
+ }
684
738
  const pipeline = [
685
739
  stage,
686
740
  ...preStages,
@@ -691,7 +745,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold)
691
745
  } }
692
746
  ];
693
747
  host._log(`aggregate (${label})`, pipeline);
694
- const result = await host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
748
+ const result = await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
695
749
  return {
696
750
  data: result[0]?.data || [],
697
751
  count: result[0]?.meta[0]?.count || 0
@@ -1252,12 +1306,12 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1252
1306
  if (query.controls?.$count) {
1253
1307
  const pipeline = buildCountPipeline(query);
1254
1308
  this._log("aggregate (count)", pipeline);
1255
- const result = await this.aggregatePipeline(pipeline).toArray();
1309
+ const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
1256
1310
  return result.length > 0 ? result : [{ count: 0 }];
1257
1311
  }
1258
1312
  const pipeline = buildAggregatePipeline(query);
1259
1313
  this._log("aggregate", pipeline);
1260
- return this.aggregatePipeline(pipeline).toArray();
1314
+ return wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
1261
1315
  }
1262
1316
  get idType() {
1263
1317
  const idProp = this._table.type.type.props.get("_id");
@@ -1497,16 +1551,19 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1497
1551
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
1498
1552
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
1499
1553
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
1500
- if (controls.$select) dataStages.push({ $project: controls.$select.asProjection });
1554
+ if (controls.$select) {
1555
+ const projection = controls.$select.asProjection;
1556
+ if (projection) dataStages.push({ $project: dedupeProjection(projection) });
1557
+ }
1501
1558
  const pipeline = [{ $match: filter }, { $facet: {
1502
1559
  data: dataStages,
1503
1560
  meta: [{ $count: "count" }]
1504
1561
  } }];
1505
1562
  this._log("aggregate (findManyWithCount)", pipeline);
1506
- const result = await this.collection.aggregate(pipeline, {
1563
+ const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline, {
1507
1564
  ...this._getCollationOpts(query),
1508
1565
  ...this._getSessionOpts()
1509
- }).toArray();
1566
+ }).toArray());
1510
1567
  return {
1511
1568
  data: result[0]?.data || [],
1512
1569
  count: result[0]?.meta[0]?.count || 0
@@ -1589,21 +1646,21 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1589
1646
  const filter = require_mongo_filter.buildMongoFilter(query.filter);
1590
1647
  const opts = this._buildFindOptions(query.controls);
1591
1648
  this._log("findOne", filter, opts);
1592
- return this.collection.findOne(filter, {
1649
+ return wrapInvalidQuery(() => this.collection.findOne(filter, {
1593
1650
  ...opts,
1594
1651
  ...this._getCollationOpts(query),
1595
1652
  ...this._getSessionOpts()
1596
- });
1653
+ }));
1597
1654
  }
1598
1655
  async findMany(query) {
1599
1656
  const filter = require_mongo_filter.buildMongoFilter(query.filter);
1600
1657
  const opts = this._buildFindOptions(query.controls);
1601
1658
  this._log("findMany", filter, opts);
1602
- return this.collection.find(filter, {
1659
+ return wrapInvalidQuery(() => this.collection.find(filter, {
1603
1660
  ...opts,
1604
1661
  ...this._getCollationOpts(query),
1605
1662
  ...this._getSessionOpts()
1606
- }).toArray();
1663
+ }).toArray());
1607
1664
  }
1608
1665
  async count(query) {
1609
1666
  const filter = require_mongo_filter.buildMongoFilter(query.filter);
@@ -1790,7 +1847,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1790
1847
  if (controls.$sort) opts.sort = controls.$sort;
1791
1848
  if (controls.$limit) opts.limit = controls.$limit;
1792
1849
  if (controls.$skip) opts.skip = controls.$skip;
1793
- if (controls.$select) opts.projection = controls.$select.asProjection;
1850
+ if (controls.$select) {
1851
+ const projection = controls.$select.asProjection;
1852
+ if (projection) opts.projection = dedupeProjection(projection);
1853
+ }
1794
1854
  return opts;
1795
1855
  }
1796
1856
  /**
package/dist/index.mjs CHANGED
@@ -1,8 +1,55 @@
1
1
  import { t as buildMongoFilter } from "./mongo-filter-BsocUQG3.mjs";
2
2
  import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace, computeInsights, getDbFieldOp, getKeyProps, resolveDesignType } from "@atscript/db";
3
3
  import { MongoClient, MongoServerError, ObjectId } from "mongodb";
4
+ //#region src/lib/projection-dedupe.ts
5
+ function dedupeProjection(projection) {
6
+ const includeKeys = Object.keys(projection).filter((k) => projection[k] === 1);
7
+ if (includeKeys.length < 2) return projection;
8
+ const toRemove = /* @__PURE__ */ new Set();
9
+ for (const parent of includeKeys) {
10
+ const prefix = parent + ".";
11
+ for (const other of includeKeys) if (other !== parent && other.startsWith(prefix)) toRemove.add(other);
12
+ }
13
+ if (toRemove.size === 0) return projection;
14
+ const result = {};
15
+ for (const k of Object.keys(projection)) if (!toRemove.has(k)) result[k] = projection[k];
16
+ return result;
17
+ }
18
+ //#endregion
19
+ //#region src/lib/mongo-errors.ts
20
+ /**
21
+ * Maps MongoDB projection-validation errors (31249, 31254) to `DbError("INVALID_QUERY")`
22
+ * so moost-db's validation interceptor returns HTTP 400 instead of an opaque 500.
23
+ * These codes always indicate malformed client `$select`, not a server fault.
24
+ */
25
+ async function wrapInvalidQuery(fn) {
26
+ try {
27
+ return await fn();
28
+ } catch (error) {
29
+ if (error instanceof MongoServerError && (error.code === 31249 || error.code === 31254)) throw new DbError("INVALID_QUERY", [{
30
+ path: "$select",
31
+ message: error.message
32
+ }]);
33
+ throw error;
34
+ }
35
+ }
36
+ //#endregion
4
37
  //#region src/lib/collection-patcher.ts
5
38
  /**
39
+ * True when a user value contains a `$`-prefixed string or object key — the
40
+ * shapes aggregation `$set` would evaluate as field paths / operators instead
41
+ * of treating as literal data. Caller must wrap such values in `{ $literal }`.
42
+ */
43
+ function containsAggregationExpr(v) {
44
+ if (typeof v === "string") return v.startsWith("$");
45
+ if (Array.isArray(v)) return v.some(containsAggregationExpr);
46
+ if (v !== null && typeof v === "object") for (const k of Object.keys(v)) {
47
+ if (k.startsWith("$")) return true;
48
+ if (containsAggregationExpr(v[k])) return true;
49
+ }
50
+ return false;
51
+ }
52
+ /**
6
53
  * CollectionPatcher is a small helper that converts a *patch payload* produced
7
54
  * by Atscript into a shape that the official MongoDB driver understands – a
8
55
  * triple of `(filter, update, options)` to be fed to `collection.updateOne()`.
@@ -87,6 +134,7 @@ var CollectionPatcher = class {
87
134
  _setLeaf(key, value) {
88
135
  const fieldOp = getDbFieldOp(value);
89
136
  if (fieldOp) this._set(key, this._fieldOpExpr(key, fieldOp.op, fieldOp.value));
137
+ else if (containsAggregationExpr(value)) this._set(key, { $literal: value });
90
138
  else this._set(key, value);
91
139
  }
92
140
  /**
@@ -491,7 +539,7 @@ function buildLookupInnerPipeline(withRel, requiredFields) {
491
539
  for (const f of select) projection[f] = 1;
492
540
  for (const f of requiredFields) projection[f] = 1;
493
541
  if (!select.includes("_id") && !requiredFields.includes("_id")) projection["_id"] = 0;
494
- pipeline.push({ $project: projection });
542
+ pipeline.push({ $project: dedupeProjection(projection) });
495
543
  }
496
544
  return pipeline;
497
545
  }
@@ -662,9 +710,12 @@ async function runSearchPipeline(host, stage, query, label, threshold) {
662
710
  if (controls.$skip) pipeline.push({ $skip: controls.$skip });
663
711
  if (controls.$limit) pipeline.push({ $limit: controls.$limit });
664
712
  else pipeline.push({ $limit: 1e3 });
665
- if (controls.$select) pipeline.push({ $project: controls.$select.asProjection });
713
+ if (controls.$select) {
714
+ const projection = controls.$select.asProjection;
715
+ if (projection) pipeline.push({ $project: dedupeProjection(projection) });
716
+ }
666
717
  host._log(`aggregate (${label})`, pipeline);
667
- return host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
718
+ return wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
668
719
  }
669
720
  /** Runs a search/vector pipeline with $facet for count. Shared by searchWithCount + vectorSearchWithCount. */
670
721
  async function runSearchWithCountPipeline(host, stage, query, label, threshold) {
@@ -679,7 +730,10 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold)
679
730
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
680
731
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
681
732
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
682
- if (controls.$select) dataStages.push({ $project: controls.$select.asProjection });
733
+ if (controls.$select) {
734
+ const projection = controls.$select.asProjection;
735
+ if (projection) dataStages.push({ $project: dedupeProjection(projection) });
736
+ }
683
737
  const pipeline = [
684
738
  stage,
685
739
  ...preStages,
@@ -690,7 +744,7 @@ async function runSearchWithCountPipeline(host, stage, query, label, threshold)
690
744
  } }
691
745
  ];
692
746
  host._log(`aggregate (${label})`, pipeline);
693
- const result = await host.collection.aggregate(pipeline, host._getSessionOpts()).toArray();
747
+ const result = await wrapInvalidQuery(() => host.collection.aggregate(pipeline, host._getSessionOpts()).toArray());
694
748
  return {
695
749
  data: result[0]?.data || [],
696
750
  count: result[0]?.meta[0]?.count || 0
@@ -1251,12 +1305,12 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1251
1305
  if (query.controls?.$count) {
1252
1306
  const pipeline = buildCountPipeline(query);
1253
1307
  this._log("aggregate (count)", pipeline);
1254
- const result = await this.aggregatePipeline(pipeline).toArray();
1308
+ const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
1255
1309
  return result.length > 0 ? result : [{ count: 0 }];
1256
1310
  }
1257
1311
  const pipeline = buildAggregatePipeline(query);
1258
1312
  this._log("aggregate", pipeline);
1259
- return this.aggregatePipeline(pipeline).toArray();
1313
+ return wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
1260
1314
  }
1261
1315
  get idType() {
1262
1316
  const idProp = this._table.type.type.props.get("_id");
@@ -1496,16 +1550,19 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1496
1550
  if (controls.$sort) dataStages.push({ $sort: controls.$sort });
1497
1551
  if (controls.$skip) dataStages.push({ $skip: controls.$skip });
1498
1552
  if (controls.$limit) dataStages.push({ $limit: controls.$limit });
1499
- if (controls.$select) dataStages.push({ $project: controls.$select.asProjection });
1553
+ if (controls.$select) {
1554
+ const projection = controls.$select.asProjection;
1555
+ if (projection) dataStages.push({ $project: dedupeProjection(projection) });
1556
+ }
1500
1557
  const pipeline = [{ $match: filter }, { $facet: {
1501
1558
  data: dataStages,
1502
1559
  meta: [{ $count: "count" }]
1503
1560
  } }];
1504
1561
  this._log("aggregate (findManyWithCount)", pipeline);
1505
- const result = await this.collection.aggregate(pipeline, {
1562
+ const result = await wrapInvalidQuery(() => this.collection.aggregate(pipeline, {
1506
1563
  ...this._getCollationOpts(query),
1507
1564
  ...this._getSessionOpts()
1508
- }).toArray();
1565
+ }).toArray());
1509
1566
  return {
1510
1567
  data: result[0]?.data || [],
1511
1568
  count: result[0]?.meta[0]?.count || 0
@@ -1588,21 +1645,21 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1588
1645
  const filter = buildMongoFilter(query.filter);
1589
1646
  const opts = this._buildFindOptions(query.controls);
1590
1647
  this._log("findOne", filter, opts);
1591
- return this.collection.findOne(filter, {
1648
+ return wrapInvalidQuery(() => this.collection.findOne(filter, {
1592
1649
  ...opts,
1593
1650
  ...this._getCollationOpts(query),
1594
1651
  ...this._getSessionOpts()
1595
- });
1652
+ }));
1596
1653
  }
1597
1654
  async findMany(query) {
1598
1655
  const filter = buildMongoFilter(query.filter);
1599
1656
  const opts = this._buildFindOptions(query.controls);
1600
1657
  this._log("findMany", filter, opts);
1601
- return this.collection.find(filter, {
1658
+ return wrapInvalidQuery(() => this.collection.find(filter, {
1602
1659
  ...opts,
1603
1660
  ...this._getCollationOpts(query),
1604
1661
  ...this._getSessionOpts()
1605
- }).toArray();
1662
+ }).toArray());
1606
1663
  }
1607
1664
  async count(query) {
1608
1665
  const filter = buildMongoFilter(query.filter);
@@ -1789,7 +1846,10 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1789
1846
  if (controls.$sort) opts.sort = controls.$sort;
1790
1847
  if (controls.$limit) opts.limit = controls.$limit;
1791
1848
  if (controls.$skip) opts.skip = controls.$skip;
1792
- if (controls.$select) opts.projection = controls.$select.asProjection;
1849
+ if (controls.$select) {
1850
+ const projection = controls.$select.asProjection;
1851
+ if (projection) opts.projection = dedupeProjection(projection);
1852
+ }
1793
1853
  return opts;
1794
1854
  }
1795
1855
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,17 +46,17 @@
46
46
  "access": "public"
47
47
  },
48
48
  "devDependencies": {
49
- "@atscript/core": "^0.1.63",
50
- "@atscript/typescript": "^0.1.63",
49
+ "@atscript/core": "^0.1.64",
50
+ "@atscript/typescript": "^0.1.64",
51
51
  "mongodb": "^6.17.0",
52
52
  "mongodb-memory-server-core": "^10.0.0",
53
- "unplugin-atscript": "^0.1.63"
53
+ "unplugin-atscript": "^0.1.64"
54
54
  },
55
55
  "peerDependencies": {
56
- "@atscript/core": "^0.1.63",
57
- "@atscript/typescript": "^0.1.63",
56
+ "@atscript/core": "^0.1.64",
57
+ "@atscript/typescript": "^0.1.64",
58
58
  "mongodb": "^6.17.0",
59
- "@atscript/db": "^0.1.89"
59
+ "@atscript/db": "^0.1.91"
60
60
  },
61
61
  "scripts": {
62
62
  "postinstall": "asc -f dts",