@uptimizr/collector-server 2.0.1 → 2.2.0
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/AGENTS.md +187 -11
- package/README.md +91 -7
- package/dist/__tests__/support/registryRequests.d.ts +2 -1
- package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
- package/dist/__tests__/support/registryRequests.js +17 -1
- package/dist/__tests__/support/registryRequests.js.map +1 -1
- package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
- package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
- package/dist/__tests__/support/subscriptionFixtures.js +17 -0
- package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
- package/dist/agentReport.d.ts +256 -0
- package/dist/agentReport.d.ts.map +1 -0
- package/dist/agentReport.js +861 -0
- package/dist/agentReport.js.map +1 -0
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -5
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +5 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +24 -4
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +41 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +47 -0
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +133 -13
- package/dist/cli.js.map +1 -1
- package/dist/cliStore.d.ts +31 -2
- package/dist/cliStore.d.ts.map +1 -1
- package/dist/cliStore.js +34 -0
- package/dist/cliStore.js.map +1 -1
- package/dist/clickhouseStore.d.ts.map +1 -1
- package/dist/clickhouseStore.js +44 -2
- package/dist/clickhouseStore.js.map +1 -1
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/connectionLimiter.d.ts +23 -0
- package/dist/connectionLimiter.d.ts.map +1 -0
- package/dist/connectionLimiter.js +24 -0
- package/dist/connectionLimiter.js.map +1 -0
- package/dist/duckdbStore.d.ts.map +1 -1
- package/dist/duckdbStore.js +44 -1
- package/dist/duckdbStore.js.map +1 -1
- package/dist/internalDispatch.d.ts +36 -0
- package/dist/internalDispatch.d.ts.map +1 -0
- package/dist/internalDispatch.js +50 -0
- package/dist/internalDispatch.js.map +1 -0
- package/dist/memoryStore.d.ts.map +1 -1
- package/dist/memoryStore.js +314 -1
- package/dist/memoryStore.js.map +1 -1
- package/dist/mssqlStore.d.ts.map +1 -1
- package/dist/mssqlStore.js +44 -2
- package/dist/mssqlStore.js.map +1 -1
- package/dist/postgresStore.d.ts.map +1 -1
- package/dist/postgresStore.js +44 -2
- package/dist/postgresStore.js.map +1 -1
- package/dist/projectMetadata.d.ts +82 -0
- package/dist/projectMetadata.d.ts.map +1 -0
- package/dist/projectMetadata.js +50 -0
- package/dist/projectMetadata.js.map +1 -0
- package/dist/routes/context.d.ts +135 -0
- package/dist/routes/context.d.ts.map +1 -0
- package/dist/routes/context.js +306 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/insights.d.ts +36 -0
- package/dist/routes/insights.d.ts.map +1 -0
- package/dist/routes/insights.js +810 -0
- package/dist/routes/insights.js.map +1 -0
- package/dist/routes/live.d.ts +8 -0
- package/dist/routes/live.d.ts.map +1 -1
- package/dist/routes/live.js +8 -7
- package/dist/routes/live.js.map +1 -1
- package/dist/routes/mcp.d.ts +36 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +262 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/meta.d.ts +16 -2
- package/dist/routes/meta.d.ts.map +1 -1
- package/dist/routes/meta.js +699 -10
- package/dist/routes/meta.js.map +1 -1
- package/dist/routes/metadata.d.ts +31 -0
- package/dist/routes/metadata.d.ts.map +1 -0
- package/dist/routes/metadata.js +163 -0
- package/dist/routes/metadata.js.map +1 -0
- package/dist/routes/narrative.d.ts +36 -0
- package/dist/routes/narrative.d.ts.map +1 -0
- package/dist/routes/narrative.js +95 -0
- package/dist/routes/narrative.js.map +1 -0
- package/dist/routes/panels.d.ts +44 -0
- package/dist/routes/panels.d.ts.map +1 -0
- package/dist/routes/panels.js +108 -0
- package/dist/routes/panels.js.map +1 -0
- package/dist/routes/query-dsl.d.ts +50 -0
- package/dist/routes/query-dsl.d.ts.map +1 -0
- package/dist/routes/query-dsl.js +359 -0
- package/dist/routes/query-dsl.js.map +1 -0
- package/dist/routes/query.d.ts +36 -0
- package/dist/routes/query.d.ts.map +1 -1
- package/dist/routes/query.js +101 -13
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/subscriptions.d.ts +52 -0
- package/dist/routes/subscriptions.d.ts.map +1 -0
- package/dist/routes/subscriptions.js +290 -0
- package/dist/routes/subscriptions.js.map +1 -0
- package/dist/store.d.ts +113 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/subscriptions/cli.d.ts +36 -0
- package/dist/subscriptions/cli.d.ts.map +1 -0
- package/dist/subscriptions/cli.js +143 -0
- package/dist/subscriptions/cli.js.map +1 -0
- package/dist/subscriptions/delivery.d.ts +101 -0
- package/dist/subscriptions/delivery.d.ts.map +1 -0
- package/dist/subscriptions/delivery.js +165 -0
- package/dist/subscriptions/delivery.js.map +1 -0
- package/dist/subscriptions/evaluate.d.ts +119 -0
- package/dist/subscriptions/evaluate.d.ts.map +1 -0
- package/dist/subscriptions/evaluate.js +326 -0
- package/dist/subscriptions/evaluate.js.map +1 -0
- package/dist/subscriptions/scheduler.d.ts +97 -0
- package/dist/subscriptions/scheduler.d.ts.map +1 -0
- package/dist/subscriptions/scheduler.js +332 -0
- package/dist/subscriptions/scheduler.js.map +1 -0
- package/dist/subscriptions/stream.d.ts +40 -0
- package/dist/subscriptions/stream.d.ts.map +1 -0
- package/dist/subscriptions/stream.js +38 -0
- package/dist/subscriptions/stream.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/dist/webhookSignature.d.ts +64 -0
- package/dist/webhookSignature.d.ts.map +1 -0
- package/dist/webhookSignature.js +91 -0
- package/dist/webhookSignature.js.map +1 -0
- package/llms.txt +39 -2
- package/package.json +17 -14
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The query DSL endpoint** (ADR 0051 §3, design sketch §C.3).
|
|
3
|
+
*
|
|
4
|
+
* `POST /api/v1/query` with the JSON document, and `GET /api/v1/query?q=<url-encoded JSON>`
|
|
5
|
+
* for the GET-only clients (the collector client in `@uptimizr/agent-core`, a
|
|
6
|
+
* simple MCP host, `curl`). Both are reads and both need the ordinary `query`
|
|
7
|
+
* capability — the DSL reaches exactly the aggregations the canned endpoints
|
|
8
|
+
* already serve, so it grants nothing new.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is a separate plugin
|
|
11
|
+
*
|
|
12
|
+
* `routes/query.ts` is seventy hand-written routes, one per metric. This is one
|
|
13
|
+
* route that can run any of them. It shares their machinery rather than
|
|
14
|
+
* duplicating it — the same region/`cellSize` resolution helpers, the same
|
|
15
|
+
* summariser, the same auth and the same audit hooks — but it has no business
|
|
16
|
+
* being appended to that file.
|
|
17
|
+
*
|
|
18
|
+
* ## The request's journey
|
|
19
|
+
*
|
|
20
|
+
* 1. **Shape** — `queryV1Schema` (`@uptimizr/schema`). Closed grammar, bounded
|
|
21
|
+
* values, unknown keys rejected. No raw SQL can be expressed.
|
|
22
|
+
* 2. **Vocabulary** — `validateQuery` (`@uptimizr/metrics`). Does the metric
|
|
23
|
+
* exist, is it an aggregation, does it accept these dimensions and filters,
|
|
24
|
+
* is the limit within its cap. Every objection is collected, so one `400`
|
|
25
|
+
* tells the caller everything that is wrong, with the accepted values named.
|
|
26
|
+
* 3. **Resolution** — a `region` given as a registered region id becomes bounds,
|
|
27
|
+
* and a spatial metric with no `cellSize` gets the one derived from the
|
|
28
|
+
* scene's registered extent (ADR 0040 §1), exactly as the canned routes do.
|
|
29
|
+
* 4. **Compilation** — `store.runMetric` renders the metric's own builder for
|
|
30
|
+
* the store's dialect and runs it. No second SQL path exists.
|
|
31
|
+
* 5. **Shaping** — `format` (`full` | `table` | `summary`), applied here with
|
|
32
|
+
* the same pure `@uptimizr/db` functions the canned routes' `preSerialization`
|
|
33
|
+
* hook uses. The DSL defaults to `table`: it is an agent-facing surface, and
|
|
34
|
+
* rows without the window, the sample size and the truncation flag are
|
|
35
|
+
* exactly what the envelope exists to prevent.
|
|
36
|
+
*/
|
|
37
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
38
|
+
import type { CollectorStore } from "../store.js";
|
|
39
|
+
interface Options {
|
|
40
|
+
store: CollectorStore;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The query DSL routes. Registered as its own plugin in `app.ts`, after
|
|
44
|
+
* `queryRoutes` (whose `format` hook it deliberately does not rely on — this
|
|
45
|
+
* route shapes its own response, because it knows its filters and window
|
|
46
|
+
* first-hand rather than having to read them back off a querystring).
|
|
47
|
+
*/
|
|
48
|
+
export declare const queryDslRoutes: FastifyPluginAsync<Options>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=query-dsl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-dsl.d.ts","sourceRoot":"","sources":["../../src/routes/query-dsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAiChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIlD,UAAU,OAAO;IACf,KAAK,EAAE,cAAc,CAAC;CACvB;AAsVD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,kBAAkB,CAAC,OAAO,CAkCtD,CAAC"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The query DSL endpoint** (ADR 0051 §3, design sketch §C.3).
|
|
3
|
+
*
|
|
4
|
+
* `POST /api/v1/query` with the JSON document, and `GET /api/v1/query?q=<url-encoded JSON>`
|
|
5
|
+
* for the GET-only clients (the collector client in `@uptimizr/agent-core`, a
|
|
6
|
+
* simple MCP host, `curl`). Both are reads and both need the ordinary `query`
|
|
7
|
+
* capability — the DSL reaches exactly the aggregations the canned endpoints
|
|
8
|
+
* already serve, so it grants nothing new.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is a separate plugin
|
|
11
|
+
*
|
|
12
|
+
* `routes/query.ts` is seventy hand-written routes, one per metric. This is one
|
|
13
|
+
* route that can run any of them. It shares their machinery rather than
|
|
14
|
+
* duplicating it — the same region/`cellSize` resolution helpers, the same
|
|
15
|
+
* summariser, the same auth and the same audit hooks — but it has no business
|
|
16
|
+
* being appended to that file.
|
|
17
|
+
*
|
|
18
|
+
* ## The request's journey
|
|
19
|
+
*
|
|
20
|
+
* 1. **Shape** — `queryV1Schema` (`@uptimizr/schema`). Closed grammar, bounded
|
|
21
|
+
* values, unknown keys rejected. No raw SQL can be expressed.
|
|
22
|
+
* 2. **Vocabulary** — `validateQuery` (`@uptimizr/metrics`). Does the metric
|
|
23
|
+
* exist, is it an aggregation, does it accept these dimensions and filters,
|
|
24
|
+
* is the limit within its cap. Every objection is collected, so one `400`
|
|
25
|
+
* tells the caller everything that is wrong, with the accepted values named.
|
|
26
|
+
* 3. **Resolution** — a `region` given as a registered region id becomes bounds,
|
|
27
|
+
* and a spatial metric with no `cellSize` gets the one derived from the
|
|
28
|
+
* scene's registered extent (ADR 0040 §1), exactly as the canned routes do.
|
|
29
|
+
* 4. **Compilation** — `store.runMetric` renders the metric's own builder for
|
|
30
|
+
* the store's dialect and runs it. No second SQL path exists.
|
|
31
|
+
* 5. **Shaping** — `format` (`full` | `table` | `summary`), applied here with
|
|
32
|
+
* the same pure `@uptimizr/db` functions the canned routes' `preSerialization`
|
|
33
|
+
* hook uses. The DSL defaults to `table`: it is an agent-facing surface, and
|
|
34
|
+
* rows without the window, the sample size and the truncation flag are
|
|
35
|
+
* exactly what the envelope exists to prevent.
|
|
36
|
+
*/
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import { queryV1Schema } from "@uptimizr/schema";
|
|
39
|
+
import { ORDER_AFTER_CAP_CAVEAT, applyOrder, channelRows, compareRows, comparisonKeys, explainQuery, queryEnvelopeSchema, reordersCappedResult, summarizeComparison, summarizeRows, tableResult, toBuilderOptions, } from "@uptimizr/db";
|
|
40
|
+
import { dimensionColumn, validateQuery, } from "@uptimizr/metrics";
|
|
41
|
+
import { requireCapability } from "../auth.js";
|
|
42
|
+
import { computeSpatialCellSize, isRegionError, resolveRegionFilter } from "./query.js";
|
|
43
|
+
/**
|
|
44
|
+
* `GET /api/v1/query?q=…`: the whole document, URL-encoded, in one parameter.
|
|
45
|
+
*
|
|
46
|
+
* Bounded at 8 KiB so a malformed or hostile `q` is rejected before it is
|
|
47
|
+
* parsed — well above any real query (the largest is a twenty-step funnel) and
|
|
48
|
+
* well below what a proxy will carry in a URL.
|
|
49
|
+
*/
|
|
50
|
+
const getQueryParams = z.object({
|
|
51
|
+
q: z.string().min(1).max(8192),
|
|
52
|
+
});
|
|
53
|
+
/** `{ error, issues? }` — the one shape every rejection uses. */
|
|
54
|
+
const badRequestResponse = z.object({
|
|
55
|
+
error: z.string(),
|
|
56
|
+
issues: z
|
|
57
|
+
.array(z.object({
|
|
58
|
+
code: z.string(),
|
|
59
|
+
path: z.string(),
|
|
60
|
+
message: z.string(),
|
|
61
|
+
accepted: z.array(z.string()).optional(),
|
|
62
|
+
}))
|
|
63
|
+
.optional(),
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* The 200 body: the same three envelopes every registry-served endpoint can
|
|
67
|
+
* answer with, over a row shape that is only known at request time (it is the
|
|
68
|
+
* metric's). The row is therefore a loose object — the registry's own `row`
|
|
69
|
+
* schema is what *describes* the columns, and it reaches the caller through
|
|
70
|
+
* `GET /api/v1/openapi.json` and the metric's canned endpoint rather than by
|
|
71
|
+
* being restated here.
|
|
72
|
+
*/
|
|
73
|
+
const looseRow = z.looseObject({});
|
|
74
|
+
const queryResponse = queryEnvelopeSchema(z.array(looseRow), looseRow);
|
|
75
|
+
/**
|
|
76
|
+
* Parse the `q` parameter of the GET form. A JSON syntax error is the caller's,
|
|
77
|
+
* so it is a `400` with the parser's own message rather than a 500.
|
|
78
|
+
*/
|
|
79
|
+
function parseGetQuery(raw) {
|
|
80
|
+
try {
|
|
81
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return { ok: false, error: `q is not valid JSON: ${err instanceof Error ? err.message : ""}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Everything the summariser needs about the request that produced the rows.
|
|
89
|
+
* The DSL knows all of it first-hand: the range is required, the filters are
|
|
90
|
+
* already a typed object, and the effective row cap is the query's `limit` or
|
|
91
|
+
* the metric's registry cap.
|
|
92
|
+
*/
|
|
93
|
+
function summaryContextFor(metric, query, cellSize, caveats = []) {
|
|
94
|
+
return {
|
|
95
|
+
range: { since: query.range.since, until: query.range.until },
|
|
96
|
+
filters: { ...(query.filters ?? {}) },
|
|
97
|
+
limit: query.limit ?? metric.limits.maxRows,
|
|
98
|
+
cellSize,
|
|
99
|
+
// Given the query, every ranked row carries the *query* that narrows to it
|
|
100
|
+
// rather than a bag of filter names (#304) — see `drillQueryFor`.
|
|
101
|
+
query: { ...query },
|
|
102
|
+
...(caveats.length > 0 ? { caveats } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The dimensions a query actually groups by: its own, or the metric's grain. */
|
|
106
|
+
function effectiveDimensions(metric, query) {
|
|
107
|
+
return query.dimensions ?? metric.grainDimensions;
|
|
108
|
+
}
|
|
109
|
+
/** The same query with its range and segment replaced by the comparison's. */
|
|
110
|
+
function comparisonQuery(query) {
|
|
111
|
+
if (query.compare == null)
|
|
112
|
+
return query;
|
|
113
|
+
if ("range" in query.compare)
|
|
114
|
+
return { ...query, range: query.compare.range };
|
|
115
|
+
return { ...query, segment: query.compare.segment };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Per-event-type counts over the query's window, used for the two things
|
|
119
|
+
* `explain` can only answer with them: which capture channels are silent, and
|
|
120
|
+
* how many events of this metric's own channels exist at all.
|
|
121
|
+
*
|
|
122
|
+
* Run through `event_counts` — the metric whose whole job this is — so it takes
|
|
123
|
+
* the same compiled path, the same coercion and the same parity coverage as
|
|
124
|
+
* every other read. A failure here must never fail the explain: a plan without
|
|
125
|
+
* channel warnings is still worth more than a 500.
|
|
126
|
+
*/
|
|
127
|
+
async function channelCounts(store, projectId, query) {
|
|
128
|
+
try {
|
|
129
|
+
const rows = await store.runMetric(projectId, "event_counts", {
|
|
130
|
+
since: query.range.since,
|
|
131
|
+
until: query.range.until,
|
|
132
|
+
...(query.filters?.scene != null ? { scene: query.filters.scene } : {}),
|
|
133
|
+
});
|
|
134
|
+
const counts = {};
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
const type = row.event_type;
|
|
137
|
+
const count = row.count;
|
|
138
|
+
if (typeof type === "string" && typeof count === "number")
|
|
139
|
+
counts[type] = count;
|
|
140
|
+
}
|
|
141
|
+
return counts;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether the project has anything a spatial hotspot could be *named* after.
|
|
149
|
+
* Only asked for a binned or voxelised metric, and never allowed to fail the
|
|
150
|
+
* request.
|
|
151
|
+
*/
|
|
152
|
+
async function spatialLabels(store, projectId, metric, query) {
|
|
153
|
+
if (metric.grain !== "bin" && metric.grain !== "voxel")
|
|
154
|
+
return undefined;
|
|
155
|
+
try {
|
|
156
|
+
const scene = query.filters?.scene;
|
|
157
|
+
const proxies = await store.listSceneRepresentations(projectId);
|
|
158
|
+
const regions = await store.listSceneRegions(projectId);
|
|
159
|
+
const matches = (id) => scene == null || id === scene;
|
|
160
|
+
return {
|
|
161
|
+
proxy: proxies.some((representation) => matches(representation.sceneId)),
|
|
162
|
+
regions: regions.filter((region) => matches(region.sceneId)).length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Build the `explain: true` plan for a query, without running it. */
|
|
170
|
+
async function explainOnly(store, projectId, metric, query, tier, options) {
|
|
171
|
+
const described = store.describeMetric(projectId, metric.id, options);
|
|
172
|
+
const counts = await channelCounts(store, projectId, query);
|
|
173
|
+
const spatial = await spatialLabels(store, projectId, metric, query);
|
|
174
|
+
// `explain` deliberately does not run the query, so the sample it reports is
|
|
175
|
+
// the *window's* — how many events of this metric's own capture channels exist
|
|
176
|
+
// to answer from — rather than the result's. That is the number the
|
|
177
|
+
// below-minimum warning should weigh anyway, and it costs the one
|
|
178
|
+
// `event_counts` pass already made rather than a second run of the query.
|
|
179
|
+
const sampleSize = { sessions: null, events: channelRows(metric, counts) };
|
|
180
|
+
const context = {
|
|
181
|
+
tier,
|
|
182
|
+
dialect: described?.dialect ?? "none",
|
|
183
|
+
...(counts != null ? { channelCounts: counts } : {}),
|
|
184
|
+
sampleSize,
|
|
185
|
+
...(spatial != null ? { spatial } : {}),
|
|
186
|
+
extra: described == null
|
|
187
|
+
? ["This store compiles no SQL, so the plan carries the warnings only."]
|
|
188
|
+
: [],
|
|
189
|
+
};
|
|
190
|
+
const spec = described?.spec ?? { query: "", query_params: {} };
|
|
191
|
+
return explainQuery(metric, spec, context);
|
|
192
|
+
}
|
|
193
|
+
/** Run a `compare` query: the same spec twice, joined in TypeScript. */
|
|
194
|
+
async function runComparison(store, projectId, metric, query, tier, resolved, options) {
|
|
195
|
+
const other = comparisonQuery(query);
|
|
196
|
+
const previousOptions = toBuilderOptions(other, resolved, tier);
|
|
197
|
+
const [current, previous] = await Promise.all([
|
|
198
|
+
store.runMetric(projectId, metric.id, options),
|
|
199
|
+
store.runMetric(projectId, metric.id, previousOptions),
|
|
200
|
+
]);
|
|
201
|
+
const columns = effectiveDimensions(metric, query).map((dimension) => dimensionColumn(metric, dimension));
|
|
202
|
+
const context = {
|
|
203
|
+
basis: query.compare != null && "range" in query.compare ? "range" : "segment",
|
|
204
|
+
keys: comparisonKeys(metric, columns),
|
|
205
|
+
currentRange: { since: query.range.since, until: query.range.until },
|
|
206
|
+
previousRange: { since: other.range.since, until: other.range.until },
|
|
207
|
+
currentSegment: query.segment,
|
|
208
|
+
previousSegment: other.segment,
|
|
209
|
+
limit: query.limit ?? metric.limits.maxRows,
|
|
210
|
+
};
|
|
211
|
+
const comparison = compareRows(metric, current, previous, context);
|
|
212
|
+
if (comparison == null)
|
|
213
|
+
return current;
|
|
214
|
+
if (query.format === "summary") {
|
|
215
|
+
return summarizeComparison(metric, comparison, { maxRows: metric.limits.maxSummaryRows });
|
|
216
|
+
}
|
|
217
|
+
// `full` gives the joined rows on their own; `table` keeps the envelope that
|
|
218
|
+
// says which windows they came from and whether either was truncated.
|
|
219
|
+
return query.format === "full" ? comparison.rows : comparison;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the two values a query cannot carry on its own: a registered region
|
|
223
|
+
* id's bounds, and the voxel `cellSize` a spatial metric should use when the
|
|
224
|
+
* caller did not pin one. Returns the resolution, or an error string for a
|
|
225
|
+
* `400` (an unknown region id must never be answered as "no hits here").
|
|
226
|
+
*/
|
|
227
|
+
async function resolveQuery(store, projectId, query) {
|
|
228
|
+
const filters = query.filters ?? {};
|
|
229
|
+
const resolution = {};
|
|
230
|
+
if (filters.region != null) {
|
|
231
|
+
const filter = typeof filters.region === "string"
|
|
232
|
+
? { kind: "id", id: filters.region }
|
|
233
|
+
: { kind: "box", bounds: filters.region };
|
|
234
|
+
const bounds = await resolveRegionFilter(store, projectId, filters.scene, filter);
|
|
235
|
+
if (isRegionError(bounds))
|
|
236
|
+
return bounds;
|
|
237
|
+
resolution.region = bounds;
|
|
238
|
+
}
|
|
239
|
+
// Only ask for a derived cell size when the metric can take one and the
|
|
240
|
+
// caller left it open; `computeSpatialCellSize` returns the pinned value
|
|
241
|
+
// otherwise, and `undefined` when nothing is known (the builder's own default).
|
|
242
|
+
const cellSize = await computeSpatialCellSize(store, projectId, {
|
|
243
|
+
cellSize: filters.cellSize,
|
|
244
|
+
scene: filters.scene,
|
|
245
|
+
region: resolution.region,
|
|
246
|
+
});
|
|
247
|
+
if (cellSize != null)
|
|
248
|
+
resolution.cellSize = cellSize;
|
|
249
|
+
return resolution;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Run one validated query and shape the response. Shared by both transports so
|
|
253
|
+
* `POST` and `GET` cannot drift apart: the body and the decoded `q` are the same
|
|
254
|
+
* document, and everything after parsing is identical.
|
|
255
|
+
*/
|
|
256
|
+
async function runQuery(store, request, reply, input) {
|
|
257
|
+
const parsed = queryV1Schema.safeParse(input);
|
|
258
|
+
if (!parsed.success) {
|
|
259
|
+
return reply.code(400).send({
|
|
260
|
+
error: "the query is not a valid queryV1 document",
|
|
261
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
262
|
+
code: issue.code,
|
|
263
|
+
path: issue.path.join("."),
|
|
264
|
+
message: issue.message,
|
|
265
|
+
})),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const query = parsed.data;
|
|
269
|
+
// What the audit log records for this request: the querystring is empty on the
|
|
270
|
+
// POST form, so the query itself is the only honest description of it.
|
|
271
|
+
request.auditParams = {
|
|
272
|
+
metric: query.metric,
|
|
273
|
+
since: query.range.since,
|
|
274
|
+
until: query.range.until,
|
|
275
|
+
format: query.format,
|
|
276
|
+
...(query.limit != null ? { limit: query.limit } : {}),
|
|
277
|
+
// Stage 2 fields, so the log says what was actually asked for: an `explain`
|
|
278
|
+
// ran no aggregation and a `compare` ran two (#304).
|
|
279
|
+
...(query.explain ? { explain: true } : {}),
|
|
280
|
+
...(query.compare != null ? { compare: "range" in query.compare ? "range" : "segment" } : {}),
|
|
281
|
+
...(query.dimensions != null ? { dimensions: query.dimensions.join(",") } : {}),
|
|
282
|
+
...(query.filters ?? {}),
|
|
283
|
+
};
|
|
284
|
+
const { issues, metric, tier } = validateQuery(query);
|
|
285
|
+
if (issues.length > 0 || !metric || tier == null) {
|
|
286
|
+
return reply.code(400).send({
|
|
287
|
+
error: `the query cannot be answered: ${issues[0]?.message ?? "unknown metric"}`,
|
|
288
|
+
issues: issues.map((issue) => ({ ...issue })),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const projectId = request.resolvedKey?.projectId;
|
|
292
|
+
if (projectId == null)
|
|
293
|
+
return reply.code(401).send({ error: "missing or unknown API key" });
|
|
294
|
+
const resolved = await resolveQuery(store, projectId, query);
|
|
295
|
+
if ("error" in resolved)
|
|
296
|
+
return reply.code(400).send({ error: resolved.error });
|
|
297
|
+
const options = toBuilderOptions(query, resolved, tier);
|
|
298
|
+
// `explain` answers with the plan instead of the rows, so it must come before
|
|
299
|
+
// anything that runs one.
|
|
300
|
+
if (query.explain) {
|
|
301
|
+
return explainOnly(store, projectId, metric, query, tier, options);
|
|
302
|
+
}
|
|
303
|
+
if (query.compare != null) {
|
|
304
|
+
return runComparison(store, projectId, metric, query, tier, resolved, options);
|
|
305
|
+
}
|
|
306
|
+
const raw = await store.runMetric(projectId, metric.id, options);
|
|
307
|
+
// The generic tier put `order` in its own `ORDER BY`; a delegated metric's
|
|
308
|
+
// builder did not, so the rows are re-sorted here — honestly, with a caveat
|
|
309
|
+
// when the builder's own cap had already chosen which rows exist.
|
|
310
|
+
const reorder = tier === "delegated" ? query.order : undefined;
|
|
311
|
+
const rows = reorder == null ? raw : applyOrder(raw, reorder);
|
|
312
|
+
const caveats = reorder != null && reordersCappedResult(rows, query.limit ?? metric.limits.maxRows)
|
|
313
|
+
? [ORDER_AFTER_CAP_CAVEAT]
|
|
314
|
+
: [];
|
|
315
|
+
if (query.format === "full")
|
|
316
|
+
return rows;
|
|
317
|
+
const context = summaryContextFor(metric, query, resolved.cellSize, caveats);
|
|
318
|
+
const shaped = query.format === "table"
|
|
319
|
+
? tableResult(metric, rows, context)
|
|
320
|
+
: summarizeRows(metric, rows, context);
|
|
321
|
+
// `null` only comes back for a metric the registry does not know, which
|
|
322
|
+
// `validateQuery` already excluded — fall back to the rows rather than fail.
|
|
323
|
+
return shaped ?? rows;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The query DSL routes. Registered as its own plugin in `app.ts`, after
|
|
327
|
+
* `queryRoutes` (whose `format` hook it deliberately does not rely on — this
|
|
328
|
+
* route shapes its own response, because it knows its filters and window
|
|
329
|
+
* first-hand rather than having to read them back off a querystring).
|
|
330
|
+
*/
|
|
331
|
+
export const queryDslRoutes = async (app, { store }) => {
|
|
332
|
+
const r = app.withTypeProvider();
|
|
333
|
+
r.post("/api/v1/query", {
|
|
334
|
+
schema: {
|
|
335
|
+
body: z.unknown(),
|
|
336
|
+
response: { 200: queryResponse, 400: badRequestResponse },
|
|
337
|
+
},
|
|
338
|
+
}, async (req, reply) => {
|
|
339
|
+
// Validated by `queryV1Schema` inside `runQuery` rather than by the route
|
|
340
|
+
// schema, so the POST and GET forms produce byte-identical error bodies.
|
|
341
|
+
if (!(await requireCapability(req, reply, store, "query")))
|
|
342
|
+
return reply;
|
|
343
|
+
return runQuery(store, req, reply, req.body);
|
|
344
|
+
});
|
|
345
|
+
r.get("/api/v1/query", {
|
|
346
|
+
schema: {
|
|
347
|
+
querystring: getQueryParams,
|
|
348
|
+
response: { 200: queryResponse, 400: badRequestResponse },
|
|
349
|
+
},
|
|
350
|
+
}, async (req, reply) => {
|
|
351
|
+
if (!(await requireCapability(req, reply, store, "query")))
|
|
352
|
+
return reply;
|
|
353
|
+
const decoded = parseGetQuery(req.query.q);
|
|
354
|
+
if (!decoded.ok)
|
|
355
|
+
return reply.code(400).send({ error: decoded.error });
|
|
356
|
+
return runQuery(store, req, reply, decoded.value);
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
//# sourceMappingURL=query-dsl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-dsl.js","sourceRoot":"","sources":["../../src/routes/query-dsl.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/query.d.ts
CHANGED
|
@@ -1,10 +1,46 @@
|
|
|
1
1
|
import type { FastifyPluginAsync } from "fastify";
|
|
2
|
+
import { type WorldAabb } from "@uptimizr/db";
|
|
2
3
|
import type { CollectorConfig } from "../config.js";
|
|
3
4
|
import type { CollectorStore } from "../store.js";
|
|
4
5
|
interface Options {
|
|
5
6
|
store: CollectorStore;
|
|
6
7
|
config: CollectorConfig;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A parsed `region` querystring value: either an explicit box or the id of a
|
|
11
|
+
* region registered in the scene registry (ADR 0051 §2), which the route
|
|
12
|
+
* resolves to that region's stored bounds before the store ever sees it.
|
|
13
|
+
*/
|
|
14
|
+
export type RegionFilter = {
|
|
15
|
+
kind: "box";
|
|
16
|
+
bounds: WorldAabb;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "id";
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
/** Why a `region=<id>` filter could not be resolved, for the 400 body. */
|
|
22
|
+
export type RegionResolutionError = {
|
|
23
|
+
error: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function isRegionError(value: unknown): value is RegionResolutionError;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a parsed `region` filter to the world box the aggregations take.
|
|
28
|
+
*
|
|
29
|
+
* A box passes straight through. A **region id** is looked up in the scene
|
|
30
|
+
* registry: regions are keyed by `(project, scene, region)`, so the request must
|
|
31
|
+
* also name the `scene` the region belongs to, and an id the project has never
|
|
32
|
+
* registered is a client error (`400`) rather than a silently unfiltered query —
|
|
33
|
+
* an agent must never be told "no hits in the entrance" when it merely misspelt
|
|
34
|
+
* the region. Returns the box, `undefined` when no filter was given, or an error
|
|
35
|
+
* object the caller turns into a `400`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveRegionFilter(store: CollectorStore, projectId: string, scene: string | undefined, filter: RegionFilter | undefined): Promise<WorldAabb | undefined | RegionResolutionError>;
|
|
38
|
+
/** The resolution rule itself; see {@link resolveSpatialCellSize}. */
|
|
39
|
+
export declare function computeSpatialCellSize(store: CollectorStore, projectId: string, opts: {
|
|
40
|
+
cellSize?: number;
|
|
41
|
+
scene?: string;
|
|
42
|
+
region?: WorldAabb;
|
|
43
|
+
}): Promise<number | undefined>;
|
|
8
44
|
/**
|
|
9
45
|
* Query API. All aggregations are computed at query time (v1). Every route is
|
|
10
46
|
* scoped to the authenticated project.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/routes/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/routes/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAWhF,OAAO,EAcL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,UAAU,OAAO;IACf,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,eAAe,CAAC;CACzB;AAsCD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AA6D3F,0EAA0E;AAC1E,MAAM,MAAM,qBAAqB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,qBAAqB,CAE5E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,YAAY,GAAG,SAAS,GAC/B,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,qBAAqB,CAAC,CAYxD;AAkCD,sEAAsE;AACtE,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GAC9D,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AA2qBD;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,OAAO,CAijDnD,CAAC"}
|