@atscript/db-mongo 0.1.131 → 0.1.132

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/agg.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
2
+ const require_mongo_filter = require("./mongo-filter-Bf5rWV4P.cjs");
3
3
  let _atscript_db_agg = require("@atscript/db/agg");
4
+ let _uniqu_core = require("@uniqu/core");
4
5
  //#region src/agg.ts
5
6
  /** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
6
7
  const SIMPLE_ACCUMULATORS = {
@@ -25,21 +26,94 @@ function toAccumulator(expr) {
25
26
  }
26
27
  throw new Error(`Unsupported aggregate function: ${expr.$fn}`);
27
28
  }
29
+ const DAY_MS = 864e5;
30
+ const ISO_DATE = "%Y-%m-%d";
28
31
  /**
29
- * `$group` output field names (including `_id` sub-keys) may not contain `.`,
30
- * so a dotted `$groupBy` path (a JSON/nested descendant such as
31
- * `metadata.clicks`) is keyed positionally inside `_id` (`k0`, `k1`, …) and
32
- * projected back under its dotted path — `$project` accepts dotted output
33
- * keys and nests them, which is the row shape a dotted `$select` yields on
34
- * find (`{ metadata: { clicks } }`). Plain paths keep their own name.
32
+ * Units whose first day `$dateToString` renders straight from the instant in
33
+ * the zone: the local day, then the literal `01` for day-of-month / month.
35
34
  */
