@uptimizr/db 2.0.0 → 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/cli/seed.js +9 -7
- package/dist/cli/seed.js.map +1 -1
- 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
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panelSpecs.js","sourceRoot":"","sources":["../../src/duckdb/panelSpecs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,GAMf,MAAM,gBAAgB,CAAC;AA6BxB,MAAM,eAAe,GAAG;oFAC4D,CAAC;AAErF;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,GAAiB;IACvC,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,IAAI;QACJ,UAAU,EAAE,GAAG,CAAC,WAAiC;QACjD,WAAW,EAAE,GAAG,CAAC,aAAa;QAC9B,SAAS,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC9C,SAAS,EAAE,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,KAAK,UAAU,SAAS,CACtB,MAAoB,EACpB,GAAW,EACX,MAA+B;IAE/B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAyB,GAAG,EAAE,MAAM,CAAC,CAAC;IACnE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,4DAA4D;AAC5D,KAAK,UAAU,OAAO,CAAC,MAAoB,EAAE,EAAU;IACrD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAC3B,UAAU,eAAe,kCAAkC,EAC3D,EAAE,EAAE,EAAE,CACP,CAAC;IACF,4EAA4E;IAC5E,qDAAqD;IACrD,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,CAAE,CAAE,CAAC;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAoB,EACpB,SAAiB,EACjB,KAA2B;IAE3B,MAAM,KAAK,GAAG,MAAM,SAAS,CAC3B,MAAM,EACN,qEAAqE,EACrE,EAAE,SAAS,EAAE,CACd,CAAC;IACF,IAAI,KAAK,IAAI,eAAe,CAAC,UAAU,EAAE,CAAC;QACxC,MAAM,IAAI,kBAAkB,CAAC,YAAY,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IACxB,MAAM,MAAM,CAAC,GAAG,CACd;;8EAE0E,EAC1E;QACE,EAAE;QACF,SAAS;QACT,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;QAChC,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CACF,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAoB,EACpB,SAAiB,EACjB,OAA4B,EAAE;IAE9B,MAAM,KAAK,GAAG,kBAAkB,CAC9B,IAAI,CAAC,KAAK,EACV,eAAe,CAAC,UAAU,EAC1B,eAAe,CAAC,UAAU,CAC3B,CAAC;IACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAC3B,UAAU,eAAe;;;cAGf,KAAK,EAAE,EACjB,EAAE,SAAS,EAAE,CACd,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAA6B,EAAE,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC;AACjG,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAoB,EACpB,SAAiB,EACjB,EAAU,EACV,KAA2B;IAE3B,MAAM,MAAM,GAAG,MAAM,SAAS,CAC5B,MAAM,EACN,kFAAkF,EAClF,EAAE,SAAS,EAAE,EAAE,EAAE,CAClB,CAAC;IACF,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,MAAM,CAAC,GAAG,CACd;iDAC6C,EAC7C,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CACpD,CAAC;IACF,OAAO,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7B,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAoB,EACpB,SAAiB,EACjB,EAAU;IAEV,MAAM,MAAM,GAAG,MAAM,SAAS,CAC5B,MAAM,EACN,kFAAkF,EAClF,EAAE,SAAS,EAAE,EAAE,EAAE,CAClB,CAAC;IACF,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,MAAM,CAAC,GAAG,CAAC,oEAAoE,EAAE;QACrF,SAAS;QACT,EAAE;KACH,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AnnotationRecord, type CreateAnnotationInput, type CreateSavedAnalysisInput, type GlossaryEntryRecord, type ListAnnotationsOptions, type MetadataListOptions, type PutGlossaryEntryInput, type SavedAnalysisRecord } from "../metadata.js";
|
|
2
|
+
import type { DuckdbClient } from "./client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create one annotation and return the stored row.
|
|
5
|
+
*
|
|
6
|
+
* Refuses with {@link MetadataLimitError} once the project holds
|
|
7
|
+
* {@link METADATA_LIMITS.annotations} rows: metadata is curated notes, not an
|
|
8
|
+
* unbounded write surface, and refusing is more honest than evicting somebody
|
|
9
|
+
* else's note.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createAnnotation(client: DuckdbClient, projectId: string, input: CreateAnnotationInput): Promise<AnnotationRecord>;
|
|
12
|
+
/**
|
|
13
|
+
* A project's annotations, newest first.
|
|
14
|
+
*
|
|
15
|
+
* The optional `since`/`until` bounds are an **overlap** filter, not a
|
|
16
|
+
* containment one: an annotation matches when its period intersects the
|
|
17
|
+
* requested window, and a standing note (no period at all) always matches
|
|
18
|
+
* because it is about the project rather than about a moment.
|
|
19
|
+
*/
|
|
20
|
+
export declare function listAnnotations(client: DuckdbClient, projectId: string, opts?: ListAnnotationsOptions): Promise<AnnotationRecord[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Delete one annotation of this project. Returns whether a row was removed, so
|
|
23
|
+
* the route can answer 404 for an id that is not there (or belongs to another
|
|
24
|
+
* project — the two are indistinguishable to the caller, deliberately).
|
|
25
|
+
*/
|
|
26
|
+
export declare function deleteAnnotation(client: DuckdbClient, projectId: string, id: string): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Upsert one glossary entry and return the stored row. Idempotent: the term is
|
|
29
|
+
* the identity, so re-defining it replaces the meaning instead of accumulating
|
|
30
|
+
* duplicates. The per-project cap applies only when a *new* term is added.
|
|
31
|
+
*/
|
|
32
|
+
export declare function putGlossaryEntry(client: DuckdbClient, projectId: string, input: PutGlossaryEntryInput): Promise<GlossaryEntryRecord>;
|
|
33
|
+
/** A project's whole glossary, ordered by term (stable for diffs and prompts). */
|
|
34
|
+
export declare function listGlossary(client: DuckdbClient, projectId: string, opts?: MetadataListOptions): Promise<GlossaryEntryRecord[]>;
|
|
35
|
+
/** Delete one term. Returns whether it existed. */
|
|
36
|
+
export declare function deleteGlossaryEntry(client: DuckdbClient, projectId: string, term: string): Promise<boolean>;
|
|
37
|
+
/** Create one saved analysis and return the stored row. */
|
|
38
|
+
export declare function createSavedAnalysis(client: DuckdbClient, projectId: string, input: CreateSavedAnalysisInput): Promise<SavedAnalysisRecord>;
|
|
39
|
+
/** A project's saved analyses, newest first. */
|
|
40
|
+
export declare function listSavedAnalyses(client: DuckdbClient, projectId: string, opts?: MetadataListOptions): Promise<SavedAnalysisRecord[]>;
|
|
41
|
+
/** Delete one saved analysis. Returns whether it existed. */
|
|
42
|
+
export declare function deleteSavedAnalysis(client: DuckdbClient, projectId: string, id: string): Promise<boolean>;
|
|
43
|
+
//# sourceMappingURL=projectMetadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectMetadata.d.ts","sourceRoot":"","sources":["../../src/duckdb/projectMetadata.ts"],"names":[],"mappings":"AACA,OAAO,EAKL,KAAK,gBAAgB,EAErB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAE3B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiEhD;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAqC3B;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,sBAA2B,GAChC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA6B7B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,OAAO,CAAC,CAYlB;AAkBD;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CA8B9B;AAED,kFAAkF;AAClF,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAUhC;AAED,mDAAmD;AACnD,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAYlB;AA6BD,2DAA2D;AAC3D,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,mBAAmB,CAAC,CA8B9B;AAED,gDAAgD;AAChD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAUhC;AAED,6DAA6D;AAC7D,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,OAAO,CAAC,CAYlB"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { METADATA_LIMITS, MetadataLimitError, clampMetadataLimit, parseSavedAnalysisQuery, } from "../metadata.js";
|
|
3
|
+
const ANNOTATION_COLS = `id, project_id, target_kind, target_id,
|
|
4
|
+
epoch_ms(since) AS since_ms, epoch_ms(until) AS until_ms, text,
|
|
5
|
+
author_kind, author_key_id,
|
|
6
|
+
epoch_ms(created_at) AS created_at_ms, epoch_ms(updated_at) AS updated_at_ms`;
|
|
7
|
+
function toDate(ms) {
|
|
8
|
+
return ms == null ? null : new Date(Number(ms));
|
|
9
|
+
}
|
|
10
|
+
function rowToAnnotation(row) {
|
|
11
|
+
return {
|
|
12
|
+
id: row.id,
|
|
13
|
+
projectId: row.project_id,
|
|
14
|
+
targetKind: row.target_kind,
|
|
15
|
+
targetId: row.target_id,
|
|
16
|
+
since: toDate(row.since_ms),
|
|
17
|
+
until: toDate(row.until_ms),
|
|
18
|
+
text: row.text,
|
|
19
|
+
authorKind: row.author_kind,
|
|
20
|
+
authorKeyId: row.author_key_id,
|
|
21
|
+
createdAt: new Date(Number(row.created_at_ms)),
|
|
22
|
+
updatedAt: new Date(Number(row.updated_at_ms)),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** `SELECT count(*)` as a plain number, whatever width the driver returns. */
|
|
26
|
+
async function countRows(client, sql, params) {
|
|
27
|
+
const rows = await client.all(sql, params);
|
|
28
|
+
return Number(rows[0]?.n ?? 0);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create one annotation and return the stored row.
|
|
32
|
+
*
|
|
33
|
+
* Refuses with {@link MetadataLimitError} once the project holds
|
|
34
|
+
* {@link METADATA_LIMITS.annotations} rows: metadata is curated notes, not an
|
|
35
|
+
* unbounded write surface, and refusing is more honest than evicting somebody
|
|
36
|
+
* else's note.
|
|
37
|
+
*/
|
|
38
|
+
export async function createAnnotation(client, projectId, input) {
|
|
39
|
+
const count = await countRows(client, `SELECT count(*) AS n FROM annotations WHERE project_id = $projectId`, { projectId });
|
|
40
|
+
if (count >= METADATA_LIMITS.annotations) {
|
|
41
|
+
throw new MetadataLimitError("annotations", METADATA_LIMITS.annotations);
|
|
42
|
+
}
|
|
43
|
+
const { annotation } = input;
|
|
44
|
+
const id = randomUUID();
|
|
45
|
+
await client.run(`INSERT INTO annotations
|
|
46
|
+
(id, project_id, target_kind, target_id, since, until, text,
|
|
47
|
+
author_kind, author_key_id, created_at, updated_at)
|
|
48
|
+
VALUES ($id, $projectId, $targetKind, $targetId,
|
|
49
|
+
CASE WHEN $sinceUs IS NULL THEN NULL ELSE make_timestamp($sinceUs) END,
|
|
50
|
+
CASE WHEN $untilUs IS NULL THEN NULL ELSE make_timestamp($untilUs) END,
|
|
51
|
+
$text, $authorKind, $authorKeyId, now(), now())`, {
|
|
52
|
+
id,
|
|
53
|
+
projectId,
|
|
54
|
+
targetKind: annotation.targetKind,
|
|
55
|
+
targetId: annotation.targetId ?? null,
|
|
56
|
+
sinceUs: annotation.since == null ? null : annotation.since * 1000,
|
|
57
|
+
untilUs: annotation.until == null ? null : annotation.until * 1000,
|
|
58
|
+
text: annotation.text,
|
|
59
|
+
authorKind: input.authorKind,
|
|
60
|
+
authorKeyId: input.authorKeyId,
|
|
61
|
+
});
|
|
62
|
+
const rows = await client.all(`SELECT ${ANNOTATION_COLS} FROM annotations WHERE id = $id`, { id });
|
|
63
|
+
// The row was just inserted on the same connection, so it is always there.
|
|
64
|
+
return rowToAnnotation(rows[0]);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A project's annotations, newest first.
|
|
68
|
+
*
|
|
69
|
+
* The optional `since`/`until` bounds are an **overlap** filter, not a
|
|
70
|
+
* containment one: an annotation matches when its period intersects the
|
|
71
|
+
* requested window, and a standing note (no period at all) always matches
|
|
72
|
+
* because it is about the project rather than about a moment.
|
|
73
|
+
*/
|
|
74
|
+
export async function listAnnotations(client, projectId, opts = {}) {
|
|
75
|
+
const limit = clampMetadataLimit(opts.limit);
|
|
76
|
+
const params = { projectId };
|
|
77
|
+
const where = ["project_id = $projectId"];
|
|
78
|
+
if (opts.targetKind != null) {
|
|
79
|
+
where.push("target_kind = $targetKind");
|
|
80
|
+
params.targetKind = opts.targetKind;
|
|
81
|
+
}
|
|
82
|
+
if (opts.targetId != null) {
|
|
83
|
+
where.push("target_id = $targetId");
|
|
84
|
+
params.targetId = opts.targetId;
|
|
85
|
+
}
|
|
86
|
+
if (opts.since != null) {
|
|
87
|
+
where.push("(until IS NULL OR until >= make_timestamp($since))");
|
|
88
|
+
params.since = Math.trunc(opts.since) * 1000;
|
|
89
|
+
}
|
|
90
|
+
if (opts.until != null) {
|
|
91
|
+
where.push("(since IS NULL OR since < make_timestamp($until))");
|
|
92
|
+
params.until = Math.trunc(opts.until) * 1000;
|
|
93
|
+
}
|
|
94
|
+
const rows = await client.all(`SELECT ${ANNOTATION_COLS}
|
|
95
|
+
FROM annotations
|
|
96
|
+
WHERE ${where.join(" AND ")}
|
|
97
|
+
ORDER BY created_at DESC, id DESC
|
|
98
|
+
LIMIT ${limit}`, params);
|
|
99
|
+
return rows.map(rowToAnnotation);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Delete one annotation of this project. Returns whether a row was removed, so
|
|
103
|
+
* the route can answer 404 for an id that is not there (or belongs to another
|
|
104
|
+
* project — the two are indistinguishable to the caller, deliberately).
|
|
105
|
+
*/
|
|
106
|
+
export async function deleteAnnotation(client, projectId, id) {
|
|
107
|
+
const before = await countRows(client, `SELECT count(*) AS n FROM annotations WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
108
|
+
if (before === 0)
|
|
109
|
+
return false;
|
|
110
|
+
await client.run(`DELETE FROM annotations WHERE project_id = $projectId AND id = $id`, {
|
|
111
|
+
projectId,
|
|
112
|
+
id,
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function rowToGlossaryEntry(row) {
|
|
117
|
+
return {
|
|
118
|
+
projectId: row.project_id,
|
|
119
|
+
term: row.term,
|
|
120
|
+
meaning: row.meaning,
|
|
121
|
+
updatedAt: new Date(Number(row.updated_at_ms)),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Upsert one glossary entry and return the stored row. Idempotent: the term is
|
|
126
|
+
* the identity, so re-defining it replaces the meaning instead of accumulating
|
|
127
|
+
* duplicates. The per-project cap applies only when a *new* term is added.
|
|
128
|
+
*/
|
|
129
|
+
export async function putGlossaryEntry(client, projectId, input) {
|
|
130
|
+
const { term, meaning } = input.entry;
|
|
131
|
+
const existing = await countRows(client, `SELECT count(*) AS n FROM glossary WHERE project_id = $projectId AND term = $term`, { projectId, term });
|
|
132
|
+
if (existing === 0) {
|
|
133
|
+
const count = await countRows(client, `SELECT count(*) AS n FROM glossary WHERE project_id = $projectId`, { projectId });
|
|
134
|
+
if (count >= METADATA_LIMITS.glossary) {
|
|
135
|
+
throw new MetadataLimitError("glossary", METADATA_LIMITS.glossary);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await client.run(`INSERT INTO glossary (project_id, term, meaning, updated_at)
|
|
139
|
+
VALUES ($projectId, $term, $meaning, now())
|
|
140
|
+
ON CONFLICT (project_id, term)
|
|
141
|
+
DO UPDATE SET meaning = EXCLUDED.meaning, updated_at = now()`, { projectId, term, meaning });
|
|
142
|
+
const rows = await client.all(`SELECT project_id, term, meaning, epoch_ms(updated_at) AS updated_at_ms
|
|
143
|
+
FROM glossary WHERE project_id = $projectId AND term = $term`, { projectId, term });
|
|
144
|
+
return rowToGlossaryEntry(rows[0]);
|
|
145
|
+
}
|
|
146
|
+
/** A project's whole glossary, ordered by term (stable for diffs and prompts). */
|
|
147
|
+
export async function listGlossary(client, projectId, opts = {}) {
|
|
148
|
+
const limit = clampMetadataLimit(opts.limit, METADATA_LIMITS.glossary, METADATA_LIMITS.glossary);
|
|
149
|
+
const rows = await client.all(`SELECT project_id, term, meaning, epoch_ms(updated_at) AS updated_at_ms
|
|
150
|
+
FROM glossary WHERE project_id = $projectId
|
|
151
|
+
ORDER BY term
|
|
152
|
+
LIMIT ${limit}`, { projectId });
|
|
153
|
+
return rows.map(rowToGlossaryEntry);
|
|
154
|
+
}
|
|
155
|
+
/** Delete one term. Returns whether it existed. */
|
|
156
|
+
export async function deleteGlossaryEntry(client, projectId, term) {
|
|
157
|
+
const before = await countRows(client, `SELECT count(*) AS n FROM glossary WHERE project_id = $projectId AND term = $term`, { projectId, term });
|
|
158
|
+
if (before === 0)
|
|
159
|
+
return false;
|
|
160
|
+
await client.run(`DELETE FROM glossary WHERE project_id = $projectId AND term = $term`, {
|
|
161
|
+
projectId,
|
|
162
|
+
term,
|
|
163
|
+
});
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
const ANALYSIS_COLS = `id, project_id, title, query, conclusion, author_kind, author_key_id,
|
|
167
|
+
epoch_ms(created_at) AS created_at_ms`;
|
|
168
|
+
function rowToSavedAnalysis(row) {
|
|
169
|
+
return {
|
|
170
|
+
id: row.id,
|
|
171
|
+
projectId: row.project_id,
|
|
172
|
+
title: row.title,
|
|
173
|
+
query: parseSavedAnalysisQuery(row.query),
|
|
174
|
+
conclusion: row.conclusion,
|
|
175
|
+
authorKind: row.author_kind,
|
|
176
|
+
authorKeyId: row.author_key_id,
|
|
177
|
+
createdAt: new Date(Number(row.created_at_ms)),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Create one saved analysis and return the stored row. */
|
|
181
|
+
export async function createSavedAnalysis(client, projectId, input) {
|
|
182
|
+
const count = await countRows(client, `SELECT count(*) AS n FROM saved_analyses WHERE project_id = $projectId`, { projectId });
|
|
183
|
+
if (count >= METADATA_LIMITS.savedAnalyses) {
|
|
184
|
+
throw new MetadataLimitError("savedAnalyses", METADATA_LIMITS.savedAnalyses);
|
|
185
|
+
}
|
|
186
|
+
const { analysis } = input;
|
|
187
|
+
const id = randomUUID();
|
|
188
|
+
await client.run(`INSERT INTO saved_analyses
|
|
189
|
+
(id, project_id, title, query, conclusion, author_kind, author_key_id, created_at)
|
|
190
|
+
VALUES ($id, $projectId, $title, $query, $conclusion, $authorKind, $authorKeyId, now())`, {
|
|
191
|
+
id,
|
|
192
|
+
projectId,
|
|
193
|
+
title: analysis.title,
|
|
194
|
+
query: JSON.stringify(analysis.query),
|
|
195
|
+
conclusion: analysis.conclusion ?? null,
|
|
196
|
+
authorKind: input.authorKind,
|
|
197
|
+
authorKeyId: input.authorKeyId,
|
|
198
|
+
});
|
|
199
|
+
const rows = await client.all(`SELECT ${ANALYSIS_COLS} FROM saved_analyses WHERE id = $id`, { id });
|
|
200
|
+
return rowToSavedAnalysis(rows[0]);
|
|
201
|
+
}
|
|
202
|
+
/** A project's saved analyses, newest first. */
|
|
203
|
+
export async function listSavedAnalyses(client, projectId, opts = {}) {
|
|
204
|
+
const limit = clampMetadataLimit(opts.limit);
|
|
205
|
+
const rows = await client.all(`SELECT ${ANALYSIS_COLS}
|
|
206
|
+
FROM saved_analyses WHERE project_id = $projectId
|
|
207
|
+
ORDER BY created_at DESC, id DESC
|
|
208
|
+
LIMIT ${limit}`, { projectId });
|
|
209
|
+
return rows.map(rowToSavedAnalysis);
|
|
210
|
+
}
|
|
211
|
+
/** Delete one saved analysis. Returns whether it existed. */
|
|
212
|
+
export async function deleteSavedAnalysis(client, projectId, id) {
|
|
213
|
+
const before = await countRows(client, `SELECT count(*) AS n FROM saved_analyses WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
214
|
+
if (before === 0)
|
|
215
|
+
return false;
|
|
216
|
+
await client.run(`DELETE FROM saved_analyses WHERE project_id = $projectId AND id = $id`, {
|
|
217
|
+
projectId,
|
|
218
|
+
id,
|
|
219
|
+
});
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=projectMetadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectMetadata.js","sourceRoot":"","sources":["../../src/duckdb/projectMetadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Subscription } from "@uptimizr/schema";
|
|
2
|
+
import { type SubscriptionDeliveryOutcome, type SubscriptionEventInput, type SubscriptionEventQueryOptions, type SubscriptionEventRecord, type SubscriptionRecord } from "../subscriptions.js";
|
|
3
|
+
import type { DuckdbClient } from "./client.js";
|
|
4
|
+
/** A project's subscriptions, oldest first. */
|
|
5
|
+
export declare function listSubscriptions(client: DuckdbClient, projectId: string): Promise<SubscriptionRecord[]>;
|
|
6
|
+
/** Every enabled subscription across every project — the scheduler's resume read. */
|
|
7
|
+
export declare function listEnabledSubscriptions(client: DuckdbClient, limit?: number): Promise<SubscriptionRecord[]>;
|
|
8
|
+
/** One subscription, or `null` when it does not exist in this project. */
|
|
9
|
+
export declare function getSubscription(client: DuckdbClient, projectId: string, id: string): Promise<SubscriptionRecord | null>;
|
|
10
|
+
/**
|
|
11
|
+
* Store a new subscription.
|
|
12
|
+
*
|
|
13
|
+
* The per-project cap is checked and the row inserted inside one transaction on
|
|
14
|
+
* the exclusive connection, so two concurrent creates cannot both read
|
|
15
|
+
* `count = 99` and both insert.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createSubscription(client: DuckdbClient, projectId: string, sub: Subscription): Promise<SubscriptionRecord>;
|
|
18
|
+
/** Enable or disable one subscription. `null` when it does not exist. */
|
|
19
|
+
export declare function setSubscriptionEnabled(client: DuckdbClient, projectId: string, id: string, enabled: boolean): Promise<SubscriptionRecord | null>;
|
|
20
|
+
/** Delete a subscription and its firings. `false` when it did not exist. */
|
|
21
|
+
export declare function deleteSubscription(client: DuckdbClient, projectId: string, id: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Write back what a delivery attempt learned. Each field is optional, so a
|
|
24
|
+
* successful delivery (`firedAt`, `failures: 0`, `lastError: null`) and a failed
|
|
25
|
+
* one (`lastError`, `failures: n`) use the same call.
|
|
26
|
+
*/
|
|
27
|
+
export declare function recordSubscriptionOutcome(client: DuckdbClient, projectId: string, id: string, outcome: SubscriptionDeliveryOutcome): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* The webhook signing secret for one subscription, or `null`.
|
|
30
|
+
*
|
|
31
|
+
* The only path that reads the column. Everything else — the list, the get, the
|
|
32
|
+
* API responses — selects {@link SELECT_COLS}, which deliberately omits it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getWebhookSecret(client: DuckdbClient, projectId: string, id: string): Promise<string | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Append one firing and drop everything past the retained window.
|
|
37
|
+
*
|
|
38
|
+
* The trim is a `DELETE … WHERE "at" < (the Nth newest)` rather than a row-number
|
|
39
|
+
* window, because DuckDB will not delete from a CTE over the same table. Both
|
|
40
|
+
* statements run in one transaction, so the bound is never observed broken.
|
|
41
|
+
*/
|
|
42
|
+
export declare function recordSubscriptionEvent(client: DuckdbClient, entry: SubscriptionEventInput): Promise<void>;
|
|
43
|
+
/** A subscription's recent firings, newest first. */
|
|
44
|
+
export declare function listSubscriptionEvents(client: DuckdbClient, projectId: string, id: string, opts?: SubscriptionEventQueryOptions): Promise<SubscriptionEventRecord[]>;
|
|
45
|
+
//# sourceMappingURL=subscriptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../src/duckdb/subscriptions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAUL,KAAK,2BAA2B,EAChC,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,uBAAuB,EAE5B,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6BhD,+CAA+C;AAC/C,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAQ/B;AAED,qFAAqF;AACrF,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,YAAY,EACpB,KAAK,GAAE,MAAkC,GACxC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAS/B;AAED,0EAA0E;AAC1E,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAQpC;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,YAAY,GAChB,OAAO,CAAC,kBAAkB,CAAC,CAyC7B;AAED,yEAAyE;AACzE,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpC;AAED,4EAA4E;AAC5E,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOxB;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,qDAAqD;AACrD,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,6BAAkC,GACvC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAUpC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { MAX_ENABLED_SUBSCRIPTIONS, MAX_SUBSCRIPTIONS_PER_PROJECT, MAX_SUBSCRIPTION_EVENTS, SubscriptionLimitError, clampEventLimit, clampSubscriptionError, rowToSubscription, rowToSubscriptionEvent, toSubscriptionColumns, } from "../subscriptions.js";
|
|
3
|
+
/**
|
|
4
|
+
* **Conditional subscriptions** for the DuckDB single-file store (ADR 0051 §6 /
|
|
5
|
+
* sketch §F.1–F.2).
|
|
6
|
+
*
|
|
7
|
+
* Two tables, both plain metadata — no event type is added and
|
|
8
|
+
* `@uptimizr/schema`'s event union is untouched:
|
|
9
|
+
*
|
|
10
|
+
* - `subscriptions` — one row per standing question, keyed by `id` and scoped to
|
|
11
|
+
* a project. The declaration itself lives in a JSON `config` column; the
|
|
12
|
+
* scalars beside it are exactly what the store must filter, order or update.
|
|
13
|
+
* - `subscription_events` — the bounded firing log, last
|
|
14
|
+
* {@link MAX_SUBSCRIPTION_EVENTS} per subscription. The trim runs inside the
|
|
15
|
+
* same transaction as the insert, so the bound holds even if the collector
|
|
16
|
+
* dies between the two.
|
|
17
|
+
*
|
|
18
|
+
* `at` is a DuckDB keyword, so it is double-quoted here and in the migration —
|
|
19
|
+
* the column name matches the other three engines, where it needs no quoting.
|
|
20
|
+
*/
|
|
21
|
+
const SELECT_COLS = `id, project_id, name, metric, config, enabled,
|
|
22
|
+
epoch_ms(created_at) AS created_at_ms,
|
|
23
|
+
epoch_ms(updated_at) AS updated_at_ms,
|
|
24
|
+
epoch_ms(last_fired_at) AS last_fired_at_ms,
|
|
25
|
+
last_error, failures`;
|
|
26
|
+
const EVENT_COLS = `id, subscription_id, project_id, epoch_ms("at") AS at_ms, payload`;
|
|
27
|
+
/** A project's subscriptions, oldest first. */
|
|
28
|
+
export async function listSubscriptions(client, projectId) {
|
|
29
|
+
const rows = await client.all(`SELECT ${SELECT_COLS} FROM subscriptions
|
|
30
|
+
WHERE project_id = $projectId
|
|
31
|
+
ORDER BY created_at, id`, { projectId });
|
|
32
|
+
return rows.map(rowToSubscription);
|
|
33
|
+
}
|
|
34
|
+
/** Every enabled subscription across every project — the scheduler's resume read. */
|
|
35
|
+
export async function listEnabledSubscriptions(client, limit = MAX_ENABLED_SUBSCRIPTIONS) {
|
|
36
|
+
const capped = Math.min(Math.max(Math.trunc(limit), 1), MAX_ENABLED_SUBSCRIPTIONS);
|
|
37
|
+
const rows = await client.all(`SELECT ${SELECT_COLS} FROM subscriptions
|
|
38
|
+
WHERE enabled
|
|
39
|
+
ORDER BY created_at, id
|
|
40
|
+
LIMIT ${capped}`);
|
|
41
|
+
return rows.map(rowToSubscription);
|
|
42
|
+
}
|
|
43
|
+
/** One subscription, or `null` when it does not exist in this project. */
|
|
44
|
+
export async function getSubscription(client, projectId, id) {
|
|
45
|
+
const rows = await client.all(`SELECT ${SELECT_COLS} FROM subscriptions
|
|
46
|
+
WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
47
|
+
const row = rows[0];
|
|
48
|
+
return row == null ? null : rowToSubscription(row);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Store a new subscription.
|
|
52
|
+
*
|
|
53
|
+
* The per-project cap is checked and the row inserted inside one transaction on
|
|
54
|
+
* the exclusive connection, so two concurrent creates cannot both read
|
|
55
|
+
* `count = 99` and both insert.
|
|
56
|
+
*/
|
|
57
|
+
export async function createSubscription(client, projectId, sub) {
|
|
58
|
+
const id = `sub_${randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
59
|
+
const cols = toSubscriptionColumns(sub);
|
|
60
|
+
await client.exclusive(async (con) => {
|
|
61
|
+
await con.run("BEGIN TRANSACTION");
|
|
62
|
+
try {
|
|
63
|
+
const reader = await con.runAndReadAll(`SELECT count(*) AS n FROM subscriptions WHERE project_id = $projectId`, { projectId });
|
|
64
|
+
const counted = reader.getRowObjects()[0]?.n;
|
|
65
|
+
if (Number(counted ?? 0) >= MAX_SUBSCRIPTIONS_PER_PROJECT) {
|
|
66
|
+
throw new SubscriptionLimitError();
|
|
67
|
+
}
|
|
68
|
+
await con.run(`INSERT INTO subscriptions
|
|
69
|
+
(id, project_id, name, metric, config, webhook_secret, enabled,
|
|
70
|
+
created_at, updated_at, last_fired_at, last_error, failures)
|
|
71
|
+
VALUES ($id, $projectId, $name, $metric, $config, $webhookSecret, $enabled,
|
|
72
|
+
now(), now(), NULL, NULL, 0)`, {
|
|
73
|
+
id,
|
|
74
|
+
projectId,
|
|
75
|
+
name: cols.name,
|
|
76
|
+
metric: cols.metric,
|
|
77
|
+
config: cols.config,
|
|
78
|
+
webhookSecret: cols.webhookSecret,
|
|
79
|
+
enabled: cols.enabled,
|
|
80
|
+
});
|
|
81
|
+
await con.run("COMMIT");
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
await con.run("ROLLBACK");
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const stored = await getSubscription(client, projectId, id);
|
|
89
|
+
// The row was just inserted in a committed transaction; a null here would mean
|
|
90
|
+
// the store lost it, which is a fault, not an empty result.
|
|
91
|
+
if (stored == null)
|
|
92
|
+
throw new Error(`subscription ${id} vanished after insert`);
|
|
93
|
+
return stored;
|
|
94
|
+
}
|
|
95
|
+
/** Enable or disable one subscription. `null` when it does not exist. */
|
|
96
|
+
export async function setSubscriptionEnabled(client, projectId, id, enabled) {
|
|
97
|
+
await client.run(`UPDATE subscriptions SET enabled = $enabled, updated_at = now()
|
|
98
|
+
WHERE project_id = $projectId AND id = $id`, { projectId, id, enabled });
|
|
99
|
+
return getSubscription(client, projectId, id);
|
|
100
|
+
}
|
|
101
|
+
/** Delete a subscription and its firings. `false` when it did not exist. */
|
|
102
|
+
export async function deleteSubscription(client, projectId, id) {
|
|
103
|
+
const existing = await getSubscription(client, projectId, id);
|
|
104
|
+
if (existing == null)
|
|
105
|
+
return false;
|
|
106
|
+
await client.exclusive(async (con) => {
|
|
107
|
+
await con.run("BEGIN TRANSACTION");
|
|
108
|
+
try {
|
|
109
|
+
await con.run(`DELETE FROM subscription_events WHERE project_id = $projectId AND subscription_id = $id`, { projectId, id });
|
|
110
|
+
await con.run(`DELETE FROM subscriptions WHERE project_id = $projectId AND id = $id`, {
|
|
111
|
+
projectId,
|
|
112
|
+
id,
|
|
113
|
+
});
|
|
114
|
+
await con.run("COMMIT");
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
await con.run("ROLLBACK");
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Write back what a delivery attempt learned. Each field is optional, so a
|
|
125
|
+
* successful delivery (`firedAt`, `failures: 0`, `lastError: null`) and a failed
|
|
126
|
+
* one (`lastError`, `failures: n`) use the same call.
|
|
127
|
+
*/
|
|
128
|
+
export async function recordSubscriptionOutcome(client, projectId, id, outcome) {
|
|
129
|
+
const sets = ["updated_at = now()"];
|
|
130
|
+
const params = { projectId, id };
|
|
131
|
+
if (outcome.firedAt != null) {
|
|
132
|
+
sets.push("last_fired_at = make_timestamp($firedAtUs)");
|
|
133
|
+
params.firedAtUs = outcome.firedAt.getTime() * 1000;
|
|
134
|
+
}
|
|
135
|
+
if (outcome.lastError !== undefined) {
|
|
136
|
+
sets.push("last_error = $lastError");
|
|
137
|
+
params.lastError = outcome.lastError == null ? null : clampSubscriptionError(outcome.lastError);
|
|
138
|
+
}
|
|
139
|
+
if (outcome.failures !== undefined) {
|
|
140
|
+
sets.push("failures = CAST($failures AS BIGINT)");
|
|
141
|
+
params.failures = Math.max(0, Math.trunc(outcome.failures));
|
|
142
|
+
}
|
|
143
|
+
await client.run(`UPDATE subscriptions SET ${sets.join(", ")}
|
|
144
|
+
WHERE project_id = $projectId AND id = $id`, params);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The webhook signing secret for one subscription, or `null`.
|
|
148
|
+
*
|
|
149
|
+
* The only path that reads the column. Everything else — the list, the get, the
|
|
150
|
+
* API responses — selects {@link SELECT_COLS}, which deliberately omits it.
|
|
151
|
+
*/
|
|
152
|
+
export async function getWebhookSecret(client, projectId, id) {
|
|
153
|
+
const rows = await client.all(`SELECT webhook_secret FROM subscriptions WHERE project_id = $projectId AND id = $id`, { projectId, id });
|
|
154
|
+
const secret = rows[0]?.webhook_secret;
|
|
155
|
+
return secret == null || secret === "" ? null : secret;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Append one firing and drop everything past the retained window.
|
|
159
|
+
*
|
|
160
|
+
* The trim is a `DELETE … WHERE "at" < (the Nth newest)` rather than a row-number
|
|
161
|
+
* window, because DuckDB will not delete from a CTE over the same table. Both
|
|
162
|
+
* statements run in one transaction, so the bound is never observed broken.
|
|
163
|
+
*/
|
|
164
|
+
export async function recordSubscriptionEvent(client, entry) {
|
|
165
|
+
const at = entry.at ?? new Date();
|
|
166
|
+
await client.exclusive(async (con) => {
|
|
167
|
+
await con.run("BEGIN TRANSACTION");
|
|
168
|
+
try {
|
|
169
|
+
await con.run(`INSERT INTO subscription_events (id, subscription_id, project_id, "at", payload)
|
|
170
|
+
VALUES ($id, $subscriptionId, $projectId, make_timestamp($atUs), $payload)`, {
|
|
171
|
+
id: randomUUID(),
|
|
172
|
+
subscriptionId: entry.subscriptionId,
|
|
173
|
+
projectId: entry.projectId,
|
|
174
|
+
atUs: at.getTime() * 1000,
|
|
175
|
+
payload: JSON.stringify(entry.payload),
|
|
176
|
+
});
|
|
177
|
+
await con.run(`DELETE FROM subscription_events
|
|
178
|
+
WHERE subscription_id = $subscriptionId
|
|
179
|
+
AND id NOT IN (
|
|
180
|
+
SELECT id FROM subscription_events
|
|
181
|
+
WHERE subscription_id = $subscriptionId
|
|
182
|
+
ORDER BY "at" DESC, id DESC
|
|
183
|
+
LIMIT ${MAX_SUBSCRIPTION_EVENTS}
|
|
184
|
+
)`, { subscriptionId: entry.subscriptionId });
|
|
185
|
+
await con.run("COMMIT");
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
await con.run("ROLLBACK");
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/** A subscription's recent firings, newest first. */
|
|
194
|
+
export async function listSubscriptionEvents(client, projectId, id, opts = {}) {
|
|
195
|
+
const limit = clampEventLimit(opts.limit);
|
|
196
|
+
const rows = await client.all(`SELECT ${EVENT_COLS} FROM subscription_events
|
|
197
|
+
WHERE project_id = $projectId AND subscription_id = $id
|
|
198
|
+
ORDER BY "at" DESC, id DESC
|
|
199
|
+
LIMIT ${limit}`, { projectId, id });
|
|
200
|
+
return rows.map(rowToSubscriptionEvent);
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=subscriptions.js.map
|