@mongrov/analytics 0.1.0-alpha.1 → 0.1.0-alpha.7
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/README.md +456 -0
- package/dist/core/context.d.ts +24 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +30 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/engine.d.ts +56 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +119 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extensions.d.ts +33 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +58 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/factory.d.ts +22 -5
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +231 -6
- package/dist/core/factory.js.map +1 -1
- package/dist/core/hooks.d.ts +32 -13
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +137 -15
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logger.d.ts +25 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +33 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/machine.d.ts +174 -0
- package/dist/core/machine.d.ts.map +1 -0
- package/dist/core/machine.js +228 -0
- package/dist/core/machine.js.map +1 -0
- package/dist/core/metric_metadata.d.ts +128 -0
- package/dist/core/metric_metadata.d.ts.map +1 -0
- package/dist/core/metric_metadata.js +47 -0
- package/dist/core/metric_metadata.js.map +1 -0
- package/dist/core/migrations.d.ts +52 -0
- package/dist/core/migrations.d.ts.map +1 -0
- package/dist/core/migrations.js +66 -0
- package/dist/core/migrations.js.map +1 -0
- package/dist/core/persistence.d.ts +50 -0
- package/dist/core/persistence.d.ts.map +1 -0
- package/dist/core/persistence.js +80 -0
- package/dist/core/persistence.js.map +1 -0
- package/dist/core/retention.d.ts +90 -0
- package/dist/core/retention.d.ts.map +1 -0
- package/dist/core/retention.js +119 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/schemas.d.ts +35 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +209 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/warehouse.d.ts +58 -0
- package/dist/core/warehouse.d.ts.map +1 -0
- package/dist/core/warehouse.js +116 -0
- package/dist/core/warehouse.js.map +1 -0
- package/dist/sync/buffer.d.ts +51 -0
- package/dist/sync/buffer.d.ts.map +1 -0
- package/dist/sync/buffer.js +188 -0
- package/dist/sync/buffer.js.map +1 -0
- package/dist/sync/context.d.ts +17 -0
- package/dist/sync/context.d.ts.map +1 -0
- package/dist/sync/context.js +23 -0
- package/dist/sync/context.js.map +1 -0
- package/dist/sync/errors.d.ts +19 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +21 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/events.d.ts +49 -0
- package/dist/sync/events.d.ts.map +1 -0
- package/dist/sync/events.js +46 -0
- package/dist/sync/events.js.map +1 -0
- package/dist/sync/factory.d.ts +57 -0
- package/dist/sync/factory.d.ts.map +1 -0
- package/dist/sync/factory.js +249 -0
- package/dist/sync/factory.js.map +1 -0
- package/dist/sync/fetcher.d.ts +74 -0
- package/dist/sync/fetcher.d.ts.map +1 -0
- package/dist/sync/fetcher.js +166 -0
- package/dist/sync/fetcher.js.map +1 -0
- package/dist/sync/flusher.d.ts +93 -0
- package/dist/sync/flusher.d.ts.map +1 -0
- package/dist/sync/flusher.js +234 -0
- package/dist/sync/flusher.js.map +1 -0
- package/dist/sync/hooks.d.ts +17 -0
- package/dist/sync/hooks.d.ts.map +1 -0
- package/dist/sync/hooks.js +48 -0
- package/dist/sync/hooks.js.map +1 -0
- package/dist/sync/index.d.ts +32 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +36 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/manager.d.ts +48 -0
- package/dist/sync/manager.d.ts.map +1 -0
- package/dist/sync/manager.js +9 -0
- package/dist/sync/manager.js.map +1 -0
- package/dist/sync/mapper/activity.d.ts +31 -0
- package/dist/sync/mapper/activity.d.ts.map +1 -0
- package/dist/sync/mapper/activity.js +56 -0
- package/dist/sync/mapper/activity.js.map +1 -0
- package/dist/sync/mapper/battery.d.ts +12 -0
- package/dist/sync/mapper/battery.d.ts.map +1 -0
- package/dist/sync/mapper/battery.js +22 -0
- package/dist/sync/mapper/battery.js.map +1 -0
- package/dist/sync/mapper/firmware.d.ts +28 -0
- package/dist/sync/mapper/firmware.d.ts.map +1 -0
- package/dist/sync/mapper/firmware.js +43 -0
- package/dist/sync/mapper/firmware.js.map +1 -0
- package/dist/sync/mapper/heart-rate.d.ts +10 -0
- package/dist/sync/mapper/heart-rate.d.ts.map +1 -0
- package/dist/sync/mapper/heart-rate.js +19 -0
- package/dist/sync/mapper/heart-rate.js.map +1 -0
- package/dist/sync/mapper/hrv.d.ts +22 -0
- package/dist/sync/mapper/hrv.d.ts.map +1 -0
- package/dist/sync/mapper/hrv.js +56 -0
- package/dist/sync/mapper/hrv.js.map +1 -0
- package/dist/sync/mapper/ring-config.d.ts +33 -0
- package/dist/sync/mapper/ring-config.d.ts.map +1 -0
- package/dist/sync/mapper/ring-config.js +48 -0
- package/dist/sync/mapper/ring-config.js.map +1 -0
- package/dist/sync/mapper/sleep.d.ts +32 -0
- package/dist/sync/mapper/sleep.d.ts.map +1 -0
- package/dist/sync/mapper/sleep.js +103 -0
- package/dist/sync/mapper/sleep.js.map +1 -0
- package/dist/sync/mapper/spo2.d.ts +9 -0
- package/dist/sync/mapper/spo2.d.ts.map +1 -0
- package/dist/sync/mapper/spo2.js +18 -0
- package/dist/sync/mapper/spo2.js.map +1 -0
- package/dist/sync/mapper/temperature.d.ts +10 -0
- package/dist/sync/mapper/temperature.d.ts.map +1 -0
- package/dist/sync/mapper/temperature.js +19 -0
- package/dist/sync/mapper/temperature.js.map +1 -0
- package/dist/sync/mapper/time.d.ts +49 -0
- package/dist/sync/mapper/time.d.ts.map +1 -0
- package/dist/sync/mapper/time.js +107 -0
- package/dist/sync/mapper/time.js.map +1 -0
- package/dist/sync/mapper/types.d.ts +152 -0
- package/dist/sync/mapper/types.d.ts.map +1 -0
- package/dist/sync/mapper/types.js +15 -0
- package/dist/sync/mapper/types.js.map +1 -0
- package/dist/sync/overflow.d.ts +30 -0
- package/dist/sync/overflow.d.ts.map +1 -0
- package/dist/sync/overflow.js +114 -0
- package/dist/sync/overflow.js.map +1 -0
- package/dist/sync/pusher.d.ts +59 -0
- package/dist/sync/pusher.d.ts.map +1 -0
- package/dist/sync/pusher.js +115 -0
- package/dist/sync/pusher.js.map +1 -0
- package/dist/sync/scheduler.d.ts +84 -0
- package/dist/sync/scheduler.d.ts.map +1 -0
- package/dist/sync/scheduler.js +132 -0
- package/dist/sync/scheduler.js.map +1 -0
- package/dist/sync/triggers.d.ts +64 -0
- package/dist/sync/triggers.d.ts.map +1 -0
- package/dist/sync/triggers.js +113 -0
- package/dist/sync/triggers.js.map +1 -0
- package/dist/sync/types.d.ts +50 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +8 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/watermark.d.ts +32 -0
- package/dist/sync/watermark.d.ts.map +1 -0
- package/dist/sync/watermark.js +61 -0
- package/dist/sync/watermark.js.map +1 -0
- package/package.json +12 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KVStore-backed persistence for analytics state.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Last-attach ctx** — persisted on every successful attach and cleared
|
|
7
|
+
* on detach. `loadLastAttach` returns the ctx only if it was written
|
|
8
|
+
* within `LAST_ATTACH_TTL_MS` (24h per spec §Testing) so apps can safely
|
|
9
|
+
* auto-reattach after a crash / cold start without silently reviving a
|
|
10
|
+
* stale multi-day-old context.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Retention override** — `setRetention(days)` persists the user
|
|
13
|
+
* override layer per spec §Retention precedence at
|
|
14
|
+
* `analytics:retention:override:{brand}:{tenantId}:{userId}`. v0.1.0
|
|
15
|
+
* resolves via `resolveEffectiveRetention` (retention.ts) but does not
|
|
16
|
+
* surface UX to write it (`analytics.setRetention` is the escape hatch).
|
|
17
|
+
*
|
|
18
|
+
* All keys use the `analytics:` prefix so consumers can namespace-clear.
|
|
19
|
+
*/
|
|
20
|
+
// -------------------- keys --------------------
|
|
21
|
+
/** 24h in ms — freshness bound for the persisted attach ctx. */
|
|
22
|
+
export const LAST_ATTACH_TTL_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
function lastAttachKey(brand) {
|
|
24
|
+
return `analytics:last-attach:${brand}`;
|
|
25
|
+
}
|
|
26
|
+
function retentionOverrideKey(brand, tenantId, userId) {
|
|
27
|
+
return `analytics:retention:override:${brand}:${tenantId}:${userId}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Persist the ctx that just successfully attached. Overwrites any prior
|
|
31
|
+
* entry for the same brand.
|
|
32
|
+
*/
|
|
33
|
+
export async function saveLastAttach(kv, ctx, now = Date.now()) {
|
|
34
|
+
const record = { ctx, attachedAt: now };
|
|
35
|
+
await kv.set(lastAttachKey(ctx.brand), record);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load the last attach ctx for `brand` if it was written within the TTL.
|
|
39
|
+
* Returns `null` for missing entries or entries older than
|
|
40
|
+
* `LAST_ATTACH_TTL_MS`. Stale entries are also deleted as a side effect so
|
|
41
|
+
* repeated reads don't keep resurfacing them.
|
|
42
|
+
*/
|
|
43
|
+
export async function loadLastAttach(kv, brand, now = Date.now()) {
|
|
44
|
+
const record = await kv.get(lastAttachKey(brand));
|
|
45
|
+
if (!record)
|
|
46
|
+
return null;
|
|
47
|
+
if (now - record.attachedAt > LAST_ATTACH_TTL_MS) {
|
|
48
|
+
// Best-effort cleanup — swallow errors so a flaky KV can't wedge callers.
|
|
49
|
+
try {
|
|
50
|
+
await kv.delete(lastAttachKey(brand));
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ }
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return record.ctx;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Clear the persisted attach ctx for `brand`. Invoked on `detach()` so a
|
|
59
|
+
* subsequent open doesn't auto-restore a context the app deliberately left.
|
|
60
|
+
*/
|
|
61
|
+
export async function clearLastAttach(kv, brand) {
|
|
62
|
+
await kv.delete(lastAttachKey(brand));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Persist the user-override retention for the current attach context.
|
|
66
|
+
* Overwrites any prior override for the same `(brand, tenantId, userId)`.
|
|
67
|
+
*/
|
|
68
|
+
export async function saveRetentionOverride(kv, ctx, days, now = Date.now()) {
|
|
69
|
+
const record = { days, updatedAt: now };
|
|
70
|
+
await kv.set(retentionOverrideKey(ctx.brand, ctx.tenantId, ctx.userId), record);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Load the user-override retention (days) for `(brand, tenantId, userId)`,
|
|
74
|
+
* or `null` when none is stored.
|
|
75
|
+
*/
|
|
76
|
+
export async function loadRetentionOverride(kv, ctx) {
|
|
77
|
+
const record = await kv.get(retentionOverrideKey(ctx.brand, ctx.tenantId, ctx.userId));
|
|
78
|
+
return record ? record.days : null;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=persistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/core/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,iDAAiD;AAEjD,gEAAgE;AAChE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAErD,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,yBAAyB,KAAK,EAAE,CAAA;AACzC,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAa,EAAE,QAAgB,EAAE,MAAc;IAC3E,OAAO,gCAAgC,KAAK,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAA;AACtE,CAAC;AASD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAW,EACX,GAAkB,EAClB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,MAAM,GAAqB,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAA;IACzD,MAAM,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAA;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAW,EACX,KAAa,EACb,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAmB,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;IACnE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,IAAI,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,kBAAkB,EAAE,CAAC;QACjD,0EAA0E;QAC1E,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;QACvC,CAAC;QACD,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QACtB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAA;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,EAAW,EAAE,KAAa;IAC9D,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;AACvC,CAAC;AASD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,EAAW,EACX,GAAkB,EAClB,IAAY,EACZ,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,MAAM,GAA4B,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAA;IAChE,MAAM,EAAE,CAAC,GAAG,CAAC,oBAAoB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,EAAW,EACX,GAAkB;IAElB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CACzB,oBAAoB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAC1D,CAAA;IACD,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAA;AACpC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention scheduler for the analytics warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Resolve** effective retention days for `(brand, tenantId, userId)`
|
|
7
|
+
* via `resolveEffectiveRetention` — precedence per spec §Retention
|
|
8
|
+
* is `max(userOverride, familySetting, brandDefault)` so *any* layer
|
|
9
|
+
* that would preserve data wins.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Sweep** the warehouse via `runRetentionSweep` — issues one DELETE
|
|
12
|
+
* per retention-managed table, guarded by the per-table push watermark
|
|
13
|
+
* from `sync_watermark` so rows still awaiting server sync are never
|
|
14
|
+
* dropped (spec Fix 9).
|
|
15
|
+
*
|
|
16
|
+
* Internal tables have fixed defaults independent of brand/user config:
|
|
17
|
+
* - `insight` → 90 days
|
|
18
|
+
* - `tool_call_audit` → 30 days
|
|
19
|
+
*
|
|
20
|
+
* Tables without a retention semantic (`device_config`, `sync_watermark`)
|
|
21
|
+
* are skipped.
|
|
22
|
+
*/
|
|
23
|
+
import type { HybridDuckDB } from './engine';
|
|
24
|
+
import type { TableName } from './schemas';
|
|
25
|
+
export interface EffectiveRetentionInput {
|
|
26
|
+
/** Brand-level default (v0.1.0 active layer) — from `AnalyticsConfig.retention[brand].days`. */
|
|
27
|
+
brandDefault: number;
|
|
28
|
+
/** Family-level setting (v2 candidate — plumbed but unused by v0.1.0). */
|
|
29
|
+
familySetting?: number;
|
|
30
|
+
/** User override (v2 candidate — plumbed via `setRetention`, unused in v0.1.0 UX). */
|
|
31
|
+
userOverride?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve effective retention days per spec §Retention precedence — returns
|
|
35
|
+
* `max(userOverride ?? 0, familySetting ?? 0, brandDefault)`.
|
|
36
|
+
*
|
|
37
|
+
* Rationale: retention is *protective* — if any layer would preserve data
|
|
38
|
+
* longer, we honour that layer (max wins, not first-match).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveEffectiveRetention(input: EffectiveRetentionInput): number;
|
|
41
|
+
/** Fixed retention for the `insight` table — spec §Retention. */
|
|
42
|
+
export declare const INSIGHT_RETENTION_DAYS = 90;
|
|
43
|
+
/** Fixed retention for the `tool_call_audit` table — spec §Retention. */
|
|
44
|
+
export declare const AUDIT_RETENTION_DAYS = 30;
|
|
45
|
+
export interface RetentionSweepInput {
|
|
46
|
+
/** Effective retention days for sensor tables (see `resolveEffectiveRetention`). */
|
|
47
|
+
effectiveDays: number;
|
|
48
|
+
}
|
|
49
|
+
export interface RetentionSweepEntry {
|
|
50
|
+
table: TableName;
|
|
51
|
+
days: number;
|
|
52
|
+
}
|
|
53
|
+
export interface RetentionSweepResult {
|
|
54
|
+
swept: RetentionSweepEntry[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete expired rows across every retention-managed table under `catalog`,
|
|
58
|
+
* bounded by each table's push watermark (`sync_watermark.cursor_ts`).
|
|
59
|
+
*
|
|
60
|
+
* SQL shape per table:
|
|
61
|
+
*
|
|
62
|
+
* ```sql
|
|
63
|
+
* DELETE FROM {catalog}.{table}
|
|
64
|
+
* WHERE {tsCol} < LEAST(
|
|
65
|
+
* now() - INTERVAL '{days} days',
|
|
66
|
+
* (SELECT MAX(cursor_ts) FROM {catalog}.sync_watermark
|
|
67
|
+
* WHERE table_name = '{table}')
|
|
68
|
+
* );
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* When no watermark exists, `MAX` returns NULL, `LEAST` becomes NULL and the
|
|
72
|
+
* comparison drops to false — safest default (no deletion on unsynced data).
|
|
73
|
+
*
|
|
74
|
+
* Failure at any table surfaces as `AnalyticsError('retention_failed', …)`;
|
|
75
|
+
* the caller decides whether to swallow-and-log or bubble.
|
|
76
|
+
*/
|
|
77
|
+
export declare function runRetentionSweep(db: HybridDuckDB, catalog: string, input: RetentionSweepInput): Promise<RetentionSweepResult>;
|
|
78
|
+
interface DeleteSqlInput {
|
|
79
|
+
catalog: string;
|
|
80
|
+
table: TableName;
|
|
81
|
+
tsCol: string;
|
|
82
|
+
days: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compose the retention DELETE for one table. Extracted so tests can snapshot
|
|
86
|
+
* the exact SQL issued without a live engine.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildDeleteSql(input: DeleteSqlInput): string;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAI1C,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,YAAY,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAMhF;AA4CD,iEAAiE;AACjE,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAItC,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAsB/B;AAaD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAS5D"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention scheduler for the analytics warehouse.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Resolve** effective retention days for `(brand, tenantId, userId)`
|
|
7
|
+
* via `resolveEffectiveRetention` — precedence per spec §Retention
|
|
8
|
+
* is `max(userOverride, familySetting, brandDefault)` so *any* layer
|
|
9
|
+
* that would preserve data wins.
|
|
10
|
+
*
|
|
11
|
+
* 2. **Sweep** the warehouse via `runRetentionSweep` — issues one DELETE
|
|
12
|
+
* per retention-managed table, guarded by the per-table push watermark
|
|
13
|
+
* from `sync_watermark` so rows still awaiting server sync are never
|
|
14
|
+
* dropped (spec Fix 9).
|
|
15
|
+
*
|
|
16
|
+
* Internal tables have fixed defaults independent of brand/user config:
|
|
17
|
+
* - `insight` → 90 days
|
|
18
|
+
* - `tool_call_audit` → 30 days
|
|
19
|
+
*
|
|
20
|
+
* Tables without a retention semantic (`device_config`, `sync_watermark`)
|
|
21
|
+
* are skipped.
|
|
22
|
+
*/
|
|
23
|
+
import { AnalyticsError } from './errors';
|
|
24
|
+
/**
|
|
25
|
+
* Resolve effective retention days per spec §Retention precedence — returns
|
|
26
|
+
* `max(userOverride ?? 0, familySetting ?? 0, brandDefault)`.
|
|
27
|
+
*
|
|
28
|
+
* Rationale: retention is *protective* — if any layer would preserve data
|
|
29
|
+
* longer, we honour that layer (max wins, not first-match).
|
|
30
|
+
*/
|
|
31
|
+
export function resolveEffectiveRetention(input) {
|
|
32
|
+
return Math.max(input.userOverride ?? 0, input.familySetting ?? 0, input.brandDefault);
|
|
33
|
+
}
|
|
34
|
+
const TABLE_RETENTION = Object.freeze({
|
|
35
|
+
hrv: { tsColumn: 'ts', kind: 'sensor' },
|
|
36
|
+
heart_rate: { tsColumn: 'ts', kind: 'sensor' },
|
|
37
|
+
spo2: { tsColumn: 'ts', kind: 'sensor' },
|
|
38
|
+
temperature: { tsColumn: 'ts', kind: 'sensor' },
|
|
39
|
+
activity: { tsColumn: 'ts', kind: 'sensor' },
|
|
40
|
+
activity_bucket: { tsColumn: 'ts', kind: 'sensor' },
|
|
41
|
+
// `sleep_session` retains by session close, not start — a session that ends
|
|
42
|
+
// outside the retention window has fully aged out.
|
|
43
|
+
sleep_session: { tsColumn: 'ts_end', kind: 'sensor' },
|
|
44
|
+
sleep_stage: { tsColumn: 'ts', kind: 'sensor' },
|
|
45
|
+
sleep_raw: { tsColumn: 'ts', kind: 'sensor' },
|
|
46
|
+
device_event: { tsColumn: 'ts', kind: 'sensor' },
|
|
47
|
+
device_config: null,
|
|
48
|
+
insight: { tsColumn: 'ts', kind: 'insight' },
|
|
49
|
+
sync_watermark: null,
|
|
50
|
+
tool_call_audit: { tsColumn: 'ts', kind: 'audit' },
|
|
51
|
+
});
|
|
52
|
+
// -------------------- constants --------------------
|
|
53
|
+
/** Fixed retention for the `insight` table — spec §Retention. */
|
|
54
|
+
export const INSIGHT_RETENTION_DAYS = 90;
|
|
55
|
+
/** Fixed retention for the `tool_call_audit` table — spec §Retention. */
|
|
56
|
+
export const AUDIT_RETENTION_DAYS = 30;
|
|
57
|
+
/**
|
|
58
|
+
* Delete expired rows across every retention-managed table under `catalog`,
|
|
59
|
+
* bounded by each table's push watermark (`sync_watermark.cursor_ts`).
|
|
60
|
+
*
|
|
61
|
+
* SQL shape per table:
|
|
62
|
+
*
|
|
63
|
+
* ```sql
|
|
64
|
+
* DELETE FROM {catalog}.{table}
|
|
65
|
+
* WHERE {tsCol} < LEAST(
|
|
66
|
+
* now() - INTERVAL '{days} days',
|
|
67
|
+
* (SELECT MAX(cursor_ts) FROM {catalog}.sync_watermark
|
|
68
|
+
* WHERE table_name = '{table}')
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* When no watermark exists, `MAX` returns NULL, `LEAST` becomes NULL and the
|
|
73
|
+
* comparison drops to false — safest default (no deletion on unsynced data).
|
|
74
|
+
*
|
|
75
|
+
* Failure at any table surfaces as `AnalyticsError('retention_failed', …)`;
|
|
76
|
+
* the caller decides whether to swallow-and-log or bubble.
|
|
77
|
+
*/
|
|
78
|
+
export async function runRetentionSweep(db, catalog, input) {
|
|
79
|
+
const swept = [];
|
|
80
|
+
const tableNames = Object.keys(TABLE_RETENTION);
|
|
81
|
+
for (const table of tableNames) {
|
|
82
|
+
const cfg = TABLE_RETENTION[table];
|
|
83
|
+
if (!cfg)
|
|
84
|
+
continue;
|
|
85
|
+
const days = daysForKind(cfg.kind, input.effectiveDays);
|
|
86
|
+
const sql = buildDeleteSql({ catalog, table, tsCol: cfg.tsColumn, days });
|
|
87
|
+
try {
|
|
88
|
+
await db.execute(sql);
|
|
89
|
+
swept.push({ table, days });
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
throw new AnalyticsError('retention_failed', `runRetentionSweep failed on table ${table}`, cause);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { swept };
|
|
96
|
+
}
|
|
97
|
+
function daysForKind(kind, effective) {
|
|
98
|
+
switch (kind) {
|
|
99
|
+
case 'sensor':
|
|
100
|
+
return effective;
|
|
101
|
+
case 'insight':
|
|
102
|
+
return INSIGHT_RETENTION_DAYS;
|
|
103
|
+
case 'audit':
|
|
104
|
+
return AUDIT_RETENTION_DAYS;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compose the retention DELETE for one table. Extracted so tests can snapshot
|
|
109
|
+
* the exact SQL issued without a live engine.
|
|
110
|
+
*/
|
|
111
|
+
export function buildDeleteSql(input) {
|
|
112
|
+
const { catalog, table, tsCol, days } = input;
|
|
113
|
+
return (`DELETE FROM ${catalog}.${table} `
|
|
114
|
+
+ `WHERE ${tsCol} < LEAST(`
|
|
115
|
+
+ `now() - INTERVAL '${days} days', `
|
|
116
|
+
+ `(SELECT MAX(cursor_ts) FROM ${catalog}.sync_watermark WHERE table_name = '${table}')`
|
|
117
|
+
+ `);`);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table DDL for the analytics warehouse (spec §Table schema).
|
|
3
|
+
*
|
|
4
|
+
* Fourteen tables, verbatim from `.specifica/features/analytics-core/spec.md`.
|
|
5
|
+
* DDL strings are frozen — apps must not mutate them; migrations (T-08) own
|
|
6
|
+
* schema evolution.
|
|
7
|
+
*
|
|
8
|
+
* `ensureSchemas(db, catalog)` idempotently creates every table inside the
|
|
9
|
+
* attached catalog (i.e. `zone_<tenantId>`), rewriting `CREATE TABLE <name>`
|
|
10
|
+
* to `CREATE TABLE IF NOT EXISTS <catalog>.<name>` at issue time so the base
|
|
11
|
+
* DDL stays faithful to the spec text.
|
|
12
|
+
*/
|
|
13
|
+
import type { HybridDuckDB } from './engine';
|
|
14
|
+
/** Ordered list of every warehouse table. Order = creation order. */
|
|
15
|
+
export declare const TABLE_NAMES: readonly ["hrv", "heart_rate", "spo2", "temperature", "activity", "activity_bucket", "sleep_session", "sleep_stage", "sleep_raw", "device_event", "device_config", "insight", "sync_watermark", "tool_call_audit"];
|
|
16
|
+
export type TableName = (typeof TABLE_NAMES)[number];
|
|
17
|
+
/**
|
|
18
|
+
* Frozen DDL for every warehouse table. Copied verbatim from spec §Table
|
|
19
|
+
* schema; rewrite happens in `ensureSchemas` at issue time.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SCHEMAS: Readonly<Record<TableName, string>>;
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite one base DDL to `CREATE TABLE IF NOT EXISTS <catalog>.<table>`.
|
|
24
|
+
*
|
|
25
|
+
* Exported for tests; call sites should prefer `ensureSchemas`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function qualifyDdl(baseDdl: string, table: TableName, catalog: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Create every warehouse table under `<catalog>.<table>`, in `TABLE_NAMES` order.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent — every issued statement uses `IF NOT EXISTS`. Failure at any
|
|
32
|
+
* table surfaces as `AnalyticsError('migration_failed', <table>, cause)`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ensureSchemas(db: HybridDuckDB, catalog: string): Promise<void>;
|
|
35
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/core/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C,qEAAqE;AACrE,eAAO,MAAM,WAAW,oNAed,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAA;AAEpD;;;GAGG;AACH,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAiKtD,CAAA;AAEF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKrF;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcpF"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table DDL for the analytics warehouse (spec §Table schema).
|
|
3
|
+
*
|
|
4
|
+
* Fourteen tables, verbatim from `.specifica/features/analytics-core/spec.md`.
|
|
5
|
+
* DDL strings are frozen — apps must not mutate them; migrations (T-08) own
|
|
6
|
+
* schema evolution.
|
|
7
|
+
*
|
|
8
|
+
* `ensureSchemas(db, catalog)` idempotently creates every table inside the
|
|
9
|
+
* attached catalog (i.e. `zone_<tenantId>`), rewriting `CREATE TABLE <name>`
|
|
10
|
+
* to `CREATE TABLE IF NOT EXISTS <catalog>.<name>` at issue time so the base
|
|
11
|
+
* DDL stays faithful to the spec text.
|
|
12
|
+
*/
|
|
13
|
+
import { AnalyticsError } from './errors';
|
|
14
|
+
/** Ordered list of every warehouse table. Order = creation order. */
|
|
15
|
+
export const TABLE_NAMES = [
|
|
16
|
+
'hrv',
|
|
17
|
+
'heart_rate',
|
|
18
|
+
'spo2',
|
|
19
|
+
'temperature',
|
|
20
|
+
'activity',
|
|
21
|
+
'activity_bucket',
|
|
22
|
+
'sleep_session',
|
|
23
|
+
'sleep_stage',
|
|
24
|
+
'sleep_raw',
|
|
25
|
+
'device_event',
|
|
26
|
+
'device_config',
|
|
27
|
+
'insight',
|
|
28
|
+
'sync_watermark',
|
|
29
|
+
'tool_call_audit',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Frozen DDL for every warehouse table. Copied verbatim from spec §Table
|
|
33
|
+
* schema; rewrite happens in `ensureSchemas` at issue time.
|
|
34
|
+
*/
|
|
35
|
+
export const SCHEMAS = Object.freeze({
|
|
36
|
+
hrv: `CREATE TABLE hrv (
|
|
37
|
+
ts TIMESTAMP NOT NULL,
|
|
38
|
+
brand VARCHAR NOT NULL,
|
|
39
|
+
family_id VARCHAR NOT NULL,
|
|
40
|
+
user_id VARCHAR NOT NULL,
|
|
41
|
+
device_id VARCHAR NOT NULL,
|
|
42
|
+
hrv_ms SMALLINT,
|
|
43
|
+
stress SMALLINT,
|
|
44
|
+
systolic_bp SMALLINT,
|
|
45
|
+
diastolic_bp SMALLINT,
|
|
46
|
+
vascular_aging SMALLINT
|
|
47
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
48
|
+
heart_rate: `CREATE TABLE heart_rate (
|
|
49
|
+
ts TIMESTAMP NOT NULL,
|
|
50
|
+
brand VARCHAR NOT NULL,
|
|
51
|
+
family_id VARCHAR NOT NULL,
|
|
52
|
+
user_id VARCHAR NOT NULL,
|
|
53
|
+
device_id VARCHAR NOT NULL,
|
|
54
|
+
bpm SMALLINT NOT NULL
|
|
55
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
56
|
+
spo2: `CREATE TABLE spo2 (
|
|
57
|
+
ts TIMESTAMP NOT NULL,
|
|
58
|
+
brand VARCHAR NOT NULL,
|
|
59
|
+
family_id VARCHAR NOT NULL,
|
|
60
|
+
user_id VARCHAR NOT NULL,
|
|
61
|
+
device_id VARCHAR NOT NULL,
|
|
62
|
+
spo2 SMALLINT NOT NULL
|
|
63
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
64
|
+
temperature: `CREATE TABLE temperature (
|
|
65
|
+
ts TIMESTAMP NOT NULL,
|
|
66
|
+
brand VARCHAR NOT NULL,
|
|
67
|
+
family_id VARCHAR NOT NULL,
|
|
68
|
+
user_id VARCHAR NOT NULL,
|
|
69
|
+
device_id VARCHAR NOT NULL,
|
|
70
|
+
temp_c SMALLINT NOT NULL
|
|
71
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
72
|
+
activity: `CREATE TABLE activity (
|
|
73
|
+
ts TIMESTAMP NOT NULL,
|
|
74
|
+
brand VARCHAR NOT NULL,
|
|
75
|
+
family_id VARCHAR NOT NULL,
|
|
76
|
+
user_id VARCHAR NOT NULL,
|
|
77
|
+
device_id VARCHAR NOT NULL,
|
|
78
|
+
steps INTEGER NOT NULL
|
|
79
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
80
|
+
activity_bucket: `CREATE TABLE activity_bucket (
|
|
81
|
+
ts TIMESTAMP NOT NULL,
|
|
82
|
+
brand VARCHAR NOT NULL,
|
|
83
|
+
family_id VARCHAR NOT NULL,
|
|
84
|
+
user_id VARCHAR NOT NULL,
|
|
85
|
+
device_id VARCHAR NOT NULL,
|
|
86
|
+
calories DOUBLE,
|
|
87
|
+
distance_km DOUBLE
|
|
88
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
89
|
+
sleep_session: `CREATE TABLE sleep_session (
|
|
90
|
+
session_id VARCHAR PRIMARY KEY,
|
|
91
|
+
ts_start TIMESTAMP NOT NULL,
|
|
92
|
+
ts_end TIMESTAMP NOT NULL,
|
|
93
|
+
brand VARCHAR NOT NULL,
|
|
94
|
+
family_id VARCHAR NOT NULL,
|
|
95
|
+
user_id VARCHAR NOT NULL,
|
|
96
|
+
device_id VARCHAR NOT NULL,
|
|
97
|
+
total_minutes INTEGER NOT NULL,
|
|
98
|
+
deep_minutes INTEGER,
|
|
99
|
+
rem_minutes INTEGER,
|
|
100
|
+
light_minutes INTEGER,
|
|
101
|
+
awake_minutes INTEGER,
|
|
102
|
+
avg_confidence DOUBLE,
|
|
103
|
+
night_of DATE
|
|
104
|
+
) PARTITIONED BY (day(ts_start), user_id);`,
|
|
105
|
+
sleep_stage: `CREATE TABLE sleep_stage (
|
|
106
|
+
ts TIMESTAMP NOT NULL,
|
|
107
|
+
session_id VARCHAR NOT NULL,
|
|
108
|
+
brand VARCHAR NOT NULL,
|
|
109
|
+
family_id VARCHAR NOT NULL,
|
|
110
|
+
user_id VARCHAR NOT NULL,
|
|
111
|
+
device_id VARCHAR NOT NULL,
|
|
112
|
+
stage SMALLINT NOT NULL,
|
|
113
|
+
confidence DOUBLE
|
|
114
|
+
) PARTITIONED BY (day(ts), user_id);`,
|
|
115
|
+
sleep_raw: `CREATE TABLE sleep_raw (
|
|
116
|
+
ts TIMESTAMP NOT NULL,
|
|
117
|
+
ts_session_start TIMESTAMP NOT NULL,
|
|
118
|
+
brand VARCHAR NOT NULL,
|
|
119
|
+
family_id VARCHAR NOT NULL,
|
|
120
|
+
user_id VARCHAR NOT NULL,
|
|
121
|
+
device_id VARCHAR NOT NULL,
|
|
122
|
+
quality SMALLINT NOT NULL,
|
|
123
|
+
unit_length SMALLINT
|
|
124
|
+
) PARTITIONED BY (day(ts_session_start), user_id);`,
|
|
125
|
+
device_event: `CREATE TABLE device_event (
|
|
126
|
+
ts TIMESTAMP NOT NULL,
|
|
127
|
+
brand VARCHAR NOT NULL,
|
|
128
|
+
family_id VARCHAR NOT NULL,
|
|
129
|
+
user_id VARCHAR NOT NULL,
|
|
130
|
+
device_id VARCHAR NOT NULL,
|
|
131
|
+
event_type VARCHAR NOT NULL,
|
|
132
|
+
payload JSON
|
|
133
|
+
) PARTITIONED BY (day(ts));`,
|
|
134
|
+
device_config: `CREATE TABLE device_config (
|
|
135
|
+
device_id VARCHAR NOT NULL,
|
|
136
|
+
brand VARCHAR NOT NULL,
|
|
137
|
+
family_id VARCHAR NOT NULL,
|
|
138
|
+
user_id VARCHAR NOT NULL,
|
|
139
|
+
data_type SMALLINT NOT NULL,
|
|
140
|
+
interval_minutes SMALLINT NOT NULL,
|
|
141
|
+
start_time VARCHAR,
|
|
142
|
+
end_time VARCHAR,
|
|
143
|
+
weeks SMALLINT,
|
|
144
|
+
valid_from TIMESTAMP NOT NULL,
|
|
145
|
+
valid_to TIMESTAMP,
|
|
146
|
+
PRIMARY KEY (device_id, data_type, valid_from)
|
|
147
|
+
);`,
|
|
148
|
+
insight: `CREATE TABLE insight (
|
|
149
|
+
id VARCHAR PRIMARY KEY,
|
|
150
|
+
ts TIMESTAMP NOT NULL,
|
|
151
|
+
brand VARCHAR NOT NULL,
|
|
152
|
+
family_id VARCHAR NOT NULL,
|
|
153
|
+
user_id VARCHAR NOT NULL,
|
|
154
|
+
rule_id VARCHAR,
|
|
155
|
+
severity VARCHAR NOT NULL,
|
|
156
|
+
title VARCHAR NOT NULL,
|
|
157
|
+
body TEXT,
|
|
158
|
+
evidence JSON,
|
|
159
|
+
acknowledged_at TIMESTAMP
|
|
160
|
+
) PARTITIONED BY (day(ts));`,
|
|
161
|
+
sync_watermark: `CREATE TABLE sync_watermark (
|
|
162
|
+
brand VARCHAR NOT NULL,
|
|
163
|
+
family_id VARCHAR NOT NULL,
|
|
164
|
+
table_name VARCHAR NOT NULL,
|
|
165
|
+
kind VARCHAR NOT NULL,
|
|
166
|
+
cursor_ts TIMESTAMP NOT NULL,
|
|
167
|
+
updated_at TIMESTAMP NOT NULL,
|
|
168
|
+
PRIMARY KEY (brand, family_id, table_name, kind)
|
|
169
|
+
);`,
|
|
170
|
+
tool_call_audit: `CREATE TABLE tool_call_audit (
|
|
171
|
+
ts TIMESTAMP NOT NULL DEFAULT now(),
|
|
172
|
+
brand VARCHAR NOT NULL,
|
|
173
|
+
family_id VARCHAR NOT NULL,
|
|
174
|
+
requester_user_id VARCHAR NOT NULL,
|
|
175
|
+
tool_name VARCHAR NOT NULL,
|
|
176
|
+
args JSON NOT NULL,
|
|
177
|
+
result_bytes INTEGER,
|
|
178
|
+
result_row_count INTEGER,
|
|
179
|
+
latency_ms INTEGER,
|
|
180
|
+
outcome VARCHAR NOT NULL,
|
|
181
|
+
error_message VARCHAR
|
|
182
|
+
) PARTITIONED BY (day(ts));`,
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Rewrite one base DDL to `CREATE TABLE IF NOT EXISTS <catalog>.<table>`.
|
|
186
|
+
*
|
|
187
|
+
* Exported for tests; call sites should prefer `ensureSchemas`.
|
|
188
|
+
*/
|
|
189
|
+
export function qualifyDdl(baseDdl, table, catalog) {
|
|
190
|
+
return baseDdl.replace(`CREATE TABLE ${table}`, `CREATE TABLE IF NOT EXISTS ${catalog}.${table}`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Create every warehouse table under `<catalog>.<table>`, in `TABLE_NAMES` order.
|
|
194
|
+
*
|
|
195
|
+
* Idempotent — every issued statement uses `IF NOT EXISTS`. Failure at any
|
|
196
|
+
* table surfaces as `AnalyticsError('migration_failed', <table>, cause)`.
|
|
197
|
+
*/
|
|
198
|
+
export async function ensureSchemas(db, catalog) {
|
|
199
|
+
for (const table of TABLE_NAMES) {
|
|
200
|
+
const sql = qualifyDdl(SCHEMAS[table], table, catalog);
|
|
201
|
+
try {
|
|
202
|
+
await db.execute(sql);
|
|
203
|
+
}
|
|
204
|
+
catch (cause) {
|
|
205
|
+
throw new AnalyticsError('migration_failed', `ensureSchemas failed at table ${table}`, cause);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/core/schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAGzC,qEAAqE;AACrE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,KAAK;IACL,YAAY;IACZ,MAAM;IACN,aAAa;IACb,UAAU;IACV,iBAAiB;IACjB,eAAe;IACf,aAAa;IACb,WAAW;IACX,cAAc;IACd,eAAe;IACf,SAAS;IACT,gBAAgB;IAChB,iBAAiB;CACT,CAAA;AAIV;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAwC,MAAM,CAAC,MAAM,CAAC;IACxE,GAAG,EAAE;;;;;;;;;;;qCAW8B;IAEnC,UAAU,EAAE;;;;;;;qCAOuB;IAEnC,IAAI,EAAE;;;;;;;qCAO6B;IAEnC,WAAW,EAAE;;;;;;;qCAOsB;IAEnC,QAAQ,EAAE;;;;;;;qCAOyB;IAEnC,eAAe,EAAE;;;;;;;;qCAQkB;IAEnC,aAAa,EAAE;;;;;;;;;;;;;;;2CAe0B;IAEzC,WAAW,EAAE;;;;;;;;;qCASsB;IAEnC,SAAS,EAAE;;;;;;;;;mDASsC;IAEjD,YAAY,EAAE;;;;;;;;4BAQY;IAE1B,aAAa,EAAE;;;;;;;;;;;;;GAad;IAED,OAAO,EAAE;;;;;;;;;;;;4BAYiB;IAE1B,cAAc,EAAE;;;;;;;;GAQf;IAED,eAAe,EAAE;;;;;;;;;;;;4BAYS;CAC3B,CAAC,CAAA;AAEF;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe,EAAE,KAAgB,EAAE,OAAe;IAC3E,OAAO,OAAO,CAAC,OAAO,CACpB,gBAAgB,KAAK,EAAE,EACvB,8BAA8B,OAAO,IAAI,KAAK,EAAE,CACjD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAgB,EAAE,OAAe;IACnE,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,CAAA;QACtD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACvB,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,kBAAkB,EAClB,iCAAiC,KAAK,EAAE,EACxC,KAAK,CACN,CAAA;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -97,8 +97,18 @@ export interface AnalyticsEngine {
|
|
|
97
97
|
stream<T = unknown>(sql: string, params?: Record<string, unknown>): AsyncIterable<T[]>;
|
|
98
98
|
createAppender(table: string): AnalyticsAppender;
|
|
99
99
|
readonly state: AnalyticsState;
|
|
100
|
+
/** Last machine error surfaced; cleared on successful recovery. */
|
|
101
|
+
readonly lastError: Error | null;
|
|
102
|
+
/** Catalog / warehouse secret name for the current attach; undefined when not attached. */
|
|
103
|
+
readonly catalog: string | undefined;
|
|
100
104
|
subscribe(listener: (s: AnalyticsState) => void): Unsubscribe;
|
|
101
105
|
setRetention(days: number): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Return the last successfully-attached ctx (persisted via KVStore) if it
|
|
108
|
+
* was written within the freshness window (24h). Apps may pass the result
|
|
109
|
+
* to `attach()` to auto-restore after a cold start.
|
|
110
|
+
*/
|
|
111
|
+
getLastAttach(brand: string): Promise<AttachContext | null>;
|
|
102
112
|
close(): Promise<void>;
|
|
103
113
|
}
|
|
104
114
|
/**
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warehouse attach / detach for a single tenant zone.
|
|
3
|
+
*
|
|
4
|
+
* Attaches R2 Iceberg data as a named DuckDB catalog, so downstream code can
|
|
5
|
+
* `SELECT ... FROM zone_<tenantId>.<table>`. The full sequence lives here so
|
|
6
|
+
* the state machine (T-09) can invoke it as a single actor step.
|
|
7
|
+
*
|
|
8
|
+
* Sequence per spec §Attach Protocol:
|
|
9
|
+
* 1. Build warehouse URI via `warehouseUriBuilder(brand, scope, tenantId)`.
|
|
10
|
+
* 2. Fetch bearer token via `tokenVendor.fetch({ brand, scope, tenantId })`.
|
|
11
|
+
* 3. `CREATE OR REPLACE SECRET` bound to the catalog endpoint + bearer token.
|
|
12
|
+
* 4. `ATTACH '<warehouseUri>' AS zone_<tenantId> (TYPE ICEBERG)`.
|
|
13
|
+
* 5. If `tenantScope === 'family'`, prime member ids via
|
|
14
|
+
* `familyMembersProvider({ brand, familyId })` (for retention math).
|
|
15
|
+
*
|
|
16
|
+
* All failures map to `AnalyticsError('attach_failed' | 'token_vendor_failed',
|
|
17
|
+
* <phase>, cause)`.
|
|
18
|
+
*/
|
|
19
|
+
import type { HybridDuckDB } from './engine';
|
|
20
|
+
import type { AttachContext, FamilyMembersProvider, TenantScope, TokenVendor } from './types';
|
|
21
|
+
/** Dependencies required to attach a warehouse — a subset of `AnalyticsConfig`. */
|
|
22
|
+
export interface AttachDeps {
|
|
23
|
+
warehouseUriBuilder: (brand: string, tenantScope: TenantScope, tenantId: string) => string;
|
|
24
|
+
tokenVendor: TokenVendor;
|
|
25
|
+
familyMembersProvider: FamilyMembersProvider;
|
|
26
|
+
catalogEndpoint: string;
|
|
27
|
+
}
|
|
28
|
+
/** Result of a successful attach — surfaced to the machine + config store. */
|
|
29
|
+
export interface AttachResult {
|
|
30
|
+
/** DuckDB secret + catalog alias, e.g. `zone_fam123`. */
|
|
31
|
+
warehouseSecret: string;
|
|
32
|
+
/** S3/Iceberg URI the warehouse points at. */
|
|
33
|
+
warehouseUri: string;
|
|
34
|
+
/** When the bearer token expires (drives 75% TTL refresh in T-09). */
|
|
35
|
+
tokenExpiresAt: Date;
|
|
36
|
+
/** Populated only when `tenantScope === 'family'`. */
|
|
37
|
+
familyMemberIds?: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Derive the DuckDB catalog alias for a tenant. Also used as the SECRET name.
|
|
41
|
+
*
|
|
42
|
+
* Sanitised to `[A-Za-z0-9_]` so it's always a valid DuckDB identifier.
|
|
43
|
+
*/
|
|
44
|
+
export declare function warehouseSecretName(tenantId: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Attach a warehouse for a single tenant zone.
|
|
47
|
+
*
|
|
48
|
+
* Steps are surfaced explicitly in the error message so callers (machine,
|
|
49
|
+
* logger) can see which phase failed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function attachWarehouse(db: HybridDuckDB, ctx: AttachContext, deps: AttachDeps): Promise<AttachResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Reverse of `attachWarehouse`: `DETACH` the catalog then drop its secret.
|
|
54
|
+
* Idempotent from the caller's perspective — a `DETACH` of an unknown alias
|
|
55
|
+
* throws in DuckDB, but the machine only calls this from `attached`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function detachWarehouse(db: HybridDuckDB, tenantId: string): Promise<void>;
|
|
58
|
+
//# sourceMappingURL=warehouse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"warehouse.d.ts","sourceRoot":"","sources":["../../src/core/warehouse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,WAAW,EAEX,WAAW,EACZ,MAAM,SAAS,CAAA;AAchB,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,mBAAmB,EAAE,CACnB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,MAAM,KACb,MAAM,CAAA;IACX,WAAW,EAAE,WAAW,CAAA;IACxB,qBAAqB,EAAE,qBAAqB,CAAA;IAC5C,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,eAAe,EAAE,MAAM,CAAA;IACvB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,cAAc,EAAE,IAAI,CAAA;IACpB,sDAAsD;IACtD,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,YAAY,CAAC,CAsFvB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAaf"}
|