36
- function groupIdKey(field, index) {
37
- return field.includes(".") ? `k${index}` : field;
35
+ const LOCAL_DATE_FORMATS = {
36
+ day: ISO_DATE,
37
+ month: "%Y-%m-01",
38
+ year: "%Y-01-01"
39
+ };
40
+ /**
41
+ * The first day of a quarter / week bucket as a NAIVE date (UTC midnight of
42
+ * the local calendar date), from the local parts `$$p` (`$dateToParts` in
43
+ * the zone). Pure calendar arithmetic: nothing converts local time back to an
44
+ * instant, so a zone whose DST switch skips midnight cannot shift a label.
45
+ */
46
+ function naiveFirstDay(b) {
47
+ if (b.unit === "quarter") return { $dateFromParts: {
48
+ year: "$$p.year",
49
+ month: { $subtract: ["$$p.month", { $mod: [{ $subtract: ["$$p.month", 1] }, 3] }] },
50
+ day: 1
51
+ } };
52
+ return { $let: {
53
+ vars: { n: { $dateFromParts: {
54
+ year: "$$p.year",
55
+ month: "$$p.month",
56
+ day: "$$p.day"
57
+ } } },
58
+ in: { $subtract: ["$$n", { $multiply: [{ $mod: [{ $add: [{ $subtract: [{ $isoDayOfWeek: "$$n" }, b.weekStartIso] }, 7] }, 7] }, DAY_MS] }] }
59
+ } };
60
+ }
61
+ /**
62
+ * The `$group._id` expression of a calendar bucket: the ISO local date
63
+ * `YYYY-MM-DD` of the bucket's first day in `b.tz`, or `null`.
64
+ *
65
+ * The only zone-aware step is instant → local date (`timezone` on
66
+ * `$dateToString` / `$dateToParts`, never ambiguous). `$dateTrunc` is
67
+ * deliberately not used — it returns the bucket start as an INSTANT, which
68
+ * reintroduces the midnight-gap ambiguity.
69
+ *
70
+ * The source is an epoch-ms number of any BSON numeric type. The `$cond`
71
+ * range guard `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)` labels an
72
+ * out-of-range source `null`, like every other adapter, and also folds null,
73
+ * missing and non-numeric sources (BSON orders null/missing below numbers and
74
+ * strings/objects above them) into ONE null group — a bare field path would
75
+ * group a missing source under `_id: {}`, apart from `_id: { k: null }`.
76
+ */
77
+ function bucketExpression(b) {
78
+ const source = `$${b.field}`;
79
+ const date = { $toDate: { $toLong: source } };
80
+ const format = LOCAL_DATE_FORMATS[b.unit];
81
+ const label = format ? { $dateToString: {
82
+ date,
83
+ format,
84
+ timezone: b.tz
85
+ } } : { $let: {
86
+ vars: { p: { $dateToParts: {
87
+ date,
88
+ timezone: b.tz
89
+ } } },
90
+ in: { $dateToString: {
91
+ date: naiveFirstDay(b),
92
+ format: ISO_DATE
93
+ } }
94
+ } };
95
+ return { $cond: [
96
+ { $and: [{ $gte: [source, _uniqu_core.BUCKET_MIN_INSTANT] }, { $lt: [source, _uniqu_core.BUCKET_MAX_INSTANT] }] },
97
+ label,
98
+ null
99
+ ] };
38
100
  }
39
101
  /**
40
102
  * Builds the common prefix stages: [search] + $match + $group._id from groupBy
41
103
  * fields. Shared by both full aggregate and count pipelines.
42
- * `groupKeys` maps each `$groupBy` path to its `_id` sub-key.
104
+ * `groupKeys` maps each `$groupBy` key (a field path or a calendar-bucket
105
+ * alias) to its `_id` sub-key.
106
+ *
107
+ * Every key is stored positionally inside `_id` (`k0`, `k1`, …) — internal
108
+ * names that never collide — and projected back under its own name:
109
+ * `$group` output names may not contain `.`, while `$project` accepts dotted
110
+ * output keys and nests them, which is the row shape a dotted `$select`
111
+ * yields on find (`{ metadata: { clicks } }`).
112
+ *
113
+ * A field key is grouped as `{ $ifNull: ['$f', null] }` so a missing and a
114
+ * null value form ONE null group (SQL semantics) that projects back as
115
+ * `f: null` — a bare `'$f'` would split them into `_id: {}` and
116
+ * `_id: { f: null }`. A bucket alias is grouped by {@link bucketExpression}.
43
117
  *
44
118
  * `searchStage` is the resolved text-search stage (classic `$text` `$match`, or
45
119
  * an Atlas `$search`). Both MUST be the pipeline's FIRST stage, hence its
@@ -52,10 +126,11 @@ function buildPrefix(query, searchStage) {
52
126
  const pipeline = searchStage ? [searchStage, { $match: require_mongo_filter.buildMongoFilter(query.filter) }] : [{ $match: require_mongo_filter.buildMongoFilter(query.filter) }];
53
127
  const groupId = {};
54
128
  const groupKeys = [];
55
- for (const [index, field] of groupBy.entries()) {
56
- const idKey = groupIdKey(field, index);
57
- groupId[idKey] = `$${field}`;
58
- groupKeys.push([field, idKey]);
129
+ for (const [index, key] of groupBy.entries()) {
130
+ const idKey = `k${index}`;
131
+ const bucket = controls.$select?.bucketByAlias(key);
132
+ groupId[idKey] = bucket ? bucketExpression(bucket) : { $ifNull: [`$${key}`, null] };
133
+ groupKeys.push([key, idKey]);
59
134
  }
60
135
  return {
61
136
  pipeline,
@@ -142,5 +217,6 @@ function buildCountPipeline(query, searchStage) {
142
217
  return pipeline;
143
218
  }
144
219
  //#endregion
220
+ exports.bucketExpression = bucketExpression;
145
221
  exports.buildAggregatePipeline = buildAggregatePipeline;
146
222
  exports.buildCountPipeline = buildCountPipeline;
package/dist/agg.d.cts CHANGED
@@ -1,7 +1,24 @@
1
- import { DbQuery } from "@atscript/db";
1
+ import { DbQuery, TResolvedBucket } from "@atscript/db";
2
2
  import { Document } from "mongodb";
3
3
 
4
4
  //#region src/agg.d.ts
5
+ /**
6
+ * The `$group._id` expression of a calendar bucket: the ISO local date
7
+ * `YYYY-MM-DD` of the bucket's first day in `b.tz`, or `null`.
8
+ *
9
+ * The only zone-aware step is instant → local date (`timezone` on
10
+ * `$dateToString` / `$dateToParts`, never ambiguous). `$dateTrunc` is
11
+ * deliberately not used — it returns the bucket start as an INSTANT, which
12
+ * reintroduces the midnight-gap ambiguity.
13
+ *
14
+ * The source is an epoch-ms number of any BSON numeric type. The `$cond`
15
+ * range guard `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)` labels an
16
+ * out-of-range source `null`, like every other adapter, and also folds null,
17
+ * missing and non-numeric sources (BSON orders null/missing below numbers and
18
+ * strings/objects above them) into ONE null group — a bare field path would
19
+ * group a missing source under `_id: {}`, apart from `_id: { k: null }`.
20
+ */
21
+ declare function bucketExpression(b: TResolvedBucket): Document;
5
22
  /**
6
23
  * Builds a full MongoDB aggregation pipeline for GROUP BY queries.
7
24
  *
@@ -27,4 +44,4 @@ declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document):
27
44
  */
28
45
  declare function buildCountPipeline(query: DbQuery, searchStage?: Document): Document[];
29
46
  //#endregion
30
- export { buildAggregatePipeline, buildCountPipeline };
47
+ export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
package/dist/agg.d.mts CHANGED
@@ -1,7 +1,24 @@
1
- import { DbQuery } from "@atscript/db";
1
+ import { DbQuery, TResolvedBucket } from "@atscript/db";
2
2
  import { Document } from "mongodb";
3
3
 
4
4
  //#region src/agg.d.ts
5
+ /**
6
+ * The `$group._id` expression of a calendar bucket: the ISO local date
7
+ * `YYYY-MM-DD` of the bucket's first day in `b.tz`, or `null`.
8
+ *
9
+ * The only zone-aware step is instant → local date (`timezone` on
10
+ * `$dateToString` / `$dateToParts`, never ambiguous). `$dateTrunc` is
11
+ * deliberately not used — it returns the bucket start as an INSTANT, which
12
+ * reintroduces the midnight-gap ambiguity.
13
+ *
14
+ * The source is an epoch-ms number of any BSON numeric type. The `$cond`
15
+ * range guard `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)` labels an
16
+ * out-of-range source `null`, like every other adapter, and also folds null,
17
+ * missing and non-numeric sources (BSON orders null/missing below numbers and
18
+ * strings/objects above them) into ONE null group — a bare field path would
19
+ * group a missing source under `_id: {}`, apart from `_id: { k: null }`.
20
+ */
21
+ declare function bucketExpression(b: TResolvedBucket): Document;
5
22
  /**
6
23
  * Builds a full MongoDB aggregation pipeline for GROUP BY queries.
7
24
  *
@@ -27,4 +44,4 @@ declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document):
27
44
  */
28
45
  declare function buildCountPipeline(query: DbQuery, searchStage?: Document): Document[];
29
46
  //#endregion
30
- export { buildAggregatePipeline, buildCountPipeline };
47
+ export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
package/dist/agg.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { t as buildMongoFilter } from "./mongo-filter-DceAGI-S.mjs";
1
+ import { t as buildMongoFilter } from "./mongo-filter-CtyITmZM.mjs";
2
2
  import { resolveAlias } from "@atscript/db/agg";
3
+ import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
3
4
  //#region src/agg.ts
4
5
  /** Simple accumulators that map directly to `{ $<fn>: '$field' }`. */
5
6
  const SIMPLE_ACCUMULATORS = {
@@ -24,21 +25,94 @@ function toAccumulator(expr) {
24
25
  }
25
26
  throw new Error(`Unsupported aggregate function: ${expr.$fn}`);
26
27
  }
28
+ const DAY_MS = 864e5;
29
+ const ISO_DATE = "%Y-%m-%d";
27
30
  /**
28
- * `$group` output field names (including `_id` sub-keys) may not contain `.`,
29
- * so a dotted `$groupBy` path (a JSON/nested descendant such as
30
- * `metadata.clicks`) is keyed positionally inside `_id` (`k0`, `k1`, …) and
31
- * projected back under its dotted path — `$project` accepts dotted output
32
- * keys and nests them, which is the row shape a dotted `$select` yields on
33
- * find (`{ metadata: { clicks } }`). Plain paths keep their own name.
31
+ * Units whose first day `$dateToString` renders straight from the instant in
32
+ * the zone: the local day, then the literal `01` for day-of-month / month.
34
33
  */
35
- function groupIdKey(field, index) {
36
- return field.includes(".") ? `k${index}` : field;
34
+ const LOCAL_DATE_FORMATS = {
35
+ day: ISO_DATE,
36
+ month: "%Y-%m-01",
37
+ year: "%Y-01-01"
38
+ };
39
+ /**
40
+ * The first day of a quarter / week bucket as a NAIVE date (UTC midnight of
41
+ * the local calendar date), from the local parts `$$p` (`$dateToParts` in
42
+ * the zone). Pure calendar arithmetic: nothing converts local time back to an
43
+ * instant, so a zone whose DST switch skips midnight cannot shift a label.
44
+ */
45
+ function naiveFirstDay(b) {
46
+ if (b.unit === "quarter") return { $dateFromParts: {
47
+ year: "$$p.year",
48
+ month: { $subtract: ["$$p.month", { $mod: [{ $subtract: ["$$p.month", 1] }, 3] }] },
49
+ day: 1
50
+ } };
51
+ return { $let: {
52
+ vars: { n: { $dateFromParts: {
53
+ year: "$$p.year",
54
+ month: "$$p.month",
55
+ day: "$$p.day"
56
+ } } },
57
+ in: { $subtract: ["$$n", { $multiply: [{ $mod: [{ $add: [{ $subtract: [{ $isoDayOfWeek: "$$n" }, b.weekStartIso] }, 7] }, 7] }, DAY_MS] }] }
58
+ } };
59
+ }
60
+ /**
61
+ * The `$group._id` expression of a calendar bucket: the ISO local date
62
+ * `YYYY-MM-DD` of the bucket's first day in `b.tz`, or `null`.
63
+ *
64
+ * The only zone-aware step is instant → local date (`timezone` on
65
+ * `$dateToString` / `$dateToParts`, never ambiguous). `$dateTrunc` is
66
+ * deliberately not used — it returns the bucket start as an INSTANT, which
67
+ * reintroduces the midnight-gap ambiguity.
68
+ *
69
+ * The source is an epoch-ms number of any BSON numeric type. The `$cond`
70
+ * range guard `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)` labels an
71
+ * out-of-range source `null`, like every other adapter, and also folds null,
72
+ * missing and non-numeric sources (BSON orders null/missing below numbers and
73
+ * strings/objects above them) into ONE null group — a bare field path would
74
+ * group a missing source under `_id: {}`, apart from `_id: { k: null }`.
75
+ */
76
+ function bucketExpression(b) {
77
+ const source = `$${b.field}`;
78
+ const date = { $toDate: { $toLong: source } };
79
+ const format = LOCAL_DATE_FORMATS[b.unit];
80
+ const label = format ? { $dateToString: {
81
+ date,
82
+ format,
83
+ timezone: b.tz
84
+ } } : { $let: {
85
+ vars: { p: { $dateToParts: {
86
+ date,
87
+ timezone: b.tz
88
+ } } },
89
+ in: { $dateToString: {
90
+ date: naiveFirstDay(b),
91
+ format: ISO_DATE
92
+ } }
93
+ } };
94
+ return { $cond: [
95
+ { $and: [{ $gte: [source, BUCKET_MIN_INSTANT] }, { $lt: [source, BUCKET_MAX_INSTANT] }] },
96
+ label,
97
+ null
98
+ ] };
37
99
  }
38
100
  /**
39
101
  * Builds the common prefix stages: [search] + $match + $group._id from groupBy
40
102
  * fields. Shared by both full aggregate and count pipelines.
41
- * `groupKeys` maps each `$groupBy` path to its `_id` sub-key.
103
+ * `groupKeys` maps each `$groupBy` key (a field path or a calendar-bucket
104
+ * alias) to its `_id` sub-key.
105
+ *
106
+ * Every key is stored positionally inside `_id` (`k0`, `k1`, …) — internal
107
+ * names that never collide — and projected back under its own name:
108
+ * `$group` output names may not contain `.`, while `$project` accepts dotted
109
+ * output keys and nests them, which is the row shape a dotted `$select`
110
+ * yields on find (`{ metadata: { clicks } }`).
111
+ *
112
+ * A field key is grouped as `{ $ifNull: ['$f', null] }` so a missing and a
113
+ * null value form ONE null group (SQL semantics) that projects back as
114
+ * `f: null` — a bare `'$f'` would split them into `_id: {}` and
115
+ * `_id: { f: null }`. A bucket alias is grouped by {@link bucketExpression}.
42
116
  *
43
117
  * `searchStage` is the resolved text-search stage (classic `$text` `$match`, or
44
118
  * an Atlas `$search`). Both MUST be the pipeline's FIRST stage, hence its
@@ -51,10 +125,11 @@ function buildPrefix(query, searchStage) {
51
125
  const pipeline = searchStage ? [searchStage, { $match: buildMongoFilter(query.filter) }] : [{ $match: buildMongoFilter(query.filter) }];
52
126
  const groupId = {};
53
127
  const groupKeys = [];
54
- for (const [index, field] of groupBy.entries()) {
55
- const idKey = groupIdKey(field, index);
56
- groupId[idKey] = `$${field}`;
57
- groupKeys.push([field, idKey]);
128
+ for (const [index, key] of groupBy.entries()) {
129
+ const idKey = `k${index}`;
130
+ const bucket = controls.$select?.bucketByAlias(key);
131
+ groupId[idKey] = bucket ? bucketExpression(bucket) : { $ifNull: [`$${key}`, null] };
132
+ groupKeys.push([key, idKey]);
58
133
  }
59
134
  return {
60
135
  pipeline,
@@ -141,4 +216,4 @@ function buildCountPipeline(query, searchStage) {
141
216
  return pipeline;
142
217
  }
143
218
  //#endregion
144
- export { buildAggregatePipeline, buildCountPipeline };
219
+ export { bucketExpression, buildAggregatePipeline, buildCountPipeline };
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
2
+ const require_mongo_filter = require("./mongo-filter-Bf5rWV4P.cjs");
3
3
  let _atscript_db = require("@atscript/db");
4
4
  let mongodb = require("mongodb");
5
5
  let _atscript_db_agg = require("@atscript/db/agg");
@@ -33,19 +33,32 @@ function dedupeProjection(projection) {
33
33
  }
34
34
  //#endregion
35
35
  //#region src/lib/mongo-errors.ts
36
+ /** Server code for an unrecognized `timezone` in a date expression (verified on MongoDB 7.0 and 8.0). */
37
+ const UNKNOWN_TIME_ZONE = 40485;
36
38
  /**
37
- * Maps MongoDB projection-validation errors (31249, 31254) to `DbError("INVALID_QUERY")`
38
- * so moost-db's validation interceptor returns HTTP 400 instead of an opaque 500.
39
- * These codes always indicate malformed client `$select`, not a server fault.
39
+ * Maps MongoDB server errors that describe the query, not a server fault:
40
+ * - projection validation (31249, 31254) → `DbError("INVALID_QUERY")` (moost-db:
41
+ * 400) — these codes always indicate a malformed client `$select`;
42
+ * - an unrecognized time zone identifier (40485) → `BUCKET_TZ_UNAVAILABLE`
43
+ * (moost-db: 501). Only calendar buckets put a zone into a pipeline, and the
44
+ * core already accepted it as a canonical IANA name — so the server's
45
+ * bundled tz database lacking it is a store condition, never a silent NULL
46
+ * or UTC label.
40
47
  */
41
48
  async function wrapInvalidQuery(fn) {
42
49
  try {
43
50
  return await fn();
44
51
  } catch (error) {
45
- if (error instanceof mongodb.MongoServerError && (error.code === 31249 || error.code === 31254)) throw new _atscript_db.DbError("INVALID_QUERY", [{
46
- path: "$select",
47
- message: error.message
48
- }]);
52
+ if (error instanceof mongodb.MongoServerError) {
53
+ if (error.code === 31249 || error.code === 31254) throw new _atscript_db.DbError("INVALID_QUERY", [{
54
+ path: "$select",
55
+ message: error.message
56
+ }]);
57
+ if (error.code === UNKNOWN_TIME_ZONE) {
58
+ const zone = /time zone identifier:\s*"([^"]*)"/.exec(error.message)?.[1];
59
+ throw (0, _atscript_db.bucketTimeZoneUnavailable)(zone ? `MongoDB does not recognize time zone "${zone}" — its time zone database may be outdated` : `MongoDB does not recognize a bucket time zone: ${error.message}`);
60
+ }
61
+ }
49
62
  throw error;
50
63
  }
51
64
  }
