@atscript/db-mongo 0.1.129 → 0.1.131
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 +37 -16
- package/dist/agg.d.cts +12 -4
- package/dist/agg.d.mts +12 -4
- package/dist/agg.mjs +37 -16
- package/dist/index.cjs +31 -6
- package/dist/index.mjs +31 -6
- package/package.json +2 -2
package/dist/agg.cjs
CHANGED
|
@@ -37,14 +37,19 @@ function groupIdKey(field, index) {
|
|
|
37
37
|
return field.includes(".") ? `k${index}` : field;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Builds the common prefix stages: $match + $group._id from groupBy
|
|
41
|
-
* Shared by both full aggregate and count pipelines.
|
|
40
|
+
* Builds the common prefix stages: [search] + $match + $group._id from groupBy
|
|
41
|
+
* fields. Shared by both full aggregate and count pipelines.
|
|
42
42
|
* `groupKeys` maps each `$groupBy` path to its `_id` sub-key.
|
|
43
|
+
*
|
|
44
|
+
* `searchStage` is the resolved text-search stage (classic `$text` `$match`, or
|
|
45
|
+
* an Atlas `$search`). Both MUST be the pipeline's FIRST stage, hence its
|
|
46
|
+
* position in front of the filter `$match`. Resolving it needs adapter state,
|
|
47
|
+
* so the caller passes it in and this module stays a pure translation.
|
|
43
48
|
*/
|
|
44
|
-
function buildPrefix(query) {
|
|
49
|
+
function buildPrefix(query, searchStage) {
|
|
45
50
|
const controls = query.controls || {};
|
|
46
51
|
const groupBy = controls.$groupBy ?? [];
|
|
47
|
-
const pipeline = [{ $match: require_mongo_filter.buildMongoFilter(query.filter) }];
|
|
52
|
+
const pipeline = searchStage ? [searchStage, { $match: require_mongo_filter.buildMongoFilter(query.filter) }] : [{ $match: require_mongo_filter.buildMongoFilter(query.filter) }];
|
|
48
53
|
const groupId = {};
|
|
49
54
|
const groupKeys = [];
|
|
50
55
|
for (const [index, field] of groupBy.entries()) {
|
|
@@ -60,17 +65,19 @@ function buildPrefix(query) {
|
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
|
-
* The stages every grouped query shares:
|
|
64
|
-
* (dimensions + accumulators) → `$project` (flatten `_id`, keep
|
|
65
|
-
* `$match($having)`. The row pipeline appends sort/skip/limit, the
|
|
66
|
-
* pipeline appends `$count`, so both see exactly the same group set
|
|
68
|
+
* The stages every grouped query shares: `[search →] $match(filter)` →
|
|
69
|
+
* `$group` (dimensions + accumulators) → `$project` (flatten `_id`, keep
|
|
70
|
+
* aliases) → `$match($having)`. The row pipeline appends sort/skip/limit, the
|
|
71
|
+
* count pipeline appends `$count`, so both see exactly the same group set —
|
|
72
|
+
* including the same `$search` narrowing, which is why the stage is threaded
|
|
73
|
+
* through this single seam instead of being appended by each caller.
|
|
67
74
|
*
|
|
68
75
|
* With `accumulators: false` only the `$group._id` dimensions are emitted
|
|
69
76
|
* (no accumulators, no `$project`, no `$having`) — the cheapest shape for a
|
|
70
77
|
* plain group count, where no alias has to be resolvable.
|
|
71
78
|
*/
|
|
72
|
-
function buildGroupedStages(query, { accumulators }) {
|
|
73
|
-
const { pipeline, groupId, groupKeys, controls } = buildPrefix(query);
|
|
79
|
+
function buildGroupedStages(query, { accumulators, searchStage }) {
|
|
80
|
+
const { pipeline, groupId, groupKeys, controls } = buildPrefix(query, searchStage);
|
|
74
81
|
const groupStage = { _id: groupId };
|
|
75
82
|
if (!accumulators) {
|
|
76
83
|
pipeline.push({ $group: groupStage });
|
|
@@ -97,10 +104,18 @@ function buildGroupedStages(query, { accumulators }) {
|
|
|
97
104
|
/**
|
|
98
105
|
* Builds a full MongoDB aggregation pipeline for GROUP BY queries.
|
|
99
106
|
*
|
|
100
|
-
* Pipeline: $match → $group → $project → $match(having) → $sort →
|
|
107
|
+
* Pipeline: [search →] $match → $group → $project → $match(having) → $sort →
|
|
108
|
+
* $skip → $limit
|
|
109
|
+
*
|
|
110
|
+
* `searchStage` is the resolved `$search` / `$text` stage (see
|
|
111
|
+
* `buildAggregateSearchStage`); unlike the leaf runner this path adds no
|
|
112
|
+
* relevance `$sort` and no default `$limit`.
|
|
101
113
|
*/
|
|
102
|
-
function buildAggregatePipeline(query) {
|
|
103
|
-
const { pipeline, controls } = buildGroupedStages(query, {
|
|
114
|
+
function buildAggregatePipeline(query, searchStage) {
|
|
115
|
+
const { pipeline, controls } = buildGroupedStages(query, {
|
|
116
|
+
accumulators: true,
|
|
117
|
+
searchStage
|
|
118
|
+
});
|
|
104
119
|
if (controls.$sort) pipeline.push({ $sort: controls.$sort });
|
|
105
120
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
106
121
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
@@ -113,10 +128,16 @@ function buildAggregatePipeline(query) {
|
|
|
113
128
|
* `$having` has a value to match; without it only the `$group._id`
|
|
114
129
|
* dimensions are needed.
|
|
115
130
|
*
|
|
116
|
-
* Pipeline: $match → $group → [$project → $match(having)] → $count
|
|
131
|
+
* Pipeline: [search →] $match → $group → [$project → $match(having)] → $count
|
|
132
|
+
*
|
|
133
|
+
* `searchStage` must be the SAME stage handed to `buildAggregatePipeline` for
|
|
134
|
+
* the same query — the counted groups are exactly the rows the search matched.
|
|
117
135
|
*/
|
|
118
|
-
function buildCountPipeline(query) {
|
|
119
|
-
const { pipeline } = buildGroupedStages(query, {
|
|
136
|
+
function buildCountPipeline(query, searchStage) {
|
|
137
|
+
const { pipeline } = buildGroupedStages(query, {
|
|
138
|
+
accumulators: Boolean(query.controls?.$having),
|
|
139
|
+
searchStage
|
|
140
|
+
});
|
|
120
141
|
pipeline.push({ $count: "count" });
|
|
121
142
|
return pipeline;
|
|
122
143
|
}
|
package/dist/agg.d.cts
CHANGED
|
@@ -5,9 +5,14 @@ import { Document } from "mongodb";
|
|
|
5
5
|
/**
|
|
6
6
|
* Builds a full MongoDB aggregation pipeline for GROUP BY queries.
|
|
7
7
|
*
|
|
8
|
-
* Pipeline: $match → $group → $project → $match(having) → $sort →
|
|
8
|
+
* Pipeline: [search →] $match → $group → $project → $match(having) → $sort →
|
|
9
|
+
* $skip → $limit
|
|
10
|
+
*
|
|
11
|
+
* `searchStage` is the resolved `$search` / `$text` stage (see
|
|
12
|
+
* `buildAggregateSearchStage`); unlike the leaf runner this path adds no
|
|
13
|
+
* relevance `$sort` and no default `$limit`.
|
|
9
14
|
*/
|
|
10
|
-
declare function buildAggregatePipeline(query: DbQuery): Document[];
|
|
15
|
+
declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document): Document[];
|
|
11
16
|
/**
|
|
12
17
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
13
18
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
@@ -15,8 +20,11 @@ declare function buildAggregatePipeline(query: DbQuery): Document[];
|
|
|
15
20
|
* `$having` has a value to match; without it only the `$group._id`
|
|
16
21
|
* dimensions are needed.
|
|
17
22
|
*
|
|
18
|
-
* Pipeline: $match → $group → [$project → $match(having)] → $count
|
|
23
|
+
* Pipeline: [search →] $match → $group → [$project → $match(having)] → $count
|
|
24
|
+
*
|
|
25
|
+
* `searchStage` must be the SAME stage handed to `buildAggregatePipeline` for
|
|
26
|
+
* the same query — the counted groups are exactly the rows the search matched.
|
|
19
27
|
*/
|
|
20
|
-
declare function buildCountPipeline(query: DbQuery): Document[];
|
|
28
|
+
declare function buildCountPipeline(query: DbQuery, searchStage?: Document): Document[];
|
|
21
29
|
//#endregion
|
|
22
30
|
export { buildAggregatePipeline, buildCountPipeline };
|
package/dist/agg.d.mts
CHANGED
|
@@ -5,9 +5,14 @@ import { Document } from "mongodb";
|
|
|
5
5
|
/**
|
|
6
6
|
* Builds a full MongoDB aggregation pipeline for GROUP BY queries.
|
|
7
7
|
*
|
|
8
|
-
* Pipeline: $match → $group → $project → $match(having) → $sort →
|
|
8
|
+
* Pipeline: [search →] $match → $group → $project → $match(having) → $sort →
|
|
9
|
+
* $skip → $limit
|
|
10
|
+
*
|
|
11
|
+
* `searchStage` is the resolved `$search` / `$text` stage (see
|
|
12
|
+
* `buildAggregateSearchStage`); unlike the leaf runner this path adds no
|
|
13
|
+
* relevance `$sort` and no default `$limit`.
|
|
9
14
|
*/
|
|
10
|
-
declare function buildAggregatePipeline(query: DbQuery): Document[];
|
|
15
|
+
declare function buildAggregatePipeline(query: DbQuery, searchStage?: Document): Document[];
|
|
11
16
|
/**
|
|
12
17
|
* Builds a count-only pipeline: the number of groups that survive `$having`
|
|
13
18
|
* (all groups when there is none). With `$having` it runs the same grouped
|
|
@@ -15,8 +20,11 @@ declare function buildAggregatePipeline(query: DbQuery): Document[];
|
|
|
15
20
|
* `$having` has a value to match; without it only the `$group._id`
|
|
16
21
|
* dimensions are needed.
|
|
17
22
|
*
|
|
18
|
-
* Pipeline: $match → $group → [$project → $match(having)] → $count
|
|
23
|
+
* Pipeline: [search →] $match → $group → [$project → $match(having)] → $count
|
|
24
|
+
*
|
|
25
|
+
* `searchStage` must be the SAME stage handed to `buildAggregatePipeline` for
|
|
26
|
+
* the same query — the counted groups are exactly the rows the search matched.
|
|
19
27
|
*/
|
|
20
|
-
declare function buildCountPipeline(query: DbQuery): Document[];
|
|
28
|
+
declare function buildCountPipeline(query: DbQuery, searchStage?: Document): Document[];
|
|
21
29
|
//#endregion
|
|
22
30
|
export { buildAggregatePipeline, buildCountPipeline };
|
package/dist/agg.mjs
CHANGED
|
@@ -36,14 +36,19 @@ function groupIdKey(field, index) {
|
|
|
36
36
|
return field.includes(".") ? `k${index}` : field;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Builds the common prefix stages: $match + $group._id from groupBy
|
|
40
|
-
* Shared by both full aggregate and count pipelines.
|
|
39
|
+
* Builds the common prefix stages: [search] + $match + $group._id from groupBy
|
|
40
|
+
* fields. Shared by both full aggregate and count pipelines.
|
|
41
41
|
* `groupKeys` maps each `$groupBy` path to its `_id` sub-key.
|
|
42
|
+
*
|
|
43
|
+
* `searchStage` is the resolved text-search stage (classic `$text` `$match`, or
|
|
44
|
+
* an Atlas `$search`). Both MUST be the pipeline's FIRST stage, hence its
|
|
45
|
+
* position in front of the filter `$match`. Resolving it needs adapter state,
|
|
46
|
+
* so the caller passes it in and this module stays a pure translation.
|
|
42
47
|
*/
|
|
43
|
-
function buildPrefix(query) {
|
|
48
|
+
function buildPrefix(query, searchStage) {
|
|
44
49
|
const controls = query.controls || {};
|
|
45
50
|
const groupBy = controls.$groupBy ?? [];
|
|
46
|
-
const pipeline = [{ $match: buildMongoFilter(query.filter) }];
|
|
51
|
+
const pipeline = searchStage ? [searchStage, { $match: buildMongoFilter(query.filter) }] : [{ $match: buildMongoFilter(query.filter) }];
|
|
47
52
|
const groupId = {};
|
|
48
53
|
const groupKeys = [];
|
|
49
54
|
for (const [index, field] of groupBy.entries()) {
|
|
@@ -59,17 +64,19 @@ function buildPrefix(query) {
|
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
/**
|
|
62
|
-
* The stages every grouped query shares:
|
|
63
|
-
* (dimensions + accumulators) → `$project` (flatten `_id`, keep
|
|
64
|
-
* `$match($having)`. The row pipeline appends sort/skip/limit, the
|
|
65
|
-
* pipeline appends `$count`, so both see exactly the same group set
|
|
67
|
+
* The stages every grouped query shares: `[search →] $match(filter)` →
|
|
68
|
+
* `$group` (dimensions + accumulators) → `$project` (flatten `_id`, keep
|
|
69
|
+
* aliases) → `$match($having)`. The row pipeline appends sort/skip/limit, the
|
|
70
|
+
* count pipeline appends `$count`, so both see exactly the same group set —
|
|
71
|
+
* including the same `$search` narrowing, which is why the stage is threaded
|
|
72
|
+
* through this single seam instead of being appended by each caller.
|
|
66
73
|
*
|
|
67
74
|
* With `accumulators: false` only the `$group._id` dimensions are emitted
|
|
68
75
|
* (no accumulators, no `$project`, no `$having`) — the cheapest shape for a
|
|
69
76
|
* plain group count, where no alias has to be resolvable.
|
|
70
77
|
*/
|
|
71
|
-
function buildGroupedStages(query, { accumulators }) {
|
|
72
|
-
const { pipeline, groupId, groupKeys, controls } = buildPrefix(query);
|
|
78
|
+
function buildGroupedStages(query, { accumulators, searchStage }) {
|
|
79
|
+
const { pipeline, groupId, groupKeys, controls } = buildPrefix(query, searchStage);
|
|
73
80
|
const groupStage = { _id: groupId };
|
|
74
81
|
if (!accumulators) {
|
|
75
82
|
pipeline.push({ $group: groupStage });
|
|
@@ -96,10 +103,18 @@ function buildGroupedStages(query, { accumulators }) {
|
|
|
96
103
|
/**
|
|
97
104
|
* Builds a full MongoDB aggregation pipeline for GROUP BY queries.
|
|
98
105
|
*
|
|
99
|
-
* Pipeline: $match → $group → $project → $match(having) → $sort →
|
|
106
|
+
* Pipeline: [search →] $match → $group → $project → $match(having) → $sort →
|
|
107
|
+
* $skip → $limit
|
|
108
|
+
*
|
|
109
|
+
* `searchStage` is the resolved `$search` / `$text` stage (see
|
|
110
|
+
* `buildAggregateSearchStage`); unlike the leaf runner this path adds no
|
|
111
|
+
* relevance `$sort` and no default `$limit`.
|
|
100
112
|
*/
|
|
101
|
-
function buildAggregatePipeline(query) {
|
|
102
|
-
const { pipeline, controls } = buildGroupedStages(query, {
|
|
113
|
+
function buildAggregatePipeline(query, searchStage) {
|
|
114
|
+
const { pipeline, controls } = buildGroupedStages(query, {
|
|
115
|
+
accumulators: true,
|
|
116
|
+
searchStage
|
|
117
|
+
});
|
|
103
118
|
if (controls.$sort) pipeline.push({ $sort: controls.$sort });
|
|
104
119
|
if (controls.$skip) pipeline.push({ $skip: controls.$skip });
|
|
105
120
|
if (controls.$limit) pipeline.push({ $limit: controls.$limit });
|
|
@@ -112,10 +127,16 @@ function buildAggregatePipeline(query) {
|
|
|
112
127
|
* `$having` has a value to match; without it only the `$group._id`
|
|
113
128
|
* dimensions are needed.
|
|
114
129
|
*
|
|
115
|
-
* Pipeline: $match → $group → [$project → $match(having)] → $count
|
|
130
|
+
* Pipeline: [search →] $match → $group → [$project → $match(having)] → $count
|
|
131
|
+
*
|
|
132
|
+
* `searchStage` must be the SAME stage handed to `buildAggregatePipeline` for
|
|
133
|
+
* the same query — the counted groups are exactly the rows the search matched.
|
|
116
134
|
*/
|
|
117
|
-
function buildCountPipeline(query) {
|
|
118
|
-
const { pipeline } = buildGroupedStages(query, {
|
|
135
|
+
function buildCountPipeline(query, searchStage) {
|
|
136
|
+
const { pipeline } = buildGroupedStages(query, {
|
|
137
|
+
accumulators: Boolean(query.controls?.$having),
|
|
138
|
+
searchStage
|
|
139
|
+
});
|
|
119
140
|
pipeline.push({ $count: "count" });
|
|
120
141
|
return pipeline;
|
|
121
142
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_mongo_filter = require("./mongo-filter-z_hLPMyv.cjs");
|
|
3
3
|
let _atscript_db = require("@atscript/db");
|
|
4
4
|
let mongodb = require("mongodb");
|
|
5
|
+
let _atscript_db_agg = require("@atscript/db/agg");
|
|
5
6
|
//#region src/lib/path-utils.ts
|
|
6
7
|
function joinPath(prefix, segment) {
|
|
7
8
|
return prefix ? `${prefix}.${segment}` : segment;
|
|
@@ -648,16 +649,39 @@ function isVectorSearchableImpl(host) {
|
|
|
648
649
|
for (const index of host.getMongoSearchIndexes().values()) if (index.type === "vector") return true;
|
|
649
650
|
return false;
|
|
650
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Resolves the leading stage for a text search, or throws when no index
|
|
654
|
+
* answers to `indexName` (the default index when it is omitted).
|
|
655
|
+
*/
|
|
656
|
+
function requireSearchStage(host, text, indexName, controls) {
|
|
657
|
+
const plan = buildSearchStage(host, text, indexName, controls);
|
|
658
|
+
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
659
|
+
return plan;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Resolves the leading search stage for a GROUPED aggregate query, or
|
|
663
|
+
* `undefined` when the query carries no `$search` term (the plain grouped
|
|
664
|
+
* path). `$index` picks the search index by the same rules as `search()`, and
|
|
665
|
+
* an unresolvable one throws exactly as the leaf path does.
|
|
666
|
+
*
|
|
667
|
+
* `classicText` is deliberately dropped: it only drives the leaf runner's
|
|
668
|
+
* `_score` projection and relevance `$sort`, and no per-document score
|
|
669
|
+
* survives `$group`. See `resolveAggregateSearch` in `@atscript/db/agg` for
|
|
670
|
+
* the normative cross-adapter contract.
|
|
671
|
+
*/
|
|
672
|
+
function buildAggregateSearchStage(host, controls) {
|
|
673
|
+
const search = (0, _atscript_db_agg.resolveAggregateSearch)(controls);
|
|
674
|
+
if (!search) return;
|
|
675
|
+
return requireSearchStage(host, search.text, search.indexName, controls).stage;
|
|
676
|
+
}
|
|
651
677
|
/** Text search via $search aggregation stage. */
|
|
652
678
|
async function searchImpl(host, text, query, indexName) {
|
|
653
|
-
const plan =
|
|
654
|
-
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
679
|
+
const plan = requireSearchStage(host, text, indexName, query.controls);
|
|
655
680
|
return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
|
|
656
681
|
}
|
|
657
682
|
/** Text search with faceted count. */
|
|
658
683
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
659
|
-
const plan =
|
|
660
|
-
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
684
|
+
const plan = requireSearchStage(host, text, indexName, query.controls);
|
|
661
685
|
return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
|
|
662
686
|
}
|
|
663
687
|
/** Vector search via $vectorSearch aggregation stage. */
|
|
@@ -1681,13 +1705,14 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1681
1705
|
}
|
|
1682
1706
|
async aggregate(query) {
|
|
1683
1707
|
const { buildAggregatePipeline, buildCountPipeline } = await Promise.resolve().then(() => require("./agg.cjs"));
|
|
1708
|
+
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1684
1709
|
if (query.controls?.$count) {
|
|
1685
|
-
const pipeline = buildCountPipeline(query);
|
|
1710
|
+
const pipeline = buildCountPipeline(query, searchStage);
|
|
1686
1711
|
this._log("aggregate (count)", pipeline);
|
|
1687
1712
|
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1688
1713
|
return result.length > 0 ? result : [{ count: 0 }];
|
|
1689
1714
|
}
|
|
1690
|
-
const pipeline = buildAggregatePipeline(query);
|
|
1715
|
+
const pipeline = buildAggregatePipeline(query, searchStage);
|
|
1691
1716
|
this._log("aggregate", pipeline);
|
|
1692
1717
|
return wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1693
1718
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as buildMongoFilter } from "./mongo-filter-DceAGI-S.mjs";
|
|
2
2
|
import { BaseDbAdapter, DbError, DbSpace, computeInsights, createFailureCollector, getDbFieldOp, getKeyProps, isAtscriptDbView, resolveDesignType } from "@atscript/db";
|
|
3
3
|
import { MongoClient, MongoServerError, ObjectId } from "mongodb";
|
|
4
|
+
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
4
5
|
//#region src/lib/path-utils.ts
|
|
5
6
|
function joinPath(prefix, segment) {
|
|
6
7
|
return prefix ? `${prefix}.${segment}` : segment;
|
|
@@ -647,16 +648,39 @@ function isVectorSearchableImpl(host) {
|
|
|
647
648
|
for (const index of host.getMongoSearchIndexes().values()) if (index.type === "vector") return true;
|
|
648
649
|
return false;
|
|
649
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Resolves the leading stage for a text search, or throws when no index
|
|
653
|
+
* answers to `indexName` (the default index when it is omitted).
|
|
654
|
+
*/
|
|
655
|
+
function requireSearchStage(host, text, indexName, controls) {
|
|
656
|
+
const plan = buildSearchStage(host, text, indexName, controls);
|
|
657
|
+
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
658
|
+
return plan;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Resolves the leading search stage for a GROUPED aggregate query, or
|
|
662
|
+
* `undefined` when the query carries no `$search` term (the plain grouped
|
|
663
|
+
* path). `$index` picks the search index by the same rules as `search()`, and
|
|
664
|
+
* an unresolvable one throws exactly as the leaf path does.
|
|
665
|
+
*
|
|
666
|
+
* `classicText` is deliberately dropped: it only drives the leaf runner's
|
|
667
|
+
* `_score` projection and relevance `$sort`, and no per-document score
|
|
668
|
+
* survives `$group`. See `resolveAggregateSearch` in `@atscript/db/agg` for
|
|
669
|
+
* the normative cross-adapter contract.
|
|
670
|
+
*/
|
|
671
|
+
function buildAggregateSearchStage(host, controls) {
|
|
672
|
+
const search = resolveAggregateSearch(controls);
|
|
673
|
+
if (!search) return;
|
|
674
|
+
return requireSearchStage(host, search.text, search.indexName, controls).stage;
|
|
675
|
+
}
|
|
650
676
|
/** Text search via $search aggregation stage. */
|
|
651
677
|
async function searchImpl(host, text, query, indexName) {
|
|
652
|
-
const plan =
|
|
653
|
-
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
678
|
+
const plan = requireSearchStage(host, text, indexName, query.controls);
|
|
654
679
|
return runSearchPipeline(host, plan.stage, query, "search", void 0, plan.classicText);
|
|
655
680
|
}
|
|
656
681
|
/** Text search with faceted count. */
|
|
657
682
|
async function searchWithCountImpl(host, text, query, indexName) {
|
|
658
|
-
const plan =
|
|
659
|
-
if (!plan) throw new Error(indexName ? `Search index "${indexName}" not found` : "No search index available");
|
|
683
|
+
const plan = requireSearchStage(host, text, indexName, query.controls);
|
|
660
684
|
return runSearchWithCountPipeline(host, plan.stage, query, "searchWithCount", void 0, plan.classicText);
|
|
661
685
|
}
|
|
662
686
|
/** Vector search via $vectorSearch aggregation stage. */
|
|
@@ -1680,13 +1704,14 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
|
|
|
1680
1704
|
}
|
|
1681
1705
|
async aggregate(query) {
|
|
1682
1706
|
const { buildAggregatePipeline, buildCountPipeline } = await import("./agg.mjs");
|
|
1707
|
+
const searchStage = buildAggregateSearchStage(this, query.controls);
|
|
1683
1708
|
if (query.controls?.$count) {
|
|
1684
|
-
const pipeline = buildCountPipeline(query);
|
|
1709
|
+
const pipeline = buildCountPipeline(query, searchStage);
|
|
1685
1710
|
this._log("aggregate (count)", pipeline);
|
|
1686
1711
|
const result = await wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1687
1712
|
return result.length > 0 ? result : [{ count: 0 }];
|
|
1688
1713
|
}
|
|
1689
|
-
const pipeline = buildAggregatePipeline(query);
|
|
1714
|
+
const pipeline = buildAggregatePipeline(query, searchStage);
|
|
1690
1715
|
this._log("aggregate", pipeline);
|
|
1691
1716
|
return wrapInvalidQuery(() => this.aggregatePipeline(pipeline).toArray());
|
|
1692
1717
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.131",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@atscript/core": "^0.1.92",
|
|
57
57
|
"@atscript/typescript": "^0.1.92",
|
|
58
58
|
"mongodb": "^6.17.0",
|
|
59
|
-
"@atscript/db": "^0.1.
|
|
59
|
+
"@atscript/db": "^0.1.131"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"postinstall": "asc -f dts",
|