@atscript/db-mongo 0.1.131 → 0.1.133
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 +90 -14
- package/dist/agg.d.cts +19 -2
- package/dist/agg.d.mts +19 -2
- package/dist/agg.mjs +90 -15
- package/dist/index.cjs +25 -8
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +26 -9
- package/dist/{mongo-filter-z_hLPMyv.cjs → mongo-filter-Bf5rWV4P.cjs} +1 -0
- package/dist/{mongo-filter-DceAGI-S.mjs → mongo-filter-CtyITmZM.mjs} +1 -0
- package/package.json +4 -2
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-
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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,
|
|
56
|
-
const idKey =
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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,
|
|
55
|
-
const idKey =
|
|
56
|
-
|
|
57
|
-
|
|
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-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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-
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.133",
|
|
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.10",
|
|
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.10",
|
|
58
60
|
"mongodb": "^6.17.0",
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.133"
|
|
60
62
|
},
|
|
61
63
|
"scripts": {
|
|
62
64
|
"postinstall": "asc -f dts",
|