@@ -1750,6 +1763,10 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
1750
1763
  supportsNativePatch() {
1751
1764
  return true;
1752
1765
  }
1766
+ /** All five units, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
1767
+ calendarBucketUnits() {
1768
+ return _atscript_db.ALL_BUCKET_UNITS;
1769
+ }
1753
1770
  getValidatorPlugins() {
1754
1771
  return [validateMongoIdPlugin];
1755
1772
  }
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
2
2
  import { AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
3
3
  import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
4
+ import { BucketUnit } from "@uniqu/core";
4
5
 
5
6
  //#region src/lib/collection-patcher.d.ts
6
7
  /**
@@ -300,6 +301,8 @@ declare class MongoAdapter extends BaseDbAdapter {
300
301
  prepareIdFromIdType<D = string | number | ObjectId>(id: string | number | ObjectId): D;
301
302
  supportsNestedObjects(): boolean;
302
303
  supportsNativePatch(): boolean;
304
+ /** All five units, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
305
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
303
306
  getValidatorPlugins(): ReturnType<BaseDbAdapter["getValidatorPlugins"]>;
304
307
  /**
305
308
  * Mongo can filter on JSON-stored fields natively — arrays via implicit
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbForeignKey, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbRelation, TDbUpdateResult, TExistingTableOption, TFieldOps, TMetadataOverrides, TPrimaryKeyChange, TSearchIndexInfo, TSyncColumnResult, TTableResolver, TableMetadata, WithRelation, getKeyProps } from "@atscript/db";
2
2
  import { AggregationCursor, ClientSession, Collection, Db, Document, Filter, MongoClient, ObjectId, UpdateFilter, UpdateOptions } from "mongodb";
3
+ import { BucketUnit } from "@uniqu/core";
3
4
  import { TAtscriptAnnotatedType, TMetadataMap, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
4
5
 
5
6
  //#region src/lib/collection-patcher.d.ts
@@ -300,6 +301,8 @@ declare class MongoAdapter extends BaseDbAdapter {
300
301
  prepareIdFromIdType<D = string | number | ObjectId>(id: string | number | ObjectId): D;
301
302
  supportsNestedObjects(): boolean;
302
303
  supportsNativePatch(): boolean;
304
+ /** All five units, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
305
+ calendarBucketUnits(): ReadonlySet<BucketUnit>;
303
306
  getValidatorPlugins(): ReturnType<BaseDbAdapter["getValidatorPlugins"]>;
304
307
  /**
305
308
  * Mongo can filter on JSON-stored fields natively — arrays via implicit
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { t as buildMongoFilter } from "./mongo-filter-DceAGI-S.mjs";
2
- import { BaseDbAdapter, DbError, DbSpace, computeInsights, createFailureCollector, getDbFieldOp, getKeyProps, isAtscriptDbView, resolveDesignType } from "@atscript/db";
1
+ import { t as buildMongoFilter } from "./mongo-filter-CtyITmZM.mjs";
2
+ import { ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, bucketTimeZoneUnavailable, computeInsights, createFailureCollector, getDbFieldOp, getKeyProps, isAtscriptDbView, resolveDesignType } from "@atscript/db";
3
3
  import { MongoClient, MongoServerError, ObjectId } from "mongodb";
4
4
  import { resolveAggregateSearch } from "@atscript/db/agg";
5
5
  //#region src/lib/path-utils.ts
@@ -32,19 +32,32 @@ function dedupeProjection(projection) {
32
32
  }
33
33
  //#endregion
34
34
  //#region src/lib/mongo-errors.ts
35
+ /** Server code for an unrecognized `timezone` in a date expression (verified on MongoDB 7.0 and 8.0). */
36
+ const UNKNOWN_TIME_ZONE = 40485;
35
37
  /**
36
- * Maps MongoDB projection-validation errors (31249, 31254) to `DbError("INVALID_QUERY")`
37
- * so moost-db's validation interceptor returns HTTP 400 instead of an opaque 500.
38
- * These codes always indicate malformed client `$select`, not a server fault.
38
+ * Maps MongoDB server errors that describe the query, not a server fault:
39
+ * - projection validation (31249, 31254) → `DbError("INVALID_QUERY")` (moost-db:
40
+ * 400) — these codes always indicate a malformed client `$select`;
41
+ * - an unrecognized time zone identifier (40485) → `BUCKET_TZ_UNAVAILABLE`
42
+ * (moost-db: 501). Only calendar buckets put a zone into a pipeline, and the
43
+ * core already accepted it as a canonical IANA name — so the server's
44
+ * bundled tz database lacking it is a store condition, never a silent NULL
45
+ * or UTC label.
39
46
  */
