@uptimizr/db-postgres 0.0.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/dist/events.js ADDED
@@ -0,0 +1,224 @@
1
+ import { nodeSampleRowToEvent, toEventRow, toNodeSampleRow, } from "@uptimizr/db";
2
+ import { anyEventSchema } from "@uptimizr/schema";
3
+ /**
4
+ * Column order of the wide `events` table as written by {@link insertEvents}
5
+ * (must match migration `0001_events`; `inserted_at` is engine-defaulted).
6
+ */
7
+ const EVENT_COLUMNS = [
8
+ "project_id",
9
+ "session_id",
10
+ "visitor_id",
11
+ "event_type",
12
+ "ts",
13
+ "sdk_version",
14
+ "url",
15
+ "scene_id",
16
+ "source",
17
+ "handedness",
18
+ "source_id",
19
+ "ray_origin",
20
+ "ray_direction",
21
+ "position",
22
+ "direction",
23
+ "hit_point",
24
+ "screen",
25
+ "mesh",
26
+ "fps",
27
+ "visible_ms",
28
+ "centered_ms",
29
+ "screen_fraction",
30
+ "texture_bytes",
31
+ "geometry_bytes",
32
+ "triangles",
33
+ "vertices",
34
+ "js_heap_bytes",
35
+ "cap_from",
36
+ "cap_to",
37
+ "frame_time_ms",
38
+ "frame_time_p95_ms",
39
+ "long_frames",
40
+ "dpr",
41
+ "render_scale",
42
+ "fov",
43
+ "aspect",
44
+ "near",
45
+ "name",
46
+ "payload",
47
+ ];
48
+ /** Column order of `node_samples` as written by {@link insertEvents}. */
49
+ const NODE_SAMPLE_COLUMNS = [
50
+ "project_id",
51
+ "session_id",
52
+ "ts",
53
+ "sdk_version",
54
+ "scene_id",
55
+ "node_id",
56
+ "bone_id",
57
+ "position",
58
+ "rotation",
59
+ "scale",
60
+ "child_path",
61
+ ];
62
+ /**
63
+ * Rows per multi-row `INSERT`. Postgres caps a statement at 65535 bound
64
+ * parameters; 39 columns × 500 rows stays well under it.
65
+ */
66
+ const INSERT_CHUNK_ROWS = 500;
67
+ /**
68
+ * Multi-row `INSERT INTO table (cols) VALUES ($1,…),($n,…)` in chunks. Column
69
+ * types are taken from the target table, so `pg`'s text encoding of arrays
70
+ * (`{1,2,3}`), timestamps (naive-UTC strings) and JSON needs no explicit casts.
71
+ */
72
+ async function insertRows(tx, table, columns, rows) {
73
+ for (let start = 0; start < rows.length; start += INSERT_CHUNK_ROWS) {
74
+ const chunk = rows.slice(start, start + INSERT_CHUNK_ROWS);
75
+ const values = [];
76
+ const tuples = chunk.map((row) => {
77
+ const placeholders = columns.map((col) => `$${values.push(row[col])}`);
78
+ return `(${placeholders.join(", ")})`;
79
+ });
80
+ await tx.query(`INSERT INTO ${table} (${columns.join(", ")}) VALUES ${tuples.join(", ")}`, values);
81
+ }
82
+ }
83
+ /**
84
+ * Batched insert of validated events. Reuses the shared {@link toEventRow} mapper
85
+ * so the promoted columns match the DuckDB store exactly; `ts` is bound as the
86
+ * naive-UTC literal every engine accepts and `payload` lands in `jsonb`.
87
+ *
88
+ * `node_transform` events (ADR 0027) are split out into the dedicated
89
+ * `node_samples` table rather than the wide `events` table. Both inserts run in
90
+ * one transaction, so a batch is never partially applied.
91
+ */
92
+ export async function insertEvents(client, events) {
93
+ if (events.length === 0)
94
+ return;
95
+ const wideRows = [];
96
+ const nodeRows = [];
97
+ for (const event of events) {
98
+ if (event.type === "node_transform") {
99
+ nodeRows.push({ ...toNodeSampleRow(event) });
100
+ }
101
+ else {
102
+ wideRows.push({ ...toEventRow(event) });
103
+ }
104
+ }
105
+ await client.transaction(async (tx) => {
106
+ if (wideRows.length > 0)
107
+ await insertRows(tx, "events", EVENT_COLUMNS, wideRows);
108
+ if (nodeRows.length > 0)
109
+ await insertRows(tx, "node_samples", NODE_SAMPLE_COLUMNS, nodeRows);
110
+ });
111
+ }
112
+ /**
113
+ * Read a session's stored `node_transform` samples (ADR 0027), reconstructed
114
+ * into replay-complete events in `ts` order.
115
+ */
116
+ async function readSessionNodeSamples(client, projectId, sessionId) {
117
+ const rows = await client.query(`SELECT (EXTRACT(EPOCH FROM ts) * 1000)::bigint AS ts_ms, sdk_version, scene_id, node_id,
118
+ bone_id, child_path, position, rotation, scale
119
+ FROM node_samples
120
+ WHERE project_id = $1 AND session_id = $2
121
+ ORDER BY ts ASC`, [projectId, sessionId]);
122
+ return rows.map((row) => nodeSampleRowToEvent({
123
+ project_id: projectId,
124
+ session_id: sessionId,
125
+ sdk_version: row.sdk_version,
126
+ scene_id: row.scene_id,
127
+ node_id: row.node_id,
128
+ bone_id: row.bone_id,
129
+ child_path: row.child_path,
130
+ position: row.position ?? [],
131
+ rotation: row.rotation ?? [],
132
+ scale: row.scale ?? [],
133
+ }, row.ts_ms));
134
+ }
135
+ /**
136
+ * Merge two `ts`-ordered event streams into one ascending stream. Stable on ties
137
+ * (wide events before node samples at the same `ts`), so replay sees a single
138
+ * ordered timeline (ADR 0027 §8).
139
+ */
140
+ function mergeByTs(wide, nodes) {
141
+ const merged = [];
142
+ let i = 0;
143
+ let j = 0;
144
+ while (i < wide.length && j < nodes.length) {
145
+ if (nodes[j].ts < wide[i].ts)
146
+ merged.push(nodes[j++]);
147
+ else
148
+ merged.push(wide[i++]);
149
+ }
150
+ while (i < wide.length)
151
+ merged.push(wide[i++]);
152
+ while (j < nodes.length)
153
+ merged.push(nodes[j++]);
154
+ return merged;
155
+ }
156
+ /**
157
+ * `pg` parses `jsonb` columns into objects already; accept a JSON string too so
158
+ * the mapper is robust to a `text`-typed projection.
159
+ */
160
+ function parsePayload(payload) {
161
+ const value = typeof payload === "string" ? JSON.parse(payload) : payload;
162
+ const parsed = anyEventSchema.safeParse(value);
163
+ return parsed.success ? parsed.data : undefined;
164
+ }
165
+ async function readSessionWideEvents(client, projectId, sessionId) {
166
+ const rows = await client.query(`SELECT payload FROM events
167
+ WHERE project_id = $1 AND session_id = $2
168
+ ORDER BY ts ASC`, [projectId, sessionId]);
169
+ const events = [];
170
+ for (const row of rows) {
171
+ const event = parsePayload(row.payload);
172
+ if (event)
173
+ events.push(event);
174
+ }
175
+ return events;
176
+ }
177
+ /**
178
+ * Ordered read of a single session's events for replay/timeline. Returns
179
+ * fully-parsed, schema-validated events in `ts` order, merging the wide `events`
180
+ * table with the dedicated `node_samples` table (ADR 0027 §9). Gated upstream by
181
+ * `ENABLE_RAW_SESSION_RETENTION` (ADR 0003) — this function does not enforce it.
182
+ */
183
+ export async function getSessionEvents(client, projectId, sessionId) {
184
+ const events = await readSessionWideEvents(client, projectId, sessionId);
185
+ const nodes = await readSessionNodeSamples(client, projectId, sessionId);
186
+ return nodes.length > 0 ? mergeByTs(events, nodes) : events;
187
+ }
188
+ /**
189
+ * Streaming counterpart to {@link getSessionEvents}: yields one validated event
190
+ * at a time in `ts` order (the path behind the collector's NDJSON replay
191
+ * response, ADR 0015). The session is read as one ordered result set; node
192
+ * samples are merged in by `ts` before yielding.
193
+ */
194
+ export async function* streamSessionEvents(client, projectId, sessionId) {
195
+ const wide = await readSessionWideEvents(client, projectId, sessionId);
196
+ const nodes = await readSessionNodeSamples(client, projectId, sessionId);
197
+ const ordered = nodes.length > 0 ? mergeByTs(wide, nodes) : wide;
198
+ for (const event of ordered)
199
+ yield event;
200
+ }
201
+ /**
202
+ * Read a session's stored metadata (`device`/`scene`/`user`) from its
203
+ * `session_start` event. Returns `null` when the session has no start event.
204
+ */
205
+ export async function getSessionMeta(client, projectId, sessionId) {
206
+ const rows = await client.query(`SELECT payload, CAST(ts AS TEXT) AS ts FROM events
207
+ WHERE project_id = $1 AND session_id = $2
208
+ AND event_type = 'session_start'
209
+ ORDER BY ts ASC
210
+ LIMIT 1`, [projectId, sessionId]);
211
+ const row = rows[0];
212
+ if (!row)
213
+ return null;
214
+ const parsed = parsePayload(row.payload);
215
+ const event = parsed?.type === "session_start" ? parsed : undefined;
216
+ return {
217
+ sessionId,
218
+ startedAt: row.ts,
219
+ device: event?.device,
220
+ scene: event?.scene,
221
+ user: event?.user,
222
+ };
223
+ }
224
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `@uptimizr/db-postgres` — the optional single-tenant PostgreSQL store
3
+ * (ADR 0020, #84).
4
+ *
5
+ * Composes the dialect-agnostic query layer and engine-neutral mappers from
6
+ * `@uptimizr/db` with a pooled `pg` client, forward-only migrations, and
7
+ * metadata helpers, so a self-hosted collector can swap DuckDB for Postgres via
8
+ * `COLLECTOR_STORE=postgres` without any change to routes, schema contracts, or
9
+ * the dashboard. Single-tenant only — no `org_id`, no tenant isolation.
10
+ *
11
+ * Server/Node only — no DOM imports.
12
+ */
13
+ export { createPostgresClient, assertSafeIdentifier } from "./client.js";
14
+ export type { PostgresClient, PostgresExecutor, PostgresRow } from "./client.js";
15
+ export { POSTGRES_MIGRATIONS, migratePostgres } from "./migrations.js";
16
+ export { runPostgresQuery } from "./queries.js";
17
+ export { insertEvents, getSessionEvents, streamSessionEvents, getSessionMeta } from "./events.js";
18
+ export type { SessionMeta } from "./events.js";
19
+ export { createProject, getProject, createApiKey, resolveApiKey, hashApiKey, apiKeyPrefix, generateApiKey, } from "./projects.js";
20
+ export type { Project, ApiKeyRecord } from "./projects.js";
21
+ export { upsertSceneProxy, getSceneRepresentation, listSceneRepresentations, } from "./sceneRegistry.js";
22
+ export type { SceneRepresentation, SceneRepresentationKind, SceneRepresentationSummary, } from "./sceneRegistry.js";
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACzE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEjF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClG,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@uptimizr/db-postgres` — the optional single-tenant PostgreSQL store
3
+ * (ADR 0020, #84).
4
+ *
5
+ * Composes the dialect-agnostic query layer and engine-neutral mappers from
6
+ * `@uptimizr/db` with a pooled `pg` client, forward-only migrations, and
7
+ * metadata helpers, so a self-hosted collector can swap DuckDB for Postgres via
8
+ * `COLLECTOR_STORE=postgres` without any change to routes, schema contracts, or
9
+ * the dashboard. Single-tenant only — no `org_id`, no tenant isolation.
10
+ *
11
+ * Server/Node only — no DOM imports.
12
+ */
13
+ export { createPostgresClient, assertSafeIdentifier } from "./client.js";
14
+ export { POSTGRES_MIGRATIONS, migratePostgres } from "./migrations.js";
15
+ export { runPostgresQuery } from "./queries.js";
16
+ export { insertEvents, getSessionEvents, streamSessionEvents, getSessionMeta } from "./events.js";
17
+ export { createProject, getProject, createApiKey, resolveApiKey, hashApiKey, apiKeyPrefix, generateApiKey, } from "./projects.js";
18
+ export { upsertSceneProxy, getSceneRepresentation, listSceneRepresentations, } from "./sceneRegistry.js";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlG,OAAO,EACL,aAAa,EACb,UAAU,EACV,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { PostgresSettings } from "@uptimizr/db";
2
+ import { type PostgresClient } from "./client.js";
3
+ /**
4
+ * Ordered, forward-only Postgres migrations for the optional single-tenant
5
+ * relational store (ADR 0020 / ADR 0007, #84). Append new statements; never edit
6
+ * a shipped one. All statements are idempotent (`IF NOT EXISTS` /
7
+ * `CREATE OR REPLACE VIEW`), so {@link migratePostgres} is safe to run on every
8
+ * boot — and, because several collector instances may share one database, the
9
+ * whole run is serialized behind a transaction-scoped advisory lock.
10
+ *
11
+ * This store is **single-tenant**: there is no `org_id` and no tenant isolation
12
+ * (those live only in the proprietary scale layer). The schema mirrors the
13
+ * DuckDB single-file store column-for-column so the dialect-agnostic
14
+ * aggregations render unchanged and the cross-engine parity suite holds.
15
+ *
16
+ * Row-store choices (the parts the SQL Server port, #85, mirrors 1:1):
17
+ * - Vectors are native `double precision[]` (1-indexed like DuckDB/ClickHouse,
18
+ * so `position[1]` in the shared SQL is untouched); the full validated event
19
+ * is preserved in a `jsonb` `payload` so reads stay replay-complete.
20
+ * - `ts` is a naive `timestamp` holding wall-clock UTC (never `timestamptz`), so
21
+ * ordering, `::date` truncation and epoch extraction are session-TZ-independent
22
+ * and identical to the other engines.
23
+ * - Promoted numeric columns default to 0 (the aggregations use `nullIf(x, 0)`
24
+ * where 0 is not a meaningful sample), so they need no NULL handling.
25
+ * - There are **no MergeTree-style rollups**: the daily aggregates are plain
26
+ * views recomputed at query time (`perf_daily`, `events_daily`), acceptable at
27
+ * the single-tenant scale this store targets. The multi-writer rollups remain
28
+ * the scale tier.
29
+ * - Indexes cover the two access paths the aggregations use: the per-type range
30
+ * scan `(project_id, event_type, ts)` and the per-session nearest-row lookup
31
+ * `(project_id, session_id, ts)` that the ASOF emulation (`LATERAL … LIMIT 1`)
32
+ * relies on.
33
+ */
34
+ export declare const POSTGRES_MIGRATIONS: ReadonlyArray<{
35
+ id: string;
36
+ sql: string;
37
+ }>;
38
+ /**
39
+ * Apply all Postgres migrations in order, creating the configured schema first.
40
+ * Idempotent — safe to run on every boot, concurrently from multiple instances.
41
+ *
42
+ * The database named in `settings.url` must already exist (creating databases
43
+ * needs a connection to another database and elevated privileges — the
44
+ * docker-compose service and most managed providers create it for you).
45
+ */
46
+ export declare function migratePostgres(client: PostgresClient, settings: PostgresSettings): Promise<void>;
47
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAuL1E,CAAC;AASF;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -0,0 +1,241 @@
1
+ import { assertSafeIdentifier } from "./client.js";
2
+ /**
3
+ * Ordered, forward-only Postgres migrations for the optional single-tenant
4
+ * relational store (ADR 0020 / ADR 0007, #84). Append new statements; never edit
5
+ * a shipped one. All statements are idempotent (`IF NOT EXISTS` /
6
+ * `CREATE OR REPLACE VIEW`), so {@link migratePostgres} is safe to run on every
7
+ * boot — and, because several collector instances may share one database, the
8
+ * whole run is serialized behind a transaction-scoped advisory lock.
9
+ *
10
+ * This store is **single-tenant**: there is no `org_id` and no tenant isolation
11
+ * (those live only in the proprietary scale layer). The schema mirrors the
12
+ * DuckDB single-file store column-for-column so the dialect-agnostic
13
+ * aggregations render unchanged and the cross-engine parity suite holds.
14
+ *
15
+ * Row-store choices (the parts the SQL Server port, #85, mirrors 1:1):
16
+ * - Vectors are native `double precision[]` (1-indexed like DuckDB/ClickHouse,
17
+ * so `position[1]` in the shared SQL is untouched); the full validated event
18
+ * is preserved in a `jsonb` `payload` so reads stay replay-complete.
19
+ * - `ts` is a naive `timestamp` holding wall-clock UTC (never `timestamptz`), so
20
+ * ordering, `::date` truncation and epoch extraction are session-TZ-independent
21
+ * and identical to the other engines.
22
+ * - Promoted numeric columns default to 0 (the aggregations use `nullIf(x, 0)`
23
+ * where 0 is not a meaningful sample), so they need no NULL handling.
24
+ * - There are **no MergeTree-style rollups**: the daily aggregates are plain
25
+ * views recomputed at query time (`perf_daily`, `events_daily`), acceptable at
26
+ * the single-tenant scale this store targets. The multi-writer rollups remain
27
+ * the scale tier.
28
+ * - Indexes cover the two access paths the aggregations use: the per-type range
29
+ * scan `(project_id, event_type, ts)` and the per-session nearest-row lookup
30
+ * `(project_id, session_id, ts)` that the ASOF emulation (`LATERAL … LIMIT 1`)
31
+ * relies on.
32
+ */
33
+ export const POSTGRES_MIGRATIONS = [
34
+ // --- Events ---------------------------------------------------------------
35
+ {
36
+ id: "0001_events",
37
+ sql: /* sql */ `
38
+ CREATE TABLE IF NOT EXISTS events (
39
+ project_id text NOT NULL,
40
+ session_id text NOT NULL,
41
+ visitor_id text NOT NULL DEFAULT '',
42
+ event_type text NOT NULL,
43
+ ts timestamp NOT NULL,
44
+ sdk_version text NOT NULL DEFAULT '',
45
+ url text NOT NULL DEFAULT '',
46
+ scene_id text NOT NULL DEFAULT 'default',
47
+ source text NOT NULL DEFAULT 'mouse',
48
+ handedness text NOT NULL DEFAULT '',
49
+ source_id text NOT NULL DEFAULT '',
50
+ ray_origin double precision[] NOT NULL DEFAULT '{}',
51
+ ray_direction double precision[] NOT NULL DEFAULT '{}',
52
+ position double precision[] NOT NULL DEFAULT '{}',
53
+ direction double precision[] NOT NULL DEFAULT '{}',
54
+ hit_point double precision[] NOT NULL DEFAULT '{}',
55
+ screen double precision[] NOT NULL DEFAULT '{}',
56
+ mesh text NOT NULL DEFAULT '',
57
+ fps double precision NOT NULL DEFAULT 0,
58
+ visible_ms double precision NOT NULL DEFAULT 0,
59
+ centered_ms double precision NOT NULL DEFAULT 0,
60
+ screen_fraction double precision NOT NULL DEFAULT 0,
61
+ texture_bytes double precision NOT NULL DEFAULT 0,
62
+ geometry_bytes double precision NOT NULL DEFAULT 0,
63
+ triangles double precision NOT NULL DEFAULT 0,
64
+ vertices double precision NOT NULL DEFAULT 0,
65
+ js_heap_bytes double precision NOT NULL DEFAULT 0,
66
+ cap_from text NOT NULL DEFAULT '',
67
+ cap_to text NOT NULL DEFAULT '',
68
+ frame_time_ms double precision NOT NULL DEFAULT 0,
69
+ frame_time_p95_ms double precision NOT NULL DEFAULT 0,
70
+ long_frames double precision NOT NULL DEFAULT 0,
71
+ dpr double precision NOT NULL DEFAULT 0,
72
+ render_scale double precision NOT NULL DEFAULT 0,
73
+ fov double precision NOT NULL DEFAULT 0,
74
+ aspect double precision NOT NULL DEFAULT 0,
75
+ near double precision NOT NULL DEFAULT 0,
76
+ name text NOT NULL DEFAULT '',
77
+ payload jsonb NOT NULL,
78
+ inserted_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
79
+ );
80
+ `,
81
+ },
82
+ {
83
+ id: "0002_events_type_ts_idx",
84
+ sql: /* sql */ `
85
+ CREATE INDEX IF NOT EXISTS events_project_type_ts_idx
86
+ ON events (project_id, event_type, ts);
87
+ `,
88
+ },
89
+ // Per-session ordered access: session reads (replay) and the nearest-row
90
+ // ASOF emulation's correlated lookup (`WHERE session_id = … AND ts <= …
91
+ // ORDER BY ts DESC LIMIT 1`) both walk this index.
92
+ {
93
+ id: "0003_events_session_ts_idx",
94
+ sql: /* sql */ `
95
+ CREATE INDEX IF NOT EXISTS events_project_session_ts_idx
96
+ ON events (project_id, session_id, ts);
97
+ `,
98
+ },
99
+ // --- Scene-actor transforms (node_transform, ADR 0027) --------------------
100
+ // The highest-cardinality signal gets its own transform-shaped table instead
101
+ // of padding `events` with quaternion/bone columns. `bone_id` is '' for the
102
+ // Tier-1 node/root tier; `scale` is empty when it never left identity;
103
+ // `child_path` (ADR 0033) is '' for the root and for bone rows.
104
+ {
105
+ id: "0004_node_samples",
106
+ sql: /* sql */ `
107
+ CREATE TABLE IF NOT EXISTS node_samples (
108
+ project_id text NOT NULL,
109
+ session_id text NOT NULL,
110
+ ts timestamp NOT NULL,
111
+ sdk_version text NOT NULL DEFAULT '',
112
+ scene_id text NOT NULL DEFAULT 'default',
113
+ node_id text NOT NULL,
114
+ bone_id text NOT NULL DEFAULT '',
115
+ position double precision[] NOT NULL DEFAULT '{}',
116
+ rotation double precision[] NOT NULL DEFAULT '{}',
117
+ scale double precision[] NOT NULL DEFAULT '{}',
118
+ child_path text NOT NULL DEFAULT '',
119
+ inserted_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
120
+ );
121
+ `,
122
+ },
123
+ {
124
+ id: "0005_node_samples_idx",
125
+ sql: /* sql */ `
126
+ CREATE INDEX IF NOT EXISTS node_samples_session_node_ts_idx
127
+ ON node_samples (project_id, session_id, node_id, ts);
128
+ `,
129
+ },
130
+ // --- Metadata (single-tenant: `projects` has no `org_id`) ------------------
131
+ {
132
+ id: "0006_projects",
133
+ sql: /* sql */ `
134
+ CREATE TABLE IF NOT EXISTS projects (
135
+ id text PRIMARY KEY,
136
+ name text NOT NULL,
137
+ created_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc')
138
+ );
139
+ `,
140
+ },
141
+ // API keys are stored as SHA-256 hashes (never plaintext). `capability`
142
+ // scopes a key to `ingest` | `query` (enforced at the read boundaries).
143
+ {
144
+ id: "0007_api_keys",
145
+ sql: /* sql */ `
146
+ CREATE TABLE IF NOT EXISTS api_keys (
147
+ id text PRIMARY KEY,
148
+ project_id text NOT NULL,
149
+ key_hash text NOT NULL UNIQUE,
150
+ key_prefix text NOT NULL,
151
+ capability text NOT NULL DEFAULT 'query',
152
+ created_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
153
+ revoked_at timestamp
154
+ );
155
+ `,
156
+ },
157
+ // One representation per (project, scene): a developer-supplied label plus an
158
+ // optional engine-agnostic proxy (ADR 0010/0014). `bounds`/`proxy` are JSON
159
+ // text parsed by the row mapper, exactly as in the DuckDB store.
160
+ {
161
+ id: "0008_scene_representations",
162
+ sql: /* sql */ `
163
+ CREATE TABLE IF NOT EXISTS scene_representations (
164
+ project_id text NOT NULL,
165
+ scene_id text NOT NULL,
166
+ label text,
167
+ kind text NOT NULL DEFAULT 'none',
168
+ up_axis text NOT NULL DEFAULT 'y',
169
+ unit_scale double precision NOT NULL DEFAULT 1,
170
+ bounds text,
171
+ proxy text,
172
+ asset_url text,
173
+ content_hash text,
174
+ proxy_version integer,
175
+ captured_at timestamp,
176
+ updated_at timestamp NOT NULL DEFAULT (now() AT TIME ZONE 'utc'),
177
+ PRIMARY KEY (project_id, scene_id)
178
+ );
179
+ `,
180
+ },
181
+ // --- Query-time rollups ---------------------------------------------------
182
+ // No incremental materialized views: the daily rollups read by
183
+ // `buildPerfDaily`/`buildEventsDaily` are plain views that pre-group by
184
+ // `(project_id, …, day)` and recompute on every read. Column names match the
185
+ // shared read queries, so each read GROUP BY sees exactly one source row per
186
+ // group and the `-Merge` combinators pass the precomputed value through.
187
+ {
188
+ id: "0009_perf_daily_view",
189
+ sql: /* sql */ `
190
+ CREATE OR REPLACE VIEW perf_daily AS
191
+ SELECT
192
+ project_id,
193
+ CAST(ts AS DATE) AS day,
194
+ count(*) AS samples_state,
195
+ avg(fps) AS avg_fps_state,
196
+ min(fps) AS min_fps,
197
+ percentile_cont(0.5) WITHIN GROUP (ORDER BY fps) AS p50_fps_state
198
+ FROM events
199
+ WHERE event_type = 'frame_perf'
200
+ GROUP BY project_id, CAST(ts AS DATE);
201
+ `,
202
+ },
203
+ {
204
+ id: "0010_events_daily_view",
205
+ sql: /* sql */ `
206
+ CREATE OR REPLACE VIEW events_daily AS
207
+ SELECT
208
+ project_id,
209
+ event_type,
210
+ CAST(ts AS DATE) AS day,
211
+ count(*) AS events
212
+ FROM events
213
+ GROUP BY project_id, event_type, CAST(ts AS DATE);
214
+ `,
215
+ },
216
+ ];
217
+ /**
218
+ * Stable advisory-lock key that serializes concurrent boots of several
219
+ * collector instances against one database (DDL is transactional in Postgres,
220
+ * so the migrations either all apply or none do).
221
+ */
222
+ const MIGRATION_LOCK_KEY = 0x7570_7469; // "upti"
223
+ /**
224
+ * Apply all Postgres migrations in order, creating the configured schema first.
225
+ * Idempotent — safe to run on every boot, concurrently from multiple instances.
226
+ *
227
+ * The database named in `settings.url` must already exist (creating databases
228
+ * needs a connection to another database and elevated privileges — the
229
+ * docker-compose service and most managed providers create it for you).
230
+ */
231
+ export async function migratePostgres(client, settings) {
232
+ const schema = assertSafeIdentifier(settings.schema);
233
+ await client.transaction(async (tx) => {
234
+ await tx.command(`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_KEY})`);
235
+ await tx.command(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
236
+ for (const migration of POSTGRES_MIGRATIONS) {
237
+ await tx.command(migration.sql);
238
+ }
239
+ });
240
+ }
241
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAuB,MAAM,aAAa,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAA+C;IAC7E,6EAA6E;IAC7E;QACE,EAAE,EAAE,aAAa;QACjB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2Cd;KACF;IACD;QACE,EAAE,EAAE,yBAAyB;QAC7B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,yEAAyE;IACzE,wEAAwE;IACxE,mDAAmD;IACnD;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,uEAAuE;IACvE,gEAAgE;IAChE;QACE,EAAE,EAAE,mBAAmB;QACvB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;KAed;KACF;IACD;QACE,EAAE,EAAE,uBAAuB;QAC3B,GAAG,EAAE,SAAS,CAAC;;;KAGd;KACF;IACD,8EAA8E;IAC9E;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;KAMd;KACF;IACD,wEAAwE;IACxE,wEAAwE;IACxE;QACE,EAAE,EAAE,eAAe;QACnB,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;KAUd;KACF;IACD,8EAA8E;IAC9E,4EAA4E;IAC5E,iEAAiE;IACjE;QACE,EAAE,EAAE,4BAA4B;QAChC,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;;;;;;KAiBd;KACF;IACD,6EAA6E;IAC7E,+DAA+D;IAC/D,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yEAAyE;IACzE;QACE,EAAE,EAAE,sBAAsB;QAC1B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;;;;KAYd;KACF;IACD;QACE,EAAE,EAAE,wBAAwB;QAC5B,GAAG,EAAE,SAAS,CAAC;;;;;;;;;KASd;KACF;CACF,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,WAAW,CAAC,CAAC,SAAS;AAEjD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAsB,EACtB,QAA0B;IAE1B,MAAM,MAAM,GAAG,oBAAoB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACrD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,EAAE,CAAC,OAAO,CAAC,gCAAgC,kBAAkB,GAAG,CAAC,CAAC;QACxE,MAAM,EAAE,CAAC,OAAO,CAAC,+BAA+B,MAAM,EAAE,CAAC,CAAC;QAC1D,KAAK,MAAM,SAAS,IAAI,mBAAmB,EAAE,CAAC;YAC5C,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { apiKeyPrefix, generateApiKey, hashApiKey, type ApiKeyCapability, type ApiKeyRecord, type Project, type ResolvedApiKey } from "@uptimizr/db";
2
+ import type { PostgresClient } from "./client.js";
3
+ export type { Project, ApiKeyRecord };
4
+ export { hashApiKey, apiKeyPrefix, generateApiKey };
5
+ /** Create a project and return it. */
6
+ export declare function createProject(client: PostgresClient, name: string): Promise<Project>;
7
+ /** Fetch a project by id, or `null` if it does not exist. */
8
+ export declare function getProject(client: PostgresClient, id: string): Promise<Project | null>;
9
+ /**
10
+ * Issue a new API key for a project. Returns both the record and the plaintext
11
+ * key — the plaintext is shown to the caller exactly once and never stored.
12
+ */
13
+ export declare function createApiKey(client: PostgresClient, projectId: string, capability?: ApiKeyCapability): Promise<{
14
+ key: string;
15
+ record: ApiKeyRecord;
16
+ }>;
17
+ /**
18
+ * Resolve a plaintext API key to its (non-revoked) project id and capability, or
19
+ * `null` when the key is unknown or revoked. The collector uses this to
20
+ * authenticate and scope read requests at the boundary.
21
+ */
22
+ export declare function resolveApiKey(client: PostgresClient, plaintext: string): Promise<ResolvedApiKey | null>;
23
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,cAAc,EACd,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC;AAwBpD,sCAAsC;AACtC,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO1F;AAED,6DAA6D;AAC7D,wBAAsB,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAO5F;AAWD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,UAAU,GAAE,gBAA0B,GACrC,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,CAAC,CAqBhD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAOhC"}