@uptimizr/collector-server 2.1.0 → 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.
Files changed (135) hide show
  1. package/AGENTS.md +179 -6
  2. package/README.md +69 -0
  3. package/dist/__tests__/support/registryRequests.d.ts +2 -1
  4. package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
  5. package/dist/__tests__/support/registryRequests.js +17 -1
  6. package/dist/__tests__/support/registryRequests.js.map +1 -1
  7. package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
  8. package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
  9. package/dist/__tests__/support/subscriptionFixtures.js +17 -0
  10. package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
  11. package/dist/agentReport.d.ts +256 -0
  12. package/dist/agentReport.d.ts.map +1 -0
  13. package/dist/agentReport.js +861 -0
  14. package/dist/agentReport.js.map +1 -0
  15. package/dist/app.d.ts +13 -0
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +112 -5
  18. package/dist/app.js.map +1 -1
  19. package/dist/audit.d.ts +5 -1
  20. package/dist/audit.d.ts.map +1 -1
  21. package/dist/audit.js +24 -4
  22. package/dist/audit.js.map +1 -1
  23. package/dist/auth.d.ts +41 -1
  24. package/dist/auth.d.ts.map +1 -1
  25. package/dist/auth.js +47 -0
  26. package/dist/auth.js.map +1 -1
  27. package/dist/cli.js +99 -2
  28. package/dist/cli.js.map +1 -1
  29. package/dist/clickhouseStore.d.ts.map +1 -1
  30. package/dist/clickhouseStore.js +44 -2
  31. package/dist/clickhouseStore.js.map +1 -1
  32. package/dist/config.d.ts +49 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +21 -0
  35. package/dist/config.js.map +1 -1
  36. package/dist/connectionLimiter.d.ts +23 -0
  37. package/dist/connectionLimiter.d.ts.map +1 -0
  38. package/dist/connectionLimiter.js +24 -0
  39. package/dist/connectionLimiter.js.map +1 -0
  40. package/dist/duckdbStore.d.ts.map +1 -1
  41. package/dist/duckdbStore.js +44 -1
  42. package/dist/duckdbStore.js.map +1 -1
  43. package/dist/internalDispatch.d.ts +36 -0
  44. package/dist/internalDispatch.d.ts.map +1 -0
  45. package/dist/internalDispatch.js +50 -0
  46. package/dist/internalDispatch.js.map +1 -0
  47. package/dist/memoryStore.d.ts.map +1 -1
  48. package/dist/memoryStore.js +314 -1
  49. package/dist/memoryStore.js.map +1 -1
  50. package/dist/mssqlStore.d.ts.map +1 -1
  51. package/dist/mssqlStore.js +44 -2
  52. package/dist/mssqlStore.js.map +1 -1
  53. package/dist/postgresStore.d.ts.map +1 -1
  54. package/dist/postgresStore.js +44 -2
  55. package/dist/postgresStore.js.map +1 -1
  56. package/dist/projectMetadata.d.ts +82 -0
  57. package/dist/projectMetadata.d.ts.map +1 -0
  58. package/dist/projectMetadata.js +50 -0
  59. package/dist/projectMetadata.js.map +1 -0
  60. package/dist/routes/context.d.ts +135 -0
  61. package/dist/routes/context.d.ts.map +1 -0
  62. package/dist/routes/context.js +306 -0
  63. package/dist/routes/context.js.map +1 -0
  64. package/dist/routes/insights.d.ts +36 -0
  65. package/dist/routes/insights.d.ts.map +1 -0
  66. package/dist/routes/insights.js +810 -0
  67. package/dist/routes/insights.js.map +1 -0
  68. package/dist/routes/live.d.ts +8 -0
  69. package/dist/routes/live.d.ts.map +1 -1
  70. package/dist/routes/live.js +8 -7
  71. package/dist/routes/live.js.map +1 -1
  72. package/dist/routes/mcp.d.ts +36 -0
  73. package/dist/routes/mcp.d.ts.map +1 -0
  74. package/dist/routes/mcp.js +262 -0
  75. package/dist/routes/mcp.js.map +1 -0
  76. package/dist/routes/meta.d.ts +16 -2
  77. package/dist/routes/meta.d.ts.map +1 -1
  78. package/dist/routes/meta.js +699 -10
  79. package/dist/routes/meta.js.map +1 -1
  80. package/dist/routes/metadata.d.ts +31 -0
  81. package/dist/routes/metadata.d.ts.map +1 -0
  82. package/dist/routes/metadata.js +163 -0
  83. package/dist/routes/metadata.js.map +1 -0
  84. package/dist/routes/narrative.d.ts +36 -0
  85. package/dist/routes/narrative.d.ts.map +1 -0
  86. package/dist/routes/narrative.js +95 -0
  87. package/dist/routes/narrative.js.map +1 -0
  88. package/dist/routes/panels.d.ts +44 -0
  89. package/dist/routes/panels.d.ts.map +1 -0
  90. package/dist/routes/panels.js +108 -0
  91. package/dist/routes/panels.js.map +1 -0
  92. package/dist/routes/query-dsl.d.ts +50 -0
  93. package/dist/routes/query-dsl.d.ts.map +1 -0
  94. package/dist/routes/query-dsl.js +359 -0
  95. package/dist/routes/query-dsl.js.map +1 -0
  96. package/dist/routes/query.d.ts +36 -0
  97. package/dist/routes/query.d.ts.map +1 -1
  98. package/dist/routes/query.js +101 -13
  99. package/dist/routes/query.js.map +1 -1
  100. package/dist/routes/subscriptions.d.ts +52 -0
  101. package/dist/routes/subscriptions.d.ts.map +1 -0
  102. package/dist/routes/subscriptions.js +290 -0
  103. package/dist/routes/subscriptions.js.map +1 -0
  104. package/dist/store.d.ts +113 -2
  105. package/dist/store.d.ts.map +1 -1
  106. package/dist/subscriptions/cli.d.ts +36 -0
  107. package/dist/subscriptions/cli.d.ts.map +1 -0
  108. package/dist/subscriptions/cli.js +143 -0
  109. package/dist/subscriptions/cli.js.map +1 -0
  110. package/dist/subscriptions/delivery.d.ts +101 -0
  111. package/dist/subscriptions/delivery.d.ts.map +1 -0
  112. package/dist/subscriptions/delivery.js +165 -0
  113. package/dist/subscriptions/delivery.js.map +1 -0
  114. package/dist/subscriptions/evaluate.d.ts +119 -0
  115. package/dist/subscriptions/evaluate.d.ts.map +1 -0
  116. package/dist/subscriptions/evaluate.js +326 -0
  117. package/dist/subscriptions/evaluate.js.map +1 -0
  118. package/dist/subscriptions/scheduler.d.ts +97 -0
  119. package/dist/subscriptions/scheduler.d.ts.map +1 -0
  120. package/dist/subscriptions/scheduler.js +332 -0
  121. package/dist/subscriptions/scheduler.js.map +1 -0
  122. package/dist/subscriptions/stream.d.ts +40 -0
  123. package/dist/subscriptions/stream.d.ts.map +1 -0
  124. package/dist/subscriptions/stream.js +38 -0
  125. package/dist/subscriptions/stream.js.map +1 -0
  126. package/dist/version.d.ts +3 -0
  127. package/dist/version.d.ts.map +1 -0
  128. package/dist/version.js +25 -0
  129. package/dist/version.js.map +1 -0
  130. package/dist/webhookSignature.d.ts +64 -0
  131. package/dist/webhookSignature.d.ts.map +1 -0
  132. package/dist/webhookSignature.js +91 -0
  133. package/dist/webhookSignature.js.map +1 -0
  134. package/llms.txt +38 -1
  135. 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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"}
@@ -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;AAsBhF,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;AAoyBD;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,kBAAkB,CAAC,OAAO,CAqhDnD,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"}