40
47
  async function wrapInvalidQuery(fn) {
41
48
  try {
42
49
  return await fn();
43
50
  } catch (error) {
44
- if (error instanceof MongoServerError && (error.code === 31249 || error.code === 31254)) throw new DbError("INVALID_QUERY", [{
45
- path: "$select",
46
- message: error.message
47
- }]);
51
+ if (error instanceof MongoServerError) {
52
+ if (error.code === 31249 || error.code === 31254) throw new DbError("INVALID_QUERY", [{
53
+ path: "$select",
54
+ message: error.message
55
+ }]);
56
+ if (error.code === UNKNOWN_TIME_ZONE) {
57
+ const zone = /time zone identifier:\s*"([^"]*)"/.exec(error.message)?.[1];
58
+ throw bucketTimeZoneUnavailable(zone ? `MongoDB does not recognize time zone "${zone}" — its time zone database may be outdated` : `MongoDB does not recognize a bucket time zone: ${error.message}`);
59
+ }
60
+ }
48
61
  throw error;
49
62
  }
50
63
  }
@@ -1749,6 +1762,10 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
1749
1762
  supportsNativePatch() {
1750
1763
  return true;
1751
1764
  }
1765
+ /** All five units, over MongoDB's bundled time zone database (see `agg.ts` `bucketExpression`). */
1766
+ calendarBucketUnits() {
1767
+ return ALL_BUCKET_UNITS;
1768
+ }
1752
1769
  getValidatorPlugins() {
1753
1770
  return [validateMongoIdPlugin];
1754
1771
  }
