@uptimizr/db 2.0.1 → 2.1.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 +228 -3
- package/README.md +13 -0
- package/dist/duckdb/migrations.d.ts.map +1 -1
- package/dist/duckdb/migrations.js +169 -0
- package/dist/duckdb/migrations.js.map +1 -1
- package/dist/duckdb/panelSpecs.d.ts +29 -0
- package/dist/duckdb/panelSpecs.d.ts.map +1 -0
- package/dist/duckdb/panelSpecs.js +104 -0
- package/dist/duckdb/panelSpecs.js.map +1 -0
- package/dist/duckdb/projectMetadata.d.ts +43 -0
- package/dist/duckdb/projectMetadata.d.ts.map +1 -0
- package/dist/duckdb/projectMetadata.js +222 -0
- package/dist/duckdb/projectMetadata.js.map +1 -0
- package/dist/duckdb/subscriptions.d.ts +45 -0
- package/dist/duckdb/subscriptions.d.ts.map +1 -0
- package/dist/duckdb/subscriptions.js +202 -0
- package/dist/duckdb/subscriptions.js.map +1 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -3
- package/dist/index.js.map +1 -1
- package/dist/insights/anomalies.d.ts +213 -0
- package/dist/insights/anomalies.d.ts.map +1 -0
- package/dist/insights/anomalies.js +340 -0
- package/dist/insights/anomalies.js.map +1 -0
- package/dist/insights/baseline.d.ts +51 -0
- package/dist/insights/baseline.d.ts.map +1 -0
- package/dist/insights/baseline.js +48 -0
- package/dist/insights/baseline.js.map +1 -0
- package/dist/insights/buckets.d.ts +114 -0
- package/dist/insights/buckets.d.ts.map +1 -0
- package/dist/insights/buckets.js +220 -0
- package/dist/insights/buckets.js.map +1 -0
- package/dist/insights/changepoint.d.ts +130 -0
- package/dist/insights/changepoint.d.ts.map +1 -0
- package/dist/insights/changepoint.js +209 -0
- package/dist/insights/changepoint.js.map +1 -0
- package/dist/insights/evaluate.d.ts +63 -0
- package/dist/insights/evaluate.d.ts.map +1 -0
- package/dist/insights/evaluate.js +158 -0
- package/dist/insights/evaluate.js.map +1 -0
- package/dist/insights/health.d.ts +219 -0
- package/dist/insights/health.d.ts.map +1 -0
- package/dist/insights/health.js +349 -0
- package/dist/insights/health.js.map +1 -0
- package/dist/insights/index.d.ts +45 -0
- package/dist/insights/index.d.ts.map +1 -0
- package/dist/insights/index.js +40 -0
- package/dist/insights/index.js.map +1 -0
- package/dist/insights/measures.d.ts +222 -0
- package/dist/insights/measures.d.ts.map +1 -0
- package/dist/insights/measures.js +431 -0
- package/dist/insights/measures.js.map +1 -0
- package/dist/insights/movers.d.ts +123 -0
- package/dist/insights/movers.d.ts.map +1 -0
- package/dist/insights/movers.js +166 -0
- package/dist/insights/movers.js.map +1 -0
- package/dist/insights/significance.d.ts +221 -0
- package/dist/insights/significance.d.ts.map +1 -0
- package/dist/insights/significance.js +531 -0
- package/dist/insights/significance.js.map +1 -0
- package/dist/insights/stats.d.ts +104 -0
- package/dist/insights/stats.d.ts.map +1 -0
- package/dist/insights/stats.js +181 -0
- package/dist/insights/stats.js.map +1 -0
- package/dist/insights/windows.d.ts +95 -0
- package/dist/insights/windows.d.ts.map +1 -0
- package/dist/insights/windows.js +97 -0
- package/dist/insights/windows.js.map +1 -0
- package/dist/metadata.d.ts +178 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +80 -0
- package/dist/metadata.js.map +1 -1
- package/dist/narrative/build.d.ts +78 -0
- package/dist/narrative/build.d.ts.map +1 -0
- package/dist/narrative/build.js +437 -0
- package/dist/narrative/build.js.map +1 -0
- package/dist/narrative/index.d.ts +14 -0
- package/dist/narrative/index.d.ts.map +1 -0
- package/dist/narrative/index.js +13 -0
- package/dist/narrative/index.js.map +1 -0
- package/dist/narrative/text.d.ts +25 -0
- package/dist/narrative/text.d.ts.map +1 -0
- package/dist/narrative/text.js +41 -0
- package/dist/narrative/text.js.map +1 -0
- package/dist/parity/cases.d.ts +3 -0
- package/dist/parity/cases.d.ts.map +1 -1
- package/dist/parity/cases.js +387 -7
- package/dist/parity/cases.js.map +1 -1
- package/dist/parity/compare.d.ts +19 -0
- package/dist/parity/compare.d.ts.map +1 -1
- package/dist/parity/compare.js +19 -0
- package/dist/parity/compare.js.map +1 -1
- package/dist/parity/fixtures.d.ts.map +1 -1
- package/dist/parity/fixtures.js +25 -0
- package/dist/parity/fixtures.js.map +1 -1
- package/dist/query/aggregations.d.ts +46 -1
- package/dist/query/aggregations.d.ts.map +1 -1
- package/dist/query/aggregations.js +86 -0
- package/dist/query/aggregations.js.map +1 -1
- package/dist/query/clickhouseDialect.d.ts.map +1 -1
- package/dist/query/clickhouseDialect.js +5 -1
- package/dist/query/clickhouseDialect.js.map +1 -1
- package/dist/query/customEventVocabulary.d.ts +46 -0
- package/dist/query/customEventVocabulary.d.ts.map +1 -0
- package/dist/query/customEventVocabulary.js +127 -0
- package/dist/query/customEventVocabulary.js.map +1 -0
- package/dist/query/dsl/builders.d.ts +38 -0
- package/dist/query/dsl/builders.d.ts.map +1 -0
- package/dist/query/dsl/builders.js +35 -0
- package/dist/query/dsl/builders.js.map +1 -0
- package/dist/query/dsl/compare.d.ts +167 -0
- package/dist/query/dsl/compare.d.ts.map +1 -0
- package/dist/query/dsl/compare.js +358 -0
- package/dist/query/dsl/compare.js.map +1 -0
- package/dist/query/dsl/compile.d.ts +89 -0
- package/dist/query/dsl/compile.d.ts.map +1 -0
- package/dist/query/dsl/compile.js +174 -0
- package/dist/query/dsl/compile.js.map +1 -0
- package/dist/query/dsl/explain.d.ts +115 -0
- package/dist/query/dsl/explain.d.ts.map +1 -0
- package/dist/query/dsl/explain.js +143 -0
- package/dist/query/dsl/explain.js.map +1 -0
- package/dist/query/dsl/generic.d.ts +127 -0
- package/dist/query/dsl/generic.d.ts.map +1 -0
- package/dist/query/dsl/generic.js +323 -0
- package/dist/query/dsl/generic.js.map +1 -0
- package/dist/query/dsl/index.d.ts +30 -0
- package/dist/query/dsl/index.d.ts.map +1 -0
- package/dist/query/dsl/index.js +23 -0
- package/dist/query/dsl/index.js.map +1 -0
- package/dist/query/dsl/order.d.ts +41 -0
- package/dist/query/dsl/order.d.ts.map +1 -0
- package/dist/query/dsl/order.js +55 -0
- package/dist/query/dsl/order.js.map +1 -0
- package/dist/query/dsl/significance.d.ts +130 -0
- package/dist/query/dsl/significance.d.ts.map +1 -0
- package/dist/query/dsl/significance.js +224 -0
- package/dist/query/dsl/significance.js.map +1 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +8 -0
- package/dist/query/index.js.map +1 -1
- package/dist/query/summary/index.d.ts +4 -2
- package/dist/query/summary/index.d.ts.map +1 -1
- package/dist/query/summary/index.js +2 -1
- package/dist/query/summary/index.js.map +1 -1
- package/dist/query/summary/labels.d.ts +101 -0
- package/dist/query/summary/labels.d.ts.map +1 -0
- package/dist/query/summary/labels.js +261 -0
- package/dist/query/summary/labels.js.map +1 -0
- package/dist/query/summary/reading.d.ts.map +1 -1
- package/dist/query/summary/reading.js +21 -2
- package/dist/query/summary/reading.js.map +1 -1
- package/dist/query/summary/schema.d.ts +250 -20
- package/dist/query/summary/schema.d.ts.map +1 -1
- package/dist/query/summary/schema.js +135 -123
- package/dist/query/summary/schema.js.map +1 -1
- package/dist/query/summary/summarize.d.ts.map +1 -1
- package/dist/query/summary/summarize.js +91 -5
- package/dist/query/summary/summarize.js.map +1 -1
- package/dist/query/summary/types.d.ts +77 -2
- package/dist/query/summary/types.d.ts.map +1 -1
- package/dist/query/types.d.ts +47 -1
- package/dist/query/types.d.ts.map +1 -1
- package/dist/subscriptions.d.ts +208 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +153 -0
- package/dist/subscriptions.js.map +1 -0
- package/llms.txt +9 -0
- package/package.json +8 -8
package/AGENTS.md
CHANGED
|
@@ -11,7 +11,9 @@ The OSS storage contracts plus the single-file **DuckDB** store (ADR 0020):
|
|
|
11
11
|
table (hot fields promoted to columns; the full event preserved as JSON in `payload` so reads
|
|
12
12
|
stay replay-complete) plus `projects` / `api_keys` (stored only as SHA-256 hashes, each key
|
|
13
13
|
carrying a capability set, an optional label and an optional per-key rate limit) and the
|
|
14
|
-
`agent_audit` trail
|
|
14
|
+
`agent_audit` trail, the scene registry (`scene_representations`, `scene_regions`) and the
|
|
15
|
+
project-metadata tables `annotations` / `glossary` / `saved_analyses` (ADR 0051 §5) and
|
|
16
|
+
`panel_specs` (ADR 0051 §7).
|
|
15
17
|
- **Engine-neutral contracts** — the dialect-agnostic query layer (`buildX` + `Dialect`), the
|
|
16
18
|
neutral event-row mapper (`toEventRow`, `formatUtcTimestamp`), and the metadata types
|
|
17
19
|
(`Project`, `ApiKeyRecord`, `SceneRepresentation*`). An optional, separately-licensed
|
|
@@ -77,6 +79,24 @@ every tool (collector + CLIs) shares one canonical file regardless of cwd.
|
|
|
77
79
|
`toApiKeyColumns()` so ordering, validation and the per-key rate-limit columns stay consistent
|
|
78
80
|
across all four engines. `query:raw` is only ever honoured by a collector running with
|
|
79
81
|
`ENABLE_RAW_SESSION_RETENTION` (ADR 0003).
|
|
82
|
+
- **Metadata tables are writable; the events table is not.** `annotations`, `glossary`,
|
|
83
|
+
`saved_analyses` and `panel_specs` are the only rows a request can write besides events
|
|
84
|
+
(ADR 0051 §5/§7/§9). Their accessors enforce the per-project caps in `METADATA_LIMITS` at write
|
|
85
|
+
time and throw `MetadataLimitError` when a project is full — the collector turns that into a
|
|
86
|
+
`409`. Never add a path that updates or deletes an event row.
|
|
87
|
+
- **`panel_specs` is the one metadata table with an in-place update** (ADR 0051 §7). A spec is one
|
|
88
|
+
closed JSON document — a metric id, a chart name, some column names — that no read path queries
|
|
89
|
+
into, stored beside only what the store must filter (`project_id`), order (`created_at`) and
|
|
90
|
+
address (`id`). `duckdbCreatePanelSpec` / `duckdbListPanelSpecs` / `duckdbUpdatePanelSpec` /
|
|
91
|
+
`duckdbDeletePanelSpec` are the accessors; `parsePanelSpec` maps the JSON column back and returns
|
|
92
|
+
`null` for a row that is not an object, which the listing drops rather than failing the whole
|
|
93
|
+
grid on. Two things differ from the other three tables and must stay that way: the listing is
|
|
94
|
+
**oldest first**, because these are grid positions rather than a feed and a new pin must not
|
|
95
|
+
reshuffle somebody's dashboard, and an update keeps the row's id **and its original
|
|
96
|
+
`authorKind` / `authorKeyId`** — who pinned a panel is a fact about when it appeared, and an edit
|
|
97
|
+
does not change it. Migrations: DuckDB `0048_panel_specs` + `0049_panel_specs_idx`, Postgres
|
|
98
|
+
`0020_panel_specs`, SQL Server `0022_panel_specs` + `0023_panel_specs_idx`, ClickHouse
|
|
99
|
+
`0017_panel_specs`.
|
|
80
100
|
- **The audit log records key ids, never keys.** `agent_audit` rows carry `key_id`; serialize
|
|
81
101
|
parameters with `serializeAuditParams()` (drops credential-shaped keys, bounds the document)
|
|
82
102
|
before they reach a store, and clamp the endpoint with `clampAuditTool()`.
|
|
@@ -105,6 +125,26 @@ Its own **package**, not a subpath here, because this one depends on the ~37 MB
|
|
|
105
125
|
**only** `zod` plus a type-only `@uptimizr/schema` declaration, performs no I/O and holds no store
|
|
106
126
|
or dialect reference.
|
|
107
127
|
|
|
128
|
+
### Custom-event vocabulary (ADR 0051 §5)
|
|
129
|
+
|
|
130
|
+
`buildCustomEventVocabulary` is the one aggregation whose SQL output is **not** what the API serves.
|
|
131
|
+
Key _enumeration_ over an open JSON object is the single JSON operation the four supported engines
|
|
132
|
+
have no portable spelling for (`json_keys` / `JSONExtractKeys` / `jsonb_object_keys` / `OPENJSON`),
|
|
133
|
+
and `Dialect.jsonText` cannot express it — it reads a value at a _known_ path. So the query does what
|
|
134
|
+
SQL is good at (counts and distinct sessions per `custom` event name, plus that name's most recent
|
|
135
|
+
`payload` documents, bounded by `CUSTOM_EVENT_VOCABULARY_SAMPLE_ROWS` = 20 per name), and
|
|
136
|
+
`foldCustomEventVocabulary(rows)` — pure, unit-tested, no dialect — turns the sampled payloads into
|
|
137
|
+
one row per name with the union of prop keys and a coarse type each.
|
|
138
|
+
|
|
139
|
+
- Every store calls the fold in its `customEventVocabulary` method. The **raw payload never leaves
|
|
140
|
+
the store layer**: the collector serves the folded rows, and only key names and value kinds, never
|
|
141
|
+
a prop value (ADR 0003).
|
|
142
|
+
- The registry `row` for `custom_event_vocabulary` therefore describes the **folded** row; the parity
|
|
143
|
+
case compares the counts and the sampling shape and excludes `sample_payload`, which is engine
|
|
144
|
+
formatted (Postgres normalises it through `jsonb`).
|
|
145
|
+
- Do not add a `jsonKeys` member to `Dialect` to "fix" this without a good reason: four dialects would
|
|
146
|
+
have to agree on key ordering, type coercion and NULL handling for a discovery read.
|
|
147
|
+
|
|
108
148
|
### Numeric coercion at the store edge (ADR 0051 §2)
|
|
109
149
|
|
|
110
150
|
Numeric columns are strict `z.number()` — the schema describes the API, not the wire. Every
|
|
@@ -148,14 +188,42 @@ samples", not `0`, which is why nine perf/resource metrics declare nullable colu
|
|
|
148
188
|
from one `preSerialization` hook, so no route handler knows they exist.
|
|
149
189
|
|
|
150
190
|
```ts
|
|
151
|
-
import {
|
|
191
|
+
import {
|
|
192
|
+
summarizeRows,
|
|
193
|
+
tableResult,
|
|
194
|
+
clusterCells,
|
|
195
|
+
labelClusters,
|
|
196
|
+
labelPoint,
|
|
197
|
+
isWorldSpatialMetric,
|
|
198
|
+
wilsonInterval,
|
|
199
|
+
} from "@uptimizr/db/summary";
|
|
152
200
|
```
|
|
153
201
|
|
|
202
|
+
The **Zod mirrors** of the envelopes (`tableResultSchema`, `resultSummarySchema`,
|
|
203
|
+
`resultEnvelopeSchema`, `resultFormatSchema`) are still exported from the same subpath, but they
|
|
204
|
+
are now defined in [`@uptimizr/metrics`](../metrics) and re-exported here: `@uptimizr/agent-core`
|
|
205
|
+
and `@uptimizr/mcp` describe the same envelopes in their tool output schemas and may not depend on
|
|
206
|
+
this package. Change a shape **there**, never here.
|
|
207
|
+
|
|
154
208
|
The `grain` selects the shape — `ranked` (top rows + `rest`), `series` (first/last/min/max/trend/
|
|
155
209
|
slope over the `axis` column), `clusters` (`clusterCells`: deterministic greedy merge of adjacent
|
|
156
210
|
occupied cells above a density threshold, 8-neighbourhood in 2D / 26 in 3D), or `record` (the single
|
|
157
211
|
row plus its `rateOf` rates). Everything is bounded by `limits.maxSummaryRows`.
|
|
158
212
|
|
|
213
|
+
### Spatial labelling (`labels.ts`, ADR 0051 §2 / sketch §B.2)
|
|
214
|
+
|
|
215
|
+
Pass a `SummaryContext.scene` — `{ id, regions: [{ id, bounds }], meshes: [{ name, aabb }] }`, which
|
|
216
|
+
the collector loads from the scene registry once per request — and every cluster of a **world-space**
|
|
217
|
+
grid is labelled: `region` (smallest containing region by volume), `regions[]` (every containing id,
|
|
218
|
+
ascending), `nearestMesh` (a proxy box containing the centroid, else the nearest box centre within
|
|
219
|
+
`cellSize × 2`) and `distance` (world units; `0` when contained). A containing region also upgrades
|
|
220
|
+
`drill.region` from an ad-hoc world box to the region **id**, which `?region=` resolves server-side.
|
|
221
|
+
|
|
222
|
+
`isWorldSpatialMetric(metric)` says whether a metric is worth loading a scene for: only the voxel
|
|
223
|
+
(`vx/vy/vz`) and ground-bin (`gx/gz`) grids are world-space — the viewport pointer/UV bins and the
|
|
224
|
+
angular view-direction grid are not, and are left untouched. Cost is `O(clusters × boxes)` with a
|
|
225
|
+
per-axis early exit; `spatialLabels.test.ts` holds the largest shape under 50 ms.
|
|
226
|
+
|
|
159
227
|
**Rules for agents:**
|
|
160
228
|
|
|
161
229
|
- A metric's summary comes from its **column semantics**, never from a per-metric special case. If a
|
|
@@ -169,8 +237,164 @@ row plus its `rateOf` rates). Everything is bounded by `limits.maxSummaryRows`.
|
|
|
169
237
|
rows is meaningless, and a share derived from it is worse than none.
|
|
170
238
|
- Clustering must stay a pure function of the cell _set_: accumulate in sorted coordinate order so a
|
|
171
239
|
reordered input is bit-identical (`summary.test.ts` rotates every fixture).
|
|
240
|
+
- Labelling must stay a pure function of the region/mesh _sets_ too: break every tie by id or name
|
|
241
|
+
ascending, never by array order, and report `null` rather than a far-away guess.
|
|
172
242
|
- Adding `format` to a metric's `filters` and to the collector's querystring is one change — the
|
|
173
243
|
collector's `registryRoutes.test.ts` fails if they drift.
|
|
244
|
+
- The collector's `format` default is **`full`** and must stay that way — it is what keeps the
|
|
245
|
+
feature invisible to the dashboard. The generated agent tools apply their own `table` default
|
|
246
|
+
and send it explicitly (`DEFAULT_TOOL_FORMAT` in `@uptimizr/agent-core`).
|
|
247
|
+
|
|
248
|
+
## Query DSL (ADR 0051 §3)
|
|
249
|
+
|
|
250
|
+
`compileQuery(projectId, query, dialect)` turns a validated `queryV1` document (the Zod grammar in
|
|
251
|
+
`@uptimizr/schema`, the registry validation in `@uptimizr/metrics`) into an ordinary `QuerySpec`, by
|
|
252
|
+
way of the metric's **existing** builder: the registry names the `build*`, `FILTER_TARGETS` names the
|
|
253
|
+
option field each filter drives, and the builder renders what it has always rendered.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { compileMetric, compileQuery, toBuilderOptions } from "@uptimizr/db";
|
|
257
|
+
|
|
258
|
+
// A store's whole DSL implementation:
|
|
259
|
+
runMetric: (projectId, metric, options) =>
|
|
260
|
+
runDuckdbQuery(db, compileMetric(metric, projectId, options, duckdbDialect));
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- **No second SQL path.** The spec a DSL query compiles to is byte-identical to the one the canned
|
|
264
|
+
endpoint runs — `src/__tests__/queryDsl.test.ts` asserts that for every aggregation on all four
|
|
265
|
+
dialects, and `PARITY_CASES` carries `dsl:*` cases that execute compiled specs against the same
|
|
266
|
+
golden. Everything parity already proves about a builder holds for the DSL.
|
|
267
|
+
- **It does not validate.** By the time a query reaches `compileQuery` it has passed
|
|
268
|
+
`queryV1Schema` and `validateQuery`. The two `throw`s are guards against a caller skipping that.
|
|
269
|
+
- **Two values must be resolved first**, by whoever has a store: a `filters.region` given as a
|
|
270
|
+
registered region id (→ its bounds) and a spatial `cellSize` derived from the scene's extent. Pass
|
|
271
|
+
them as the `QueryResolution` argument.
|
|
272
|
+
- **The generic tier is the second compiler**, not a second _path_: `compileGenericGroupBy`
|
|
273
|
+
(`query/dsl/generic.ts`) renders `SELECT <dims>, <measures> … GROUP BY <dims>` for a metric that
|
|
274
|
+
declares `genericGroupBy`, at any grain it declares. Everything variable in that SQL comes from
|
|
275
|
+
registry data — the event types, the scope predicate, the measures, each dimension's expression —
|
|
276
|
+
and every caller-supplied value is a bound parameter. `compileMetric` dispatches on
|
|
277
|
+
`options.tier`, which only `toBuilderOptions` sets, so a store calling `runMetric` with a plain
|
|
278
|
+
option bag keeps the delegated behaviour it has always had.
|
|
279
|
+
- **Four pure layers sit on top**, none of which runs a query: `compareRows` /
|
|
280
|
+
`summarizeComparison` (join two runs of the same spec on the dimension key),
|
|
281
|
+
`twoProportionZ` / `welchT` (is the difference real — pinned to published table values in
|
|
282
|
+
`querySignificance.test.ts`), `explainQuery` (the plan and its warnings), and `applyOrder`
|
|
283
|
+
(an honest re-sort of a delegated result, with `ORDER_AFTER_CAP_CAVEAT` when the builder's own
|
|
284
|
+
cap had already chosen the rows).
|
|
285
|
+
- **`explain` shows the SQL because there is nothing in it to redact.** `explainSpec` lists
|
|
286
|
+
parameters by name and logical type and never by value; the SQL text carries placeholders only,
|
|
287
|
+
which is exactly the property a reader uses `explain` to check.
|
|
288
|
+
|
|
289
|
+
## Session narrative (ADR 0051 §7)
|
|
290
|
+
|
|
291
|
+
`buildSessionNarrative(events, opts)` compacts one session's `AnyEvent[]` into an ordered,
|
|
292
|
+
bounded account of what it did; `renderSessionNarrativeText(narrative)` renders it as one line per
|
|
293
|
+
entry. Both are **pure** — no store, no request, no I/O — and live in `src/narrative/`.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { buildSessionNarrative, renderSessionNarrativeText } from "@uptimizr/db";
|
|
297
|
+
|
|
298
|
+
const narrative = buildSessionNarrative(events, { minDwellMs: 2000, maxEntries: 200 });
|
|
299
|
+
narrative.entries; // ordered { tMs, kind, summary, refs }, ending with the `summary` entry
|
|
300
|
+
narrative.totals; // { events, durationMs, scenes, meshes, interactions, dips, errors }
|
|
301
|
+
console.log(renderSessionNarrativeText(narrative));
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Shapes, defaults and hard caps live in `@uptimizr/metrics` (`NARRATIVE_LIMITS`,
|
|
305
|
+
`sessionNarrativeEntrySchema`) so the collector route, the generated `session_narrative` tool and
|
|
306
|
+
this implementation cannot drift.
|
|
307
|
+
|
|
308
|
+
**It is an allow-list, not a redactor** (ADR 0003). It reads only: relative timestamps, scene ids,
|
|
309
|
+
mesh names, interaction kinds and input sources, custom-event/input-action names, FPS, truncated
|
|
310
|
+
runtime-error messages, diagnostic category/severity, capability transitions, and the rendering
|
|
311
|
+
engine. It never reads `visitorId`, `url`, `pageMeta`, `user`, any position/ray/UV, any other
|
|
312
|
+
`device` field, or custom-event property values (keys only, unless `includeCustomProps`). Keep it
|
|
313
|
+
that way when adding an event type — adding a field to the switch is a privacy decision.
|
|
314
|
+
|
|
315
|
+
### Insight primitives (ADR 0051 §4, `src/insights/`)
|
|
316
|
+
|
|
317
|
+
`baseline`, `movers`, `anomalies`, `significance` and `scene_health` — "what is normal here",
|
|
318
|
+
"what changed", "_when_ did it go wrong", "is that change real" and "which scene should I look at
|
|
319
|
+
first" — as five derived registry metrics. The shape of the directory is the design:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import {
|
|
323
|
+
buildMetricBuckets, // the ONE dialect-authored query both primitives consume
|
|
324
|
+
computeBaseline,
|
|
325
|
+
rankMovers,
|
|
326
|
+
resolveBaselineWindow,
|
|
327
|
+
resolveMoversWindows,
|
|
328
|
+
BUCKET_MEASURES, // metric id -> how its primary column buckets
|
|
329
|
+
BUCKETABLE_METRIC_IDS,
|
|
330
|
+
MOVERS_DEFAULT_METRICS,
|
|
331
|
+
MOVERS_MAX_METRICS,
|
|
332
|
+
detectAnomalies, // #306: robust z per bucket + CUSUM change-points
|
|
333
|
+
contributorDimensionFor, // the ONE dimension a metric's excess may be split by
|
|
334
|
+
contributorWindows, // the windows attribution is allowed to re-scan
|
|
335
|
+
attributeContributor,
|
|
336
|
+
ANOMALY_MAX_CONTRIBUTOR_SCANS,
|
|
337
|
+
// --- significance / scene health (#307) ---
|
|
338
|
+
computeSignificance, // picks the test from the measure: proportions / Poisson / Welch
|
|
339
|
+
computeSceneHealth, // six weighted factors, each traceable to its metric
|
|
340
|
+
rankSceneHealth,
|
|
341
|
+
resolveHealthWindows,
|
|
342
|
+
HEALTH_FACTORS, // the fixed factor catalog, with default weights
|
|
343
|
+
BUCKET_MEASURE_VARIANTS, // named auxiliary series: a rate denominator, an FPS tail
|
|
344
|
+
} from "@uptimizr/db";
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Rules for agents:**
|
|
348
|
+
|
|
349
|
+
- **No statistic may be computed in SQL.** Five dialects disagree about `quantile`, `median` and
|
|
350
|
+
`stddev`; an insight that changes with the storage engine is not an insight. The query returns raw
|
|
351
|
+
per-bucket values and `stats.ts` does the rest in TypeScript.
|
|
352
|
+
- `buildMetricBuckets` lives in `src/insights/`, **not** in `query/aggregations.ts`. The `build*`
|
|
353
|
+
exports of that module are the registry's closed list of _metrics_, each of which must have its own
|
|
354
|
+
entry and endpoint; this is the shared _input_ of two metrics and has neither.
|
|
355
|
+
- A metric is bucketable only if it has an entry in `measures.ts`, and that entry's `column` must be
|
|
356
|
+
the metric's `comparable.primary` (asserted in `src/__tests__/insights.test.ts`). Widening the
|
|
357
|
+
catalog is additive: add the measure, add a parity case, done.
|
|
358
|
+
- A metric may also declare **named auxiliary series** (`BUCKET_MEASURE_VARIANTS`): a rate
|
|
359
|
+
denominator, an FPS tail, the numerator of a ratio whose metric has no headline series at all.
|
|
360
|
+
A variant is **never** caller input — `significance` picks `denominator` from the registry and
|
|
361
|
+
`scene_health` reads a fixed factor catalog — and declaring one does **not** make the metric
|
|
362
|
+
bucketable: `isBucketableMetric` still answers about the metric's own headline column, so
|
|
363
|
+
`baseline` and `movers` keep rejecting `jank_rate` and `xr_abandonment` as before.
|
|
364
|
+
- **Which test `significance` runs is derived, not configured**: a declared rate (a `rateOf`
|
|
365
|
+
headline column plus a `denominator` variant) gets a two-proportion z with Wilson/Newcombe
|
|
366
|
+
intervals, a `count`/`sessions` aggregate gets an exact Poisson rate test, and everything else
|
|
367
|
+
gets Welch's t over the per-bucket values. Welch's `n` is the **bucket count**, never the event
|
|
368
|
+
count: samples inside one day are not independent.
|
|
369
|
+
- **Every `scene_health` factor must stay traceable.** A factor row carries the metric id, the raw
|
|
370
|
+
value, the project baseline it was compared with and the weight it took. A factor that could not
|
|
371
|
+
be measured reports `score: null` with a reason and is excluded from the mean — never defaulted
|
|
372
|
+
to 50, and never dropped from the row.
|
|
373
|
+
- **Never approximate a series to widen the catalog.** Funnels, cohort metrics and anything defined
|
|
374
|
+
by the relationship between consecutive events have no faithful per-bucket form; a `400` naming the
|
|
375
|
+
ids that do is a better answer than a plausible wrong number.
|
|
376
|
+
- Predicates come from a **closed vocabulary** over promoted columns, with constant values bound as
|
|
377
|
+
parameters. Do not add a free-SQL escape hatch.
|
|
378
|
+
- Window bounds snap **down** to whole buckets, so the day or hour in progress is excluded and a
|
|
379
|
+
default reference really is an equal window. `resolveHealthWindows` is the deliberate exception
|
|
380
|
+
(it rounds `until` **up**): every health factor is a rate or a percentile, which a partial bucket
|
|
381
|
+
does not distort, and flooring would make the score answer about yesterday.
|
|
382
|
+
- `movers` reports a sub-`minSample` delta with `aboveMinSample: false` and ranks it below every
|
|
383
|
+
gated mover. It must never be dropped — "we cannot tell" and "nothing changed" are different
|
|
384
|
+
answers — and never reported as a finding.
|
|
385
|
+
- Cost is bounded by `MOVERS_MAX_METRICS` (one grouped scan per scanned metric). Raising it is a
|
|
386
|
+
deliberate change, not a default.
|
|
387
|
+
- `anomalies` (#306) adds **one** piece of per-dialect SQL and no more: an optional `groupBy` on
|
|
388
|
+
`buildMetricBuckets` that adds one promoted column to the `SELECT`/`GROUP BY`. The column comes
|
|
389
|
+
from the measure's own `splitBy`, a compile-time union — never from request input — and the shape
|
|
390
|
+
has its own `metricBuckets:split*` parity cases on all four engines.
|
|
391
|
+
- `anomalies`' `z` divides by the MAD **rescaled to a standard deviation** (`MAD_TO_SIGMA`), because
|
|
392
|
+
`sensitivity` is a threshold and an uncalibrated one reports ordinary days. `movers` divides by the
|
|
393
|
+
raw MAD because it ranks. Do not "unify" them without moving the default with it.
|
|
394
|
+
- Attribution is capped at `ANOMALY_MAX_CONTRIBUTOR_SCANS` extra grouped scans per request, whatever
|
|
395
|
+
the data looks like. The cap, not the data, is what bounds the endpoint.
|
|
396
|
+
- `significance` and `scene_health` (ADR 0051 §4) slot in here the same way: a new pure module over
|
|
397
|
+
the same bucket series.
|
|
174
398
|
|
|
175
399
|
## Cross-engine parity (ADR 0020)
|
|
176
400
|
|
|
@@ -203,7 +427,8 @@ service). The scale tier reuses the same exported `PARITY_CASES`/golden to run
|
|
|
203
427
|
`day` strings (`YYYY-MM-DD`) render identically and are compared.
|
|
204
428
|
|
|
205
429
|
When adding an aggregation or event type, extend `PARITY_EVENTS`/`PARITY_CASES` with golden so both
|
|
206
|
-
engines stay covered.
|
|
430
|
+
engines stay covered. The `metricBuckets:*` cases cover the insight bucket series — one per aggregate
|
|
431
|
+
shape its measure catalog can render, which is what makes `baseline`/`movers` portable.
|
|
207
432
|
|
|
208
433
|
## More
|
|
209
434
|
|
package/README.md
CHANGED
|
@@ -18,6 +18,12 @@ The OSS storage contracts plus the single-file **DuckDB** store:
|
|
|
18
18
|
`scene_regions` (named, labelled boxes per scene, keyed `(project_id, scene_id, region_id)`).
|
|
19
19
|
`duckdbPutSceneRegions` replaces a scene's whole set in one transaction; `duckdbGetSceneRegions`
|
|
20
20
|
/ `duckdbListSceneRegions` read it back.
|
|
21
|
+
- **Project metadata** (ADR 0051 §5) — `annotations`, `glossary` and `saved_analyses`: notes,
|
|
22
|
+
definitions and saved questions written by a project's own people and agents. These are the only
|
|
23
|
+
rows a request can write besides events; the collector gates each write behind the `annotate`
|
|
24
|
+
capability and audits it, and nothing on this path can write, alter or delete an event. The
|
|
25
|
+
accessors enforce the per-project caps in `METADATA_LIMITS` and throw `MetadataLimitError` when a
|
|
26
|
+
project is full.
|
|
21
27
|
|
|
22
28
|
This package carries **no ClickHouse/Postgres dependency**. Optional scale adapters such as
|
|
23
29
|
`@uptimizr/db-clickhouse` compose these contracts behind the same interface. Server/Node only — no
|
|
@@ -150,6 +156,13 @@ for a `bin`/`voxel` grid (`clusterCells` — a deterministic greedy merge of adj
|
|
|
150
156
|
above a density threshold, 8-neighbourhood in 2D and 26 in 3D), and the `record` itself plus its
|
|
151
157
|
`rateOf` rates for a single-row metric. Everything is capped at `limits.maxSummaryRows`.
|
|
152
158
|
|
|
159
|
+
The Zod mirrors of the envelopes — `tableResultSchema(row)`, `resultSummarySchema`,
|
|
160
|
+
`resultEnvelopeSchema(full, row)`, `resultFormatSchema` — are exported from the same subpath as
|
|
161
|
+
always, but are **defined in [`@uptimizr/metrics`](../metrics)** and re-exported here.
|
|
162
|
+
`@uptimizr/agent-core` and `@uptimizr/mcp` describe the same envelopes in their tool output
|
|
163
|
+
schemas and cannot depend on this package (it carries the DuckDB driver), so the shapes have one
|
|
164
|
+
definition, in the package both sides already share.
|
|
165
|
+
|
|
153
166
|
Two invariants worth knowing before extending it:
|
|
154
167
|
|
|
155
168
|
- **`reading` is templated, not generated.** It is assembled from `ColumnSemantics` alone, so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/duckdb/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/duckdb/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CA6kBxE,CAAC;AAEF;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvE"}
|
|
@@ -428,6 +428,175 @@ export const DUCKDB_MIGRATIONS = [
|
|
|
428
428
|
);
|
|
429
429
|
`,
|
|
430
430
|
},
|
|
431
|
+
// Project metadata (ADR 0051 §5 / sketch §E.2): the three tables that let a
|
|
432
|
+
// person or an agent leave something behind — a note on a spike, a definition
|
|
433
|
+
// of a name, a question worth re-asking. Metadata only: the `events` table is
|
|
434
|
+
// untouched and no event type exists for any of this.
|
|
435
|
+
//
|
|
436
|
+
// `since`/`until` are nullable TIMESTAMPs (a standing note has neither);
|
|
437
|
+
// `author_kind` is the collector's decision ('user' | 'agent'), `author_key_id`
|
|
438
|
+
// the id of the API key that carried the write — never the key or its hash.
|
|
439
|
+
{
|
|
440
|
+
id: "0039_annotations",
|
|
441
|
+
sql: /* sql */ `
|
|
442
|
+
CREATE TABLE IF NOT EXISTS annotations (
|
|
443
|
+
id VARCHAR PRIMARY KEY,
|
|
444
|
+
project_id VARCHAR NOT NULL,
|
|
445
|
+
target_kind VARCHAR NOT NULL,
|
|
446
|
+
target_id VARCHAR,
|
|
447
|
+
since TIMESTAMP,
|
|
448
|
+
until TIMESTAMP,
|
|
449
|
+
text VARCHAR NOT NULL,
|
|
450
|
+
author_kind VARCHAR NOT NULL DEFAULT 'user',
|
|
451
|
+
author_key_id VARCHAR,
|
|
452
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
453
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
|
454
|
+
);
|
|
455
|
+
`,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "0040_annotations_idx",
|
|
459
|
+
sql: /* sql */ `
|
|
460
|
+
CREATE INDEX IF NOT EXISTS annotations_project_created_idx
|
|
461
|
+
ON annotations (project_id, created_at);
|
|
462
|
+
`,
|
|
463
|
+
},
|
|
464
|
+
// Glossary: what a name means *in this project*. Keyed by (project, term), so
|
|
465
|
+
// a write is an idempotent upsert and the term is the identity.
|
|
466
|
+
{
|
|
467
|
+
id: "0041_glossary",
|
|
468
|
+
sql: /* sql */ `
|
|
469
|
+
CREATE TABLE IF NOT EXISTS glossary (
|
|
470
|
+
project_id VARCHAR NOT NULL,
|
|
471
|
+
term VARCHAR NOT NULL,
|
|
472
|
+
meaning VARCHAR NOT NULL,
|
|
473
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
474
|
+
PRIMARY KEY (project_id, term)
|
|
475
|
+
);
|
|
476
|
+
`,
|
|
477
|
+
},
|
|
478
|
+
// Saved analyses: a titled question plus what was concluded from it. `query`
|
|
479
|
+
// is JSON text the collector stores but does not interpret (the DSL lands
|
|
480
|
+
// separately); it is bounded at the edge before it gets here.
|
|
481
|
+
{
|
|
482
|
+
id: "0042_saved_analyses",
|
|
483
|
+
sql: /* sql */ `
|
|
484
|
+
CREATE TABLE IF NOT EXISTS saved_analyses (
|
|
485
|
+
id VARCHAR PRIMARY KEY,
|
|
486
|
+
project_id VARCHAR NOT NULL,
|
|
487
|
+
title VARCHAR NOT NULL,
|
|
488
|
+
query VARCHAR NOT NULL DEFAULT '{}',
|
|
489
|
+
conclusion VARCHAR,
|
|
490
|
+
author_kind VARCHAR NOT NULL DEFAULT 'user',
|
|
491
|
+
author_key_id VARCHAR,
|
|
492
|
+
created_at TIMESTAMP NOT NULL DEFAULT now()
|
|
493
|
+
);
|
|
494
|
+
`,
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "0043_saved_analyses_idx",
|
|
498
|
+
sql: /* sql */ `
|
|
499
|
+
CREATE INDEX IF NOT EXISTS saved_analyses_project_created_idx
|
|
500
|
+
ON saved_analyses (project_id, created_at);
|
|
501
|
+
`,
|
|
502
|
+
},
|
|
503
|
+
// --- Conditional subscriptions (#311, ADR 0051 §6 / sketch §F.1–F.2) ------
|
|
504
|
+
// One row per standing question: which registry metric, over what window, with
|
|
505
|
+
// what predicate, and where a firing is delivered. The declaration itself is a
|
|
506
|
+
// closed Zod union validated at the request boundary and never queried *into*,
|
|
507
|
+
// so it lives in one JSON `config` column; the scalars beside it are exactly
|
|
508
|
+
// what the store must filter (`project_id`, `enabled`), order (`created_at`) or
|
|
509
|
+
// update (`last_fired_at`, `last_error`, `failures`).
|
|
510
|
+
//
|
|
511
|
+
// `webhook_secret` is the shared HMAC key. It is deliberately NOT hashed — a
|
|
512
|
+
// one-way digest cannot sign an outbound body — and is the one column no read
|
|
513
|
+
// path ever selects into a record: `duckdb/subscriptions.ts` reads it only in
|
|
514
|
+
// `getWebhookSecret`, and every API response carries a masked placeholder.
|
|
515
|
+
{
|
|
516
|
+
id: "0044_subscriptions",
|
|
517
|
+
sql: /* sql */ `
|
|
518
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
519
|
+
id VARCHAR PRIMARY KEY,
|
|
520
|
+
project_id VARCHAR NOT NULL,
|
|
521
|
+
name VARCHAR NOT NULL,
|
|
522
|
+
metric VARCHAR NOT NULL,
|
|
523
|
+
config VARCHAR NOT NULL,
|
|
524
|
+
webhook_secret VARCHAR,
|
|
525
|
+
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
526
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
527
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
528
|
+
last_fired_at TIMESTAMP,
|
|
529
|
+
last_error VARCHAR,
|
|
530
|
+
failures BIGINT NOT NULL DEFAULT 0
|
|
531
|
+
);
|
|
532
|
+
`,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: "0045_subscriptions_idx",
|
|
536
|
+
sql: /* sql */ `
|
|
537
|
+
CREATE INDEX IF NOT EXISTS subscriptions_project_idx
|
|
538
|
+
ON subscriptions (project_id, created_at);
|
|
539
|
+
`,
|
|
540
|
+
},
|
|
541
|
+
// The bounded firing log: `{ subscriptionId, at, payload }`, last 100 per
|
|
542
|
+
// subscription. The trim runs in the same transaction as the insert
|
|
543
|
+
// (`recordSubscriptionEvent`), so the bound is never observed broken.
|
|
544
|
+
//
|
|
545
|
+
// `at` is a DuckDB keyword, so it is double-quoted here and in every accessor.
|
|
546
|
+
// The column name matches the other three engines, where it needs no quoting.
|
|
547
|
+
{
|
|
548
|
+
id: "0046_subscription_events",
|
|
549
|
+
sql: /* sql */ `
|
|
550
|
+
CREATE TABLE IF NOT EXISTS subscription_events (
|
|
551
|
+
id VARCHAR PRIMARY KEY,
|
|
552
|
+
subscription_id VARCHAR NOT NULL,
|
|
553
|
+
project_id VARCHAR NOT NULL,
|
|
554
|
+
"at" TIMESTAMP NOT NULL DEFAULT now(),
|
|
555
|
+
payload VARCHAR NOT NULL DEFAULT '{}'
|
|
556
|
+
);
|
|
557
|
+
`,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
id: "0047_subscription_events_idx",
|
|
561
|
+
sql: /* sql */ `
|
|
562
|
+
CREATE INDEX IF NOT EXISTS subscription_events_sub_at_idx
|
|
563
|
+
ON subscription_events (subscription_id, "at");
|
|
564
|
+
`,
|
|
565
|
+
},
|
|
566
|
+
// --- Declarative panel specs (#315, ADR 0051 §7 / sketch §G.3) ------------
|
|
567
|
+
// What an agent leaves behind when an answer is worth keeping: a query, a
|
|
568
|
+
// chart name and a one-line reading. The spec is a closed document validated
|
|
569
|
+
// at the request boundary and never queried *into*, so — like a
|
|
570
|
+
// subscription's `config` — it lives in one JSON column and nothing about it
|
|
571
|
+
// is promoted to a column of its own.
|
|
572
|
+
//
|
|
573
|
+
// `author_kind` / `author_key_id` mirror the other three metadata tables: the
|
|
574
|
+
// collector's decision about who wrote the row, and the id of the key that
|
|
575
|
+
// carried it (never the key or its hash). `updated_at` earns its place
|
|
576
|
+
// because a spec is the one metadata row that is genuinely *edited* —
|
|
577
|
+
// retitling a pinned panel or widening it to full span should keep the panel
|
|
578
|
+
// where it is rather than mint a new one at the top of the grid.
|
|
579
|
+
{
|
|
580
|
+
id: "0048_panel_specs",
|
|
581
|
+
sql: /* sql */ `
|
|
582
|
+
CREATE TABLE IF NOT EXISTS panel_specs (
|
|
583
|
+
id VARCHAR PRIMARY KEY,
|
|
584
|
+
project_id VARCHAR NOT NULL,
|
|
585
|
+
spec VARCHAR NOT NULL,
|
|
586
|
+
author_kind VARCHAR NOT NULL DEFAULT 'user',
|
|
587
|
+
author_key_id VARCHAR,
|
|
588
|
+
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
589
|
+
updated_at TIMESTAMP NOT NULL DEFAULT now()
|
|
590
|
+
);
|
|
591
|
+
`,
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
id: "0049_panel_specs_idx",
|
|
595
|
+
sql: /* sql */ `
|
|
596
|
+
CREATE INDEX IF NOT EXISTS panel_specs_project_created_idx
|
|
597
|
+
ON panel_specs (project_id, created_at);
|
|
598
|
+
`,
|
|
599
|
+
},
|
|
431
600
|
];
|
|
432
601
|
/**
|
|
433
602
|
* Apply all DuckDB migrations in order. Idempotent — safe to run on every boot.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/duckdb/migrations.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/duckdb/migrations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CreatePanelSpecInput, type MetadataListOptions, type PanelSpecRecord, type UpdatePanelSpecInput } from "../metadata.js";
|
|
2
|
+
import type { DuckdbClient } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pin one panel and return the stored row.
|
|
5
|
+
*
|
|
6
|
+
* Refuses with {@link MetadataLimitError} once the project holds
|
|
7
|
+
* {@link METADATA_LIMITS.panelSpecs} panels. The cap is lower than the other
|
|
8
|
+
* metadata tables' on purpose: every spec is a query the dashboard runs on
|
|
9
|
+
* every load, so the bound is about what a grid can usefully hold.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPanelSpec(client: DuckdbClient, projectId: string, input: CreatePanelSpecInput): Promise<PanelSpecRecord>;
|
|
12
|
+
/**
|
|
13
|
+
* A project's pinned panels, **oldest first**.
|
|
14
|
+
*
|
|
15
|
+
* The opposite order to the other metadata listings, and deliberately so: these
|
|
16
|
+
* are grid positions, not a feed. Newest-first would move every panel down the
|
|
17
|
+
* dashboard each time somebody pinned one, which is exactly the instability a
|
|
18
|
+
* person notices and a saved layout cannot survive.
|
|
19
|
+
*/
|
|
20
|
+
export declare function listPanelSpecs(client: DuckdbClient, projectId: string, opts?: MetadataListOptions): Promise<PanelSpecRecord[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Replace one panel's spec, keeping its id, its place in the grid and its
|
|
23
|
+
* original authorship. Returns `null` when the id is unknown (or belongs to
|
|
24
|
+
* another project — the two are deliberately indistinguishable).
|
|
25
|
+
*/
|
|
26
|
+
export declare function updatePanelSpec(client: DuckdbClient, projectId: string, id: string, input: UpdatePanelSpecInput): Promise<PanelSpecRecord | null>;
|
|
27
|
+
/** Unpin one panel. Returns whether a row was removed. */
|
|
28
|
+
export declare function deletePanelSpec(client: DuckdbClient, projectId: string, id: string): Promise<boolean>;
|
|
29
|
+
//# sourceMappingURL=panelSpecs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panelSpecs.d.ts","sourceRoot":"","sources":["../../src/duckdb/panelSpecs.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,oBAAoB,EAEzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA0EhD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,eAAe,CAAC,CAuB1B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,eAAe,EAAE,CAAC,CAc5B;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAajC;AAED,0DAA0D;AAC1D,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,OAAO,CAAC,CAYlB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { METADATA_LIMITS, MetadataLimitError, clampMetadataLimit, parsePanelSpec, } from "../metadata.js";
|
|
3
|
+
const PANEL_SPEC_COLS = `id, project_id, spec, author_kind, author_key_id,
|
|
4
|
+
epoch_ms(created_at) AS created_at_ms, epoch_ms(updated_at) AS updated_at_ms`;
|
|
5
|
+
/**
|
|
6
|
+
* Map one row, or `null` when its `spec` column is not a JSON object.
|
|
7
|
+
*
|
|
8
|
+
* A row that cannot be parsed cannot be drawn, and a listing that threw on one
|
|
9
|
+
* would take the whole dashboard grid down with it. Dropping it is the same
|
|
10
|
+
* choice ADR 0041's remote-panel loader makes for a module that fails to
|
|
11
|
+
* import: one bad entry never blocks the rest.
|
|
12
|
+
*/
|
|
13
|
+
function rowToPanelSpec(row) {
|
|
14
|
+
const spec = parsePanelSpec(row.spec);
|
|
15
|
+
if (spec == null)
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
id: row.id,
|
|
19
|
+
projectId: row.project_id,
|
|
20
|
+
spec,
|
|
21
|
+
authorKind: row.author_kind,
|
|
22
|
+
authorKeyId: row.author_key_id,
|
|
23
|
+
createdAt: new Date(Number(row.created_at_ms)),
|
|
24
|
+
updatedAt: new Date(Number(row.updated_at_ms)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** `SELECT count(*)` as a plain number, whatever width the driver returns. */
|
|
28
|
+
async function countRows(client, sql, params) {
|
|
29
|
+
const rows = await client.all(sql, params);
|
|
30
|
+
return Number(rows[0]?.n ?? 0);
|
|
31
|
+
}
|
|
32
|
+
/** Read one row back by id, for the create/update paths. */
|
|
33
|
+
async function readOne(client, id) {
|
|
34
|
+
const rows = await client.all(`SELECT ${PANEL_SPEC_COLS} FROM panel_specs WHERE id = $id`, { id });
|
|
35
|
+
// Written on this connection a moment ago, from a document that serialized,
|
|
36
|
+
// so both the row and its parse are guaranteed here.
|
|
37
|
+
return rowToPanelSpec(rows[0]);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pin one panel and return the stored row.
|
|
41
|
+
*
|
|
42
|
+
* Refuses with {@link MetadataLimitError} once the project holds
|
|
43
|
+
* {@link METADATA_LIMITS.panelSpecs} panels. The cap is lower than the other
|
|
44
|
+
* metadata tables' on purpose: every spec is a query the dashboard runs on
|
|
45
|
+
* every load, so the bound is about what a grid can usefully hold.
|
|
46
|
+
*/
|
|
47
|
+
export async function createPanelSpec(client, projectId, input) {
|
|
48
|
+
const count = await countRows(client, `SELECT count(*) AS n FROM panel_specs WHERE project_id = $projectId`, { projectId });
|
|
49
|
+
if (count >= METADATA_LIMITS.panelSpecs) {
|
|
50
|
+
throw new MetadataLimitError("panelSpecs", METADATA_LIMITS.panelSpecs);
|
|
51
|
+
}
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
await client.run(`INSERT INTO panel_specs
|
|
54
|
+
(id, project_id, spec, author_kind, author_key_id, created_at, updated_at)
|
|
55
|
+
VALUES ($id, $projectId, $spec, $authorKind, $authorKeyId, now(), now())`, {
|
|
56
|
+
id,
|
|
57
|
+
projectId,
|
|
58
|
+
spec: JSON.stringify(input.spec),
|
|
59
|
+
authorKind: input.authorKind,
|
|
60
|
+
authorKeyId: input.authorKeyId,
|
|
61
|
+
});
|
|
62
|
+
return readOne(client, id);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A project's pinned panels, **oldest first**.
|
|
66
|
+
*
|
|
67
|
+
* The opposite order to the other metadata listings, and deliberately so: these
|
|
68
|
+
* are grid positions, not a feed. Newest-first would move every panel down the
|
|
69
|
+
* dashboard each time somebody pinned one, which is exactly the instability a
|
|
70
|
+
* person notices and a saved layout cannot survive.
|
|
71
|
+
*/
|
|
72
|
+
export async function listPanelSpecs(client, projectId, opts = {}) {
|
|
73
|
+
const limit = clampMetadataLimit(opts.limit, METADATA_LIMITS.panelSpecs, METADATA_LIMITS.panelSpecs);
|
|
74
|
+
const rows = await client.all(`SELECT ${PANEL_SPEC_COLS}
|
|
75
|
+
FROM panel_specs WHERE project_id = $projectId
|
|
76
|
+
ORDER BY created_at ASC, id ASC
|
|
77
|
+
LIMIT ${limit}`, { projectId });
|
|
78
|
+
return rows.map(rowToPanelSpec).filter((record) => record !== null);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Replace one panel's spec, keeping its id, its place in the grid and its
|
|
82
|
+
* original authorship. Returns `null` when the id is unknown (or belongs to
|
|
83
|
+
* another project — the two are deliberately indistinguishable).
|
|
84
|
+
*/
|
|
85
|
+
export async function updatePanelSpec(client, projectId, id, input) {
|
|
86
|
+
const before = await countRows(client, `SELECT count(*) AS n FROM panel_specs WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
87
|
+
if (before === 0)
|
|
88
|
+
return null;
|
|
89
|
+
await client.run(`UPDATE panel_specs SET spec = $spec, updated_at = now()
|
|
90
|
+
WHERE project_id = $projectId AND id = $id`, { projectId, id, spec: JSON.stringify(input.spec) });
|
|
91
|
+
return readOne(client, id);
|
|
92
|
+
}
|
|
93
|
+
/** Unpin one panel. Returns whether a row was removed. */
|
|
94
|
+
export async function deletePanelSpec(client, projectId, id) {
|
|
95
|
+
const before = await countRows(client, `SELECT count(*) AS n FROM panel_specs WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
96
|
+
if (before === 0)
|
|
97
|
+
return false;
|
|
98
|
+
await client.run(`DELETE FROM panel_specs WHERE project_id = $projectId AND id = $id`, {
|
|
99
|
+
projectId,
|
|
100
|
+
id,
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=panelSpecs.js.map
|