@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/LICENSE +201 -0
- package/README.md +118 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +124 -0
- package/dist/client.js.map +1 -0
- package/dist/events.d.ts +34 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +224 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +47 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +241 -0
- package/dist/migrations.js.map +1 -0
- package/dist/projects.d.ts +23 -0
- package/dist/projects.d.ts.map +1 -0
- package/dist/projects.js +62 -0
- package/dist/projects.js.map +1 -0
- package/dist/queries.d.ts +13 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +15 -0
- package/dist/queries.js.map +1 -0
- package/dist/sceneRegistry.d.ts +15 -0
- package/dist/sceneRegistry.d.ts.map +1 -0
- package/dist/sceneRegistry.js +97 -0
- package/dist/sceneRegistry.js.map +1 -0
- package/package.json +60 -0
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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|