@@ -25,6 +25,7 @@ const EARTH_RADIUS_M = 6378100;
25
25
  const mongoVisitor = {
26
26
  comparison(field, op, value) {
27
27
  if (op === "$eq") return { [field]: value };
28
+ if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
28
29
  if (op === "$regex") {
29
30
  const { pattern, flags } = parseRegexString(value);
30
31
  return flags ? { [field]: {
@@ -25,6 +25,7 @@ const EARTH_RADIUS_M = 6378100;
25
25
  const mongoVisitor = {
26
26
  comparison(field, op, value) {
27
27
  if (op === "$eq") return { [field]: value };
28
+ if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
28
29
  if (op === "$regex") {
29
30
  const { pattern, flags } = parseRegexString(value);
30
31
  return flags ? { [field]: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.131",
3
+ "version": "0.1.132",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -48,6 +48,7 @@
48
48
  "devDependencies": {
49
49
  "@atscript/core": "^0.1.92",
50
50
  "@atscript/typescript": "^0.1.92",
51
+ "@uniqu/core": "^0.1.9",
51
52
  "mongodb": "^6.17.0",
52
53
  "mongodb-memory-server-core": "^10.0.0",
53
54
  "unplugin-atscript": "^0.1.92"
@@ -55,8 +56,9 @@
55
56
  "peerDependencies": {
56
57
  "@atscript/core": "^0.1.92",
57
58
  "@atscript/typescript": "^0.1.92",
59
+ "@uniqu/core": "^0.1.9",
58
60
  "mongodb": "^6.17.0",
59
- "@atscript/db": "^0.1.131"
61
+ "@atscript/db": "^0.1.132"
60
62
  },
61
63
  "scripts": {
62
64
  "postinstall": "asc -f dts",