hazo_notify 5.0.1 → 5.3.1
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 +42 -10
- package/dist/components/notification_bell/index.d.ts +11 -1
- package/dist/components/notification_bell/index.d.ts.map +1 -1
- package/dist/components/notification_bell/index.js +33 -1
- package/dist/components/notification_bell/index.js.map +1 -1
- package/dist/lib/adapters/email/adapter.d.ts +2 -0
- package/dist/lib/adapters/email/adapter.d.ts.map +1 -1
- package/dist/lib/adapters/email/adapter.js +53 -22
- package/dist/lib/adapters/email/adapter.js.map +1 -1
- package/dist/lib/adapters/webhook/adapter.d.ts +10 -0
- package/dist/lib/adapters/webhook/adapter.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/adapter.js +90 -0
- package/dist/lib/adapters/webhook/adapter.js.map +1 -0
- package/dist/lib/adapters/webhook/index.d.ts +8 -0
- package/dist/lib/adapters/webhook/index.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/index.js +3 -0
- package/dist/lib/adapters/webhook/index.js.map +1 -0
- package/dist/lib/adapters/webhook/types.d.ts +45 -0
- package/dist/lib/adapters/webhook/types.d.ts.map +1 -0
- package/dist/lib/adapters/webhook/types.js +12 -0
- package/dist/lib/adapters/webhook/types.js.map +1 -0
- package/dist/lib/api/inbox.d.ts +32 -0
- package/dist/lib/api/inbox.d.ts.map +1 -1
- package/dist/lib/api/inbox.js +68 -0
- package/dist/lib/api/inbox.js.map +1 -1
- package/dist/lib/dispatcher/index.d.ts +2 -2
- package/dist/lib/dispatcher/index.d.ts.map +1 -1
- package/dist/lib/dispatcher/index.js +30 -8
- package/dist/lib/dispatcher/index.js.map +1 -1
- package/dist/lib/inbox/broadcaster.d.ts +44 -0
- package/dist/lib/inbox/broadcaster.d.ts.map +1 -0
- package/dist/lib/inbox/broadcaster.js +62 -0
- package/dist/lib/inbox/broadcaster.js.map +1 -0
- package/dist/lib/inbox/index.d.ts +2 -0
- package/dist/lib/inbox/index.d.ts.map +1 -1
- package/dist/lib/inbox/index.js +1 -0
- package/dist/lib/inbox/index.js.map +1 -1
- package/dist/lib/inbox/storage.d.ts.map +1 -1
- package/dist/lib/inbox/storage.js +8 -2
- package/dist/lib/inbox/storage.js.map +1 -1
- package/dist/lib/inbox/types.d.ts +10 -1
- package/dist/lib/inbox/types.d.ts.map +1 -1
- package/dist/lib/jobs/digest.d.ts +77 -0
- package/dist/lib/jobs/digest.d.ts.map +1 -0
- package/dist/lib/jobs/digest.js +127 -0
- package/dist/lib/jobs/digest.js.map +1 -0
- package/dist/lib/jobs/index.d.ts +2 -0
- package/dist/lib/jobs/index.d.ts.map +1 -1
- package/dist/lib/jobs/index.js +1 -0
- package/dist/lib/jobs/index.js.map +1 -1
- package/dist/lib/jobs/submit.d.ts.map +1 -1
- package/dist/lib/jobs/submit.js +1 -0
- package/dist/lib/jobs/submit.js.map +1 -1
- package/dist/lib/jobs/types.d.ts +1 -0
- package/dist/lib/jobs/types.d.ts.map +1 -1
- package/dist/lib/lifecycle/default_templates.d.ts +9 -0
- package/dist/lib/lifecycle/default_templates.d.ts.map +1 -0
- package/dist/lib/lifecycle/default_templates.js +196 -0
- package/dist/lib/lifecycle/default_templates.js.map +1 -0
- package/dist/lib/lifecycle/dispatch.d.ts +37 -0
- package/dist/lib/lifecycle/dispatch.d.ts.map +1 -0
- package/dist/lib/lifecycle/dispatch.js +32 -0
- package/dist/lib/lifecycle/dispatch.js.map +1 -0
- package/dist/lib/lifecycle/events.d.ts +34 -0
- package/dist/lib/lifecycle/events.d.ts.map +1 -0
- package/dist/lib/lifecycle/events.js +118 -0
- package/dist/lib/lifecycle/events.js.map +1 -0
- package/dist/lib/lifecycle/handler.d.ts +29 -0
- package/dist/lib/lifecycle/handler.d.ts.map +1 -0
- package/dist/lib/lifecycle/handler.js +145 -0
- package/dist/lib/lifecycle/handler.js.map +1 -0
- package/dist/lib/lifecycle/index.d.ts +23 -0
- package/dist/lib/lifecycle/index.d.ts.map +1 -0
- package/dist/lib/lifecycle/index.js +21 -0
- package/dist/lib/lifecycle/index.js.map +1 -0
- package/dist/lib/lifecycle/register.d.ts +29 -0
- package/dist/lib/lifecycle/register.d.ts.map +1 -0
- package/dist/lib/lifecycle/register.js +29 -0
- package/dist/lib/lifecycle/register.js.map +1 -0
- package/dist/lib/lifecycle/resolver.d.ts +22 -0
- package/dist/lib/lifecycle/resolver.d.ts.map +1 -0
- package/dist/lib/lifecycle/resolver.js +105 -0
- package/dist/lib/lifecycle/resolver.js.map +1 -0
- package/dist/lib/lifecycle/scheduler.d.ts +27 -0
- package/dist/lib/lifecycle/scheduler.d.ts.map +1 -0
- package/dist/lib/lifecycle/scheduler.js +108 -0
- package/dist/lib/lifecycle/scheduler.js.map +1 -0
- package/dist/lib/lifecycle/status.d.ts +39 -0
- package/dist/lib/lifecycle/status.d.ts.map +1 -0
- package/dist/lib/lifecycle/status.js +67 -0
- package/dist/lib/lifecycle/status.js.map +1 -0
- package/dist/lib/lifecycle/types.d.ts +165 -0
- package/dist/lib/lifecycle/types.d.ts.map +1 -0
- package/dist/lib/lifecycle/types.js +11 -0
- package/dist/lib/lifecycle/types.js.map +1 -0
- package/dist/lib/preferences/index.d.ts +3 -0
- package/dist/lib/preferences/index.d.ts.map +1 -0
- package/dist/lib/preferences/index.js +3 -0
- package/dist/lib/preferences/index.js.map +1 -0
- package/dist/lib/preferences/storage.d.ts +33 -0
- package/dist/lib/preferences/storage.d.ts.map +1 -0
- package/dist/lib/preferences/storage.js +129 -0
- package/dist/lib/preferences/storage.js.map +1 -0
- package/dist/lib/preferences/types.d.ts +59 -0
- package/dist/lib/preferences/types.d.ts.map +1 -0
- package/dist/lib/preferences/types.js +20 -0
- package/dist/lib/preferences/types.js.map +1 -0
- package/dist/lib/template_manager/db/template_repository.d.ts +4 -1
- package/dist/lib/template_manager/db/template_repository.d.ts.map +1 -1
- package/dist/lib/template_manager/db/template_repository.js +17 -1
- package/dist/lib/template_manager/db/template_repository.js.map +1 -1
- package/dist/lib/template_manager/types.d.ts +1 -0
- package/dist/lib/template_manager/types.d.ts.map +1 -1
- package/migrations/008_lifecycle_status.pg.sql +27 -0
- package/migrations/008_lifecycle_status.sqlite.sql +24 -0
- package/migrations/009_templates_locale.pg.sql +6 -0
- package/migrations/009_templates_locale.sqlite.sql +4 -0
- package/migrations/010_preferences.pg.sql +26 -0
- package/migrations/010_preferences.sqlite.sql +32 -0
- package/migrations/011_digest_status.pg.sql +21 -0
- package/migrations/011_digest_status.sqlite.sql +43 -0
- package/migrations/012_preferences_digest.pg.sql +12 -0
- package/migrations/012_preferences_digest.sqlite.sql +51 -0
- package/package.json +15 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createLifecycleScheduler` factory.
|
|
3
|
+
*
|
|
4
|
+
* Returns a `LifecycleScheduler` that manages the full lifecycle of a
|
|
5
|
+
* multi-step email sequence per user: starting sequences, recording events,
|
|
6
|
+
* and reading status.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import { insertStatusRow, queryStatusRows } from './status.js';
|
|
11
|
+
import { recordEvent as _recordEvent } from './events.js';
|
|
12
|
+
import { createLifecycleStepHandler } from './handler.js';
|
|
13
|
+
/**
|
|
14
|
+
* Job type string registered with hazo_jobs for deferred lifecycle steps.
|
|
15
|
+
* Re-exported from `hazo_notify/lifecycle` for consumer use.
|
|
16
|
+
*/
|
|
17
|
+
export const LIFECYCLE_STEP_JOB_TYPE = 'hazo_notify:lifecycle_step';
|
|
18
|
+
/**
|
|
19
|
+
* Detect UNIQUE / duplicate-key violation errors from Postgres (PostgREST),
|
|
20
|
+
* SQLite (better-sqlite3), and generic RDBMS drivers.
|
|
21
|
+
*
|
|
22
|
+
* PostgREST returns HTTP 409 with "duplicate key" in the message body.
|
|
23
|
+
* better-sqlite3 throws "UNIQUE constraint failed".
|
|
24
|
+
*/
|
|
25
|
+
function is_unique_violation(err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
|
|
27
|
+
return msg.includes('unique') || msg.includes('duplicate');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a lifecycle scheduler.
|
|
31
|
+
*
|
|
32
|
+
* The returned `LifecycleScheduler` should be created once at application
|
|
33
|
+
* startup and reused across requests. Callers must supply a `get_user_email`
|
|
34
|
+
* function so the scheduler can resolve recipient addresses at dispatch time.
|
|
35
|
+
*
|
|
36
|
+
* @param opts - Scheduler configuration.
|
|
37
|
+
* @param get_user_email - Async resolver from user_id to email address.
|
|
38
|
+
*/
|
|
39
|
+
export function createLifecycleScheduler(opts, get_user_email) {
|
|
40
|
+
const job_handler = createLifecycleStepHandler(opts, get_user_email);
|
|
41
|
+
async function start({ user_id, sequence_id, }) {
|
|
42
|
+
const steps = opts.sequences[sequence_id];
|
|
43
|
+
if (!steps) {
|
|
44
|
+
throw new Error(`[lifecycle] sequence not found: '${sequence_id}'`);
|
|
45
|
+
}
|
|
46
|
+
const now = new Date();
|
|
47
|
+
for (const step of steps) {
|
|
48
|
+
// 1. Insert status row (idempotent — skip on unique violation).
|
|
49
|
+
let inserted_row;
|
|
50
|
+
try {
|
|
51
|
+
inserted_row = await insertStatusRow(opts.hazo_connect, {
|
|
52
|
+
app_id: opts.app_id,
|
|
53
|
+
user_id,
|
|
54
|
+
sequence_id,
|
|
55
|
+
step_id: step.id,
|
|
56
|
+
status: 'scheduled',
|
|
57
|
+
sent_at: null,
|
|
58
|
+
skip_reason: null,
|
|
59
|
+
job_id: null,
|
|
60
|
+
failure_message: null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (is_unique_violation(err)) {
|
|
65
|
+
// Already started — idempotent no-op for this step.
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
// 2. For time-triggered steps, submit a deferred job.
|
|
71
|
+
if (step.trigger.kind === 'time') {
|
|
72
|
+
const run_at = new Date(now.getTime() + step.trigger.offset_seconds * 1000).toISOString();
|
|
73
|
+
const { jobId } = await opts.jobs.submit({
|
|
74
|
+
type: LIFECYCLE_STEP_JOB_TYPE,
|
|
75
|
+
payload: {
|
|
76
|
+
status_row_id: inserted_row.id,
|
|
77
|
+
app_id: opts.app_id,
|
|
78
|
+
user_id,
|
|
79
|
+
sequence_id,
|
|
80
|
+
step_id: step.id,
|
|
81
|
+
},
|
|
82
|
+
runAt: run_at,
|
|
83
|
+
});
|
|
84
|
+
// Persist job_id on the status row so it can be correlated later.
|
|
85
|
+
try {
|
|
86
|
+
await opts.hazo_connect.updateById('hazo_notify_lifecycle_status', inserted_row.id, { job_id: jobId });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Non-fatal: the job was still submitted. job_id is cosmetic.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Event-triggered steps: no job submitted — they fire via recordEvent().
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function recordEventPublic(params) {
|
|
96
|
+
return _recordEvent(opts, get_user_email, params);
|
|
97
|
+
}
|
|
98
|
+
async function getStatus({ user_id, sequence_id, }) {
|
|
99
|
+
return queryStatusRows(opts.hazo_connect, opts.app_id, user_id, sequence_id);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
start,
|
|
103
|
+
recordEvent: recordEventPublic,
|
|
104
|
+
getStatus,
|
|
105
|
+
__jobHandler: job_handler,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD helpers for the `hazo_notify_lifecycle_status` table.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept a `HazoConnectInstance` so they work against any
|
|
5
|
+
* conforming adapter (PostgREST, direct Postgres, SQLite test doubles, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import type { HazoConnectInstance } from '../template_manager/types.js';
|
|
10
|
+
import type { LifecycleStatusRow } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Insert a new lifecycle status row.
|
|
13
|
+
*
|
|
14
|
+
* The caller must NOT supply `id`, `scheduled_at`, `attempt_count`, or
|
|
15
|
+
* `dispatch_meta` — those are set by the DB default or left as NULL.
|
|
16
|
+
*/
|
|
17
|
+
export declare function insertStatusRow(hazo_connect: HazoConnectInstance, row: Omit<LifecycleStatusRow, 'id' | 'scheduled_at' | 'attempt_count' | 'dispatch_meta'>): Promise<LifecycleStatusRow>;
|
|
18
|
+
/**
|
|
19
|
+
* Load a single status row by primary key.
|
|
20
|
+
* Returns `null` if not found.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadStatusRow(hazo_connect: HazoConnectInstance, id: string): Promise<LifecycleStatusRow | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Partial update a status row by primary key.
|
|
25
|
+
* The `patch` may include any subset of `LifecycleStatusRow` fields.
|
|
26
|
+
*/
|
|
27
|
+
export declare function updateStatusRow(hazo_connect: HazoConnectInstance, id: string, patch: Partial<LifecycleStatusRow>): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Query status rows for a (app_id, user_id) pair, optionally filtered to one
|
|
30
|
+
* sequence. Returns rows ordered by `scheduled_at` ascending.
|
|
31
|
+
*/
|
|
32
|
+
export declare function queryStatusRows(hazo_connect: HazoConnectInstance, app_id: string, user_id: string, sequence_id?: string): Promise<LifecycleStatusRow[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Find a single status row for a specific (app_id, user_id, sequence_id, step_id).
|
|
35
|
+
* Used by `recordEvent` to locate event-triggered steps.
|
|
36
|
+
* Returns `null` if not found.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findEventStepRow(hazo_connect: HazoConnectInstance, app_id: string, user_id: string, sequence_id: string, step_id: string): Promise<LifecycleStatusRow | null>;
|
|
39
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,IAAI,GAAG,cAAc,GAAG,eAAe,GAAG,eAAe,CAAC,GACvF,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,YAAY,EAAE,mBAAmB,EACjC,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAEpC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,YAAY,EAAE,mBAAmB,EACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAe/B;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,mBAAmB,EACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAapC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD helpers for the `hazo_notify_lifecycle_status` table.
|
|
3
|
+
*
|
|
4
|
+
* All functions accept a `HazoConnectInstance` so they work against any
|
|
5
|
+
* conforming adapter (PostgREST, direct Postgres, SQLite test doubles, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
const TABLE = 'hazo_notify_lifecycle_status';
|
|
10
|
+
/**
|
|
11
|
+
* Insert a new lifecycle status row.
|
|
12
|
+
*
|
|
13
|
+
* The caller must NOT supply `id`, `scheduled_at`, `attempt_count`, or
|
|
14
|
+
* `dispatch_meta` — those are set by the DB default or left as NULL.
|
|
15
|
+
*/
|
|
16
|
+
export async function insertStatusRow(hazo_connect, row) {
|
|
17
|
+
return hazo_connect.insert(TABLE, row);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a single status row by primary key.
|
|
21
|
+
* Returns `null` if not found.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadStatusRow(hazo_connect, id) {
|
|
24
|
+
return hazo_connect.findById(TABLE, id);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Partial update a status row by primary key.
|
|
28
|
+
* The `patch` may include any subset of `LifecycleStatusRow` fields.
|
|
29
|
+
*/
|
|
30
|
+
export async function updateStatusRow(hazo_connect, id, patch) {
|
|
31
|
+
await hazo_connect.updateById(TABLE, id, patch);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Query status rows for a (app_id, user_id) pair, optionally filtered to one
|
|
35
|
+
* sequence. Returns rows ordered by `scheduled_at` ascending.
|
|
36
|
+
*/
|
|
37
|
+
export async function queryStatusRows(hazo_connect, app_id, user_id, sequence_id) {
|
|
38
|
+
let q = hazo_connect
|
|
39
|
+
.query()
|
|
40
|
+
.from(TABLE)
|
|
41
|
+
.select('*')
|
|
42
|
+
.where('app_id', 'eq', app_id)
|
|
43
|
+
.where('user_id', 'eq', user_id);
|
|
44
|
+
if (sequence_id !== undefined) {
|
|
45
|
+
q = q.where('sequence_id', 'eq', sequence_id);
|
|
46
|
+
}
|
|
47
|
+
q = q.order('scheduled_at', 'asc');
|
|
48
|
+
return hazo_connect.list(q);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Find a single status row for a specific (app_id, user_id, sequence_id, step_id).
|
|
52
|
+
* Used by `recordEvent` to locate event-triggered steps.
|
|
53
|
+
* Returns `null` if not found.
|
|
54
|
+
*/
|
|
55
|
+
export async function findEventStepRow(hazo_connect, app_id, user_id, sequence_id, step_id) {
|
|
56
|
+
const rows = await hazo_connect.list(hazo_connect
|
|
57
|
+
.query()
|
|
58
|
+
.from(TABLE)
|
|
59
|
+
.select('*')
|
|
60
|
+
.where('app_id', 'eq', app_id)
|
|
61
|
+
.where('user_id', 'eq', user_id)
|
|
62
|
+
.where('sequence_id', 'eq', sequence_id)
|
|
63
|
+
.where('step_id', 'eq', step_id)
|
|
64
|
+
.limit(1));
|
|
65
|
+
return rows[0] ?? null;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/status.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,KAAK,GAAG,8BAA8B,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,GAAwF;IAExF,OAAO,YAAY,CAAC,MAAM,CAAqB,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,YAAiC,EACjC,EAAU;IAEV,OAAO,YAAY,CAAC,QAAQ,CAAqB,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,EAAU,EACV,KAAkC;IAElC,MAAM,YAAY,CAAC,UAAU,CAAqB,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,YAAiC,EACjC,MAAc,EACd,OAAe,EACf,WAAoB;IAEpB,IAAI,CAAC,GAAG,YAAY;SACjB,KAAK,EAAE;SACP,IAAI,CAAC,KAAK,CAAC;SACX,MAAM,CAAC,GAAG,CAAC;SACX,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC;SAC7B,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAEnC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAChD,CAAC;IAED,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IAEnC,OAAO,YAAY,CAAC,IAAI,CAAqB,CAAC,CAAC,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,YAAiC,EACjC,MAAc,EACd,OAAe,EACf,WAAmB,EACnB,OAAe;IAEf,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,IAAI,CAClC,YAAY;SACT,KAAK,EAAE;SACP,IAAI,CAAC,KAAK,CAAC;SACX,MAAM,CAAC,GAAG,CAAC;SACX,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC;SAC7B,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC;SAC/B,KAAK,CAAC,aAAa,EAAE,IAAI,EAAE,WAAW,CAAC;SACvC,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC;SAC/B,KAAK,CAAC,CAAC,CAAC,CACZ,CAAC;IACF,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the hazo_notify lifecycle scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle sequences define time-triggered and event-triggered email steps
|
|
5
|
+
* with conditional dispatch logic. The scheduler manages status tracking and
|
|
6
|
+
* deferred job submission via hazo_jobs (optional peer dep).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
import type { HazoConnectInstance } from '../template_manager/types.js';
|
|
11
|
+
import type { HazoJob, HazoJobLogger, HazoJobHandler } from '../jobs/types.js';
|
|
12
|
+
export type { HazoConnectInstance, HazoJob, HazoJobLogger, HazoJobHandler };
|
|
13
|
+
/**
|
|
14
|
+
* Structural subset of hazo_jobs `JobsClient.submit` extended with `runAt`
|
|
15
|
+
* for deferred scheduling. Declared locally so the lifecycle module does not
|
|
16
|
+
* import from `hazo_jobs` at runtime (optional peer dep).
|
|
17
|
+
*/
|
|
18
|
+
export interface HazoJobsSubmitter {
|
|
19
|
+
submit(opts: {
|
|
20
|
+
type: string;
|
|
21
|
+
payload?: unknown;
|
|
22
|
+
priority?: number;
|
|
23
|
+
maxAttempts?: number;
|
|
24
|
+
expiresInSec?: number;
|
|
25
|
+
/** ISO 8601 string for deferred execution. */
|
|
26
|
+
runAt?: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
jobId: string;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
/** A time-based trigger fires after `offset_seconds` from sequence start. */
|
|
32
|
+
export type LifecycleTrigger = {
|
|
33
|
+
kind: 'time';
|
|
34
|
+
offset_seconds: number;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'event';
|
|
37
|
+
event_kind: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A leaf condition node. The `resolver` key names a function in
|
|
41
|
+
* `LifecycleSchedulerOptions.resolvers` that computes the current value
|
|
42
|
+
* for `user_id`. The result is compared against `value` using `op`.
|
|
43
|
+
*/
|
|
44
|
+
export interface Condition {
|
|
45
|
+
resolver: string;
|
|
46
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte';
|
|
47
|
+
value: unknown;
|
|
48
|
+
args?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
/** A composite condition node (boolean operators) or a leaf `Condition`. */
|
|
51
|
+
export type ConditionNode = Condition | {
|
|
52
|
+
kind: 'and';
|
|
53
|
+
conditions: ConditionNode[];
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'or';
|
|
56
|
+
conditions: ConditionNode[];
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'not';
|
|
59
|
+
condition: ConditionNode;
|
|
60
|
+
};
|
|
61
|
+
/** A single step within a lifecycle sequence. */
|
|
62
|
+
export interface LifecycleStep {
|
|
63
|
+
id: string;
|
|
64
|
+
trigger: LifecycleTrigger;
|
|
65
|
+
/** Template name to render and send when this step dispatches. */
|
|
66
|
+
template: string;
|
|
67
|
+
/**
|
|
68
|
+
* Top-level conditions evaluated as an implicit AND. All must pass for the
|
|
69
|
+
* step to dispatch. An empty array means "always dispatch".
|
|
70
|
+
*/
|
|
71
|
+
conditions: ConditionNode[];
|
|
72
|
+
}
|
|
73
|
+
export interface LifecycleStatusRow {
|
|
74
|
+
id: string;
|
|
75
|
+
app_id: string;
|
|
76
|
+
user_id: string;
|
|
77
|
+
sequence_id: string;
|
|
78
|
+
step_id: string;
|
|
79
|
+
status: 'scheduled' | 'sent' | 'skipped' | 'failed';
|
|
80
|
+
scheduled_at: string;
|
|
81
|
+
sent_at: string | null;
|
|
82
|
+
skip_reason: string | null;
|
|
83
|
+
job_id: string | null;
|
|
84
|
+
failure_message: string | null;
|
|
85
|
+
attempt_count: number;
|
|
86
|
+
dispatch_meta: Record<string, unknown> | null;
|
|
87
|
+
}
|
|
88
|
+
export interface LifecycleSchedulerOptions {
|
|
89
|
+
/** DB adapter (from hazo_notify/template_manager). */
|
|
90
|
+
hazo_connect: HazoConnectInstance;
|
|
91
|
+
/**
|
|
92
|
+
* Structural hazo_jobs submitter. Must support `runAt` for deferred steps.
|
|
93
|
+
* Declared locally — no runtime `hazo_jobs` import.
|
|
94
|
+
*/
|
|
95
|
+
jobs: HazoJobsSubmitter;
|
|
96
|
+
/** Application identifier stored on every status row. */
|
|
97
|
+
app_id: string;
|
|
98
|
+
/** Default locale for template resolution (v1: English only). */
|
|
99
|
+
default_locale?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Named resolver functions. Each resolver receives a `user_id` and optional
|
|
102
|
+
* `args`, and returns the current value for that dimension (number, boolean,
|
|
103
|
+
* string, etc.). Used by the condition evaluator.
|
|
104
|
+
*/
|
|
105
|
+
resolvers: Record<string, (user_id: string, args?: Record<string, unknown>) => Promise<unknown>>;
|
|
106
|
+
/**
|
|
107
|
+
* Optional opt-out check. When provided, the handler calls this before
|
|
108
|
+
* dispatching; if it returns `true`, the step is skipped with
|
|
109
|
+
* `skip_reason='opted_out'`.
|
|
110
|
+
*/
|
|
111
|
+
is_opted_out?: (user_id: string) => Promise<boolean>;
|
|
112
|
+
/** Sequence definitions keyed by sequence_id. */
|
|
113
|
+
sequences: Record<string, LifecycleStep[]>;
|
|
114
|
+
/**
|
|
115
|
+
* Max job attempts before a step row is marked `failed`.
|
|
116
|
+
* Defaults to 3.
|
|
117
|
+
*/
|
|
118
|
+
max_attempts?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Optional per-user variable resolver. Called at dispatch time to supply
|
|
121
|
+
* extra template variables (e.g. personalised opt-out links). The returned
|
|
122
|
+
* object is merged with the base `variables` map; caller-supplied keys win.
|
|
123
|
+
*/
|
|
124
|
+
get_variables?: (user_id: string, step_id: string) => Promise<Record<string, string>>;
|
|
125
|
+
}
|
|
126
|
+
export interface LifecycleScheduler {
|
|
127
|
+
/**
|
|
128
|
+
* Begin a sequence for a user. Inserts one status row per step and submits
|
|
129
|
+
* deferred jobs for time-triggered steps. Idempotent — duplicate calls for
|
|
130
|
+
* the same (user_id, sequence_id) are no-ops (unique constraint handling).
|
|
131
|
+
*/
|
|
132
|
+
start(opts: {
|
|
133
|
+
user_id: string;
|
|
134
|
+
sequence_id: string;
|
|
135
|
+
}): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Fire event-triggered steps. Scans all sequences for steps whose trigger
|
|
138
|
+
* matches `event_kind`, then dispatches any that are in `scheduled` state.
|
|
139
|
+
*/
|
|
140
|
+
recordEvent(opts: {
|
|
141
|
+
user_id: string;
|
|
142
|
+
event_kind: string;
|
|
143
|
+
payload?: Record<string, unknown>;
|
|
144
|
+
}): Promise<void>;
|
|
145
|
+
/** Read status rows for a user, optionally filtered to one sequence. */
|
|
146
|
+
getStatus(opts: {
|
|
147
|
+
user_id: string;
|
|
148
|
+
sequence_id?: string;
|
|
149
|
+
}): Promise<LifecycleStatusRow[]>;
|
|
150
|
+
/**
|
|
151
|
+
* Internal hazo_jobs handler exposed so `registerLifecycleHandlers` can wire
|
|
152
|
+
* it in without reaching into scheduler internals. Not intended for direct
|
|
153
|
+
* consumer use.
|
|
154
|
+
*/
|
|
155
|
+
__jobHandler: HazoJobHandler<LifecycleStepPayload>;
|
|
156
|
+
}
|
|
157
|
+
/** Payload submitted when a time-triggered lifecycle step is deferred. */
|
|
158
|
+
export interface LifecycleStepPayload {
|
|
159
|
+
status_row_id: string;
|
|
160
|
+
app_id: string;
|
|
161
|
+
user_id: string;
|
|
162
|
+
sequence_id: string;
|
|
163
|
+
step_id: string;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/lifecycle/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAG/E,YAAY,EAAE,mBAAmB,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC;AAM5E;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,IAAI,EAAE;QACX,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,8CAA8C;QAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChC;AAMD,6EAA6E;AAC7E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAM1C;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,4EAA4E;AAC5E,MAAM,MAAM,aAAa,GACrB,SAAS,GACT;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,aAAa,CAAA;CAAE,CAAC;AAM9C,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,gBAAgB,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAMD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC/C;AAMD,MAAM,WAAW,yBAAyB;IACxC,sDAAsD;IACtD,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;OAGG;IACH,IAAI,EAAE,iBAAiB,CAAC;IACxB,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,SAAS,EAAE,MAAM,CACf,MAAM,EACN,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CACtE,CAAC;IACF;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;IAC3C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACtC;AAMD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE;;;OAGG;IACH,WAAW,CAAC,IAAI,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,IAAI,EAAE;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAClC;;;;OAIG;IACH,YAAY,EAAE,cAAc,CAAC,oBAAoB,CAAC,CAAC;CACpD;AAMD,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the hazo_notify lifecycle scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle sequences define time-triggered and event-triggered email steps
|
|
5
|
+
* with conditional dispatch logic. The scheduler manages status tracking and
|
|
6
|
+
* deferred job submission via hazo_jobs (optional peer dep).
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/lib/lifecycle/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { set_preference, get_preference, list_preferences, delete_preference, batchGetPreferences, } from './storage.js';
|
|
2
|
+
export type { PreferenceMode, PreferenceRow, SetPreferenceInput, GetPreferenceOptions, BatchGetPreferencesOptions, } from './types.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/preferences/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/preferences/index.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PreferenceRow, SetPreferenceInput, GetPreferenceOptions, BatchGetPreferencesOptions, PreferenceMode } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* List all preference rows for a user, optionally filtered by scope_id.
|
|
4
|
+
*/
|
|
5
|
+
export declare function list_preferences(opts: {
|
|
6
|
+
user_id: string;
|
|
7
|
+
scope_id?: string | null;
|
|
8
|
+
}): Promise<PreferenceRow[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Upsert a preference row. A second call with the same key updates `mode`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function set_preference(input: SetPreferenceInput): Promise<PreferenceRow>;
|
|
13
|
+
/**
|
|
14
|
+
* Get the exact preference row for a key. Does NOT perform 5-level resolution.
|
|
15
|
+
*/
|
|
16
|
+
export declare function get_preference(opts: GetPreferenceOptions): Promise<PreferenceRow | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Delete an exact preference row. Returns true if deleted, false if not found.
|
|
19
|
+
*/
|
|
20
|
+
export declare function delete_preference(opts: GetPreferenceOptions): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Batch-resolve preference mode for (user × channel) pairs using 5-level resolution.
|
|
23
|
+
* Returns Map<user_id, Map<channel_id, PreferenceMode>>.
|
|
24
|
+
*
|
|
25
|
+
* Resolution priority (lower number wins):
|
|
26
|
+
* 1. (user, scope, event_type, channel) — exact match
|
|
27
|
+
* 2. (user, scope, null, channel) — scoped, any event
|
|
28
|
+
* 3. (user, null, event_type, channel) — global scope, specific event
|
|
29
|
+
* 4. (user, null, null, channel) — global wildcard
|
|
30
|
+
* 5. default_mode — consumer-supplied fallback
|
|
31
|
+
*/
|
|
32
|
+
export declare function batchGetPreferences(opts: BatchGetPreferencesOptions): Promise<Map<string, Map<string, PreferenceMode>>>;
|
|
33
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/lib/preferences/storage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,0BAA0B,EAC1B,cAAc,EACf,MAAM,YAAY,CAAC;AAqBpB;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAS3B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAuCtF;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAU9F;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAKpF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAuCnD"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/lib/preferences/storage.ts
|
|
2
|
+
/**
|
|
3
|
+
* Preference storage layer for hazo_notify.
|
|
4
|
+
* Uses rawQuery via getInboxConnection() — compatible with both SQLite and Postgres.
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { getInboxConnection } from '../inbox/connection.js';
|
|
9
|
+
const TABLE = 'hazo_notify_preferences';
|
|
10
|
+
function conn() {
|
|
11
|
+
return getInboxConnection();
|
|
12
|
+
}
|
|
13
|
+
/** Priority index for the 5-level resolution chain (lower = higher priority). */
|
|
14
|
+
function priority(row, scope_id, event_type) {
|
|
15
|
+
const scope_match = row.scope_id === scope_id;
|
|
16
|
+
const event_match = row.event_type === event_type;
|
|
17
|
+
if (scope_match && event_match)
|
|
18
|
+
return 1;
|
|
19
|
+
if (scope_match && row.event_type === null)
|
|
20
|
+
return 2;
|
|
21
|
+
if (row.scope_id === null && event_match)
|
|
22
|
+
return 3;
|
|
23
|
+
if (row.scope_id === null && row.event_type === null)
|
|
24
|
+
return 4;
|
|
25
|
+
return 99;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* List all preference rows for a user, optionally filtered by scope_id.
|
|
29
|
+
*/
|
|
30
|
+
export async function list_preferences(opts) {
|
|
31
|
+
const rows = await conn().rawQuery(`SELECT * FROM ${TABLE} WHERE user_id = ?`, { params: [opts.user_id] });
|
|
32
|
+
if (opts.scope_id !== undefined) {
|
|
33
|
+
return rows.filter(r => r.scope_id === opts.scope_id);
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Upsert a preference row. A second call with the same key updates `mode`.
|
|
39
|
+
*/
|
|
40
|
+
export async function set_preference(input) {
|
|
41
|
+
const VALID_MODES = ['on', 'off', 'digest'];
|
|
42
|
+
if (!VALID_MODES.includes(input.mode)) {
|
|
43
|
+
throw new Error(`Invalid preference mode '${input.mode}'. Must be one of: ${VALID_MODES.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
const all = await list_preferences({ user_id: input.user_id });
|
|
46
|
+
const match = all.find(r => r.scope_id === (input.scope_id ?? null) &&
|
|
47
|
+
r.event_type === (input.event_type ?? null) &&
|
|
48
|
+
r.channel_id === input.channel_id);
|
|
49
|
+
if (match) {
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
await conn().rawQuery(`UPDATE ${TABLE} SET mode = ?, updated_at = ? WHERE id = ?`, { params: [input.mode, now, match.id] });
|
|
52
|
+
return { ...match, mode: input.mode, updated_at: now };
|
|
53
|
+
}
|
|
54
|
+
const id = randomUUID();
|
|
55
|
+
const now = new Date().toISOString();
|
|
56
|
+
await conn().rawQuery(`INSERT INTO ${TABLE} (id, user_id, scope_id, event_type, channel_id, mode, created_at, updated_at)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, { params: [id, input.user_id, input.scope_id ?? null, input.event_type ?? null, input.channel_id, input.mode, now, now] });
|
|
58
|
+
return {
|
|
59
|
+
id,
|
|
60
|
+
user_id: input.user_id,
|
|
61
|
+
scope_id: input.scope_id ?? null,
|
|
62
|
+
event_type: input.event_type ?? null,
|
|
63
|
+
channel_id: input.channel_id,
|
|
64
|
+
mode: input.mode,
|
|
65
|
+
created_at: now,
|
|
66
|
+
updated_at: now,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get the exact preference row for a key. Does NOT perform 5-level resolution.
|
|
71
|
+
*/
|
|
72
|
+
export async function get_preference(opts) {
|
|
73
|
+
const all = await list_preferences({ user_id: opts.user_id });
|
|
74
|
+
return (all.find(r => r.scope_id === opts.scope_id &&
|
|
75
|
+
r.event_type === opts.event_type &&
|
|
76
|
+
r.channel_id === opts.channel_id) ?? null);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Delete an exact preference row. Returns true if deleted, false if not found.
|
|
80
|
+
*/
|
|
81
|
+
export async function delete_preference(opts) {
|
|
82
|
+
const row = await get_preference(opts);
|
|
83
|
+
if (!row)
|
|
84
|
+
return false;
|
|
85
|
+
await conn().rawQuery(`DELETE FROM ${TABLE} WHERE id = ?`, { params: [row.id] });
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Batch-resolve preference mode for (user × channel) pairs using 5-level resolution.
|
|
90
|
+
* Returns Map<user_id, Map<channel_id, PreferenceMode>>.
|
|
91
|
+
*
|
|
92
|
+
* Resolution priority (lower number wins):
|
|
93
|
+
* 1. (user, scope, event_type, channel) — exact match
|
|
94
|
+
* 2. (user, scope, null, channel) — scoped, any event
|
|
95
|
+
* 3. (user, null, event_type, channel) — global scope, specific event
|
|
96
|
+
* 4. (user, null, null, channel) — global wildcard
|
|
97
|
+
* 5. default_mode — consumer-supplied fallback
|
|
98
|
+
*/
|
|
99
|
+
export async function batchGetPreferences(opts) {
|
|
100
|
+
const { user_ids, scope_id, event_type, channel_ids, default_mode = 'on' } = opts;
|
|
101
|
+
if (user_ids.length === 0 || channel_ids.length === 0) {
|
|
102
|
+
return new Map();
|
|
103
|
+
}
|
|
104
|
+
// Fetch all rows for these users (preference tables are small — one query per user)
|
|
105
|
+
const all_rows = [];
|
|
106
|
+
for (const user_id of user_ids) {
|
|
107
|
+
const rows = await list_preferences({ user_id });
|
|
108
|
+
all_rows.push(...rows);
|
|
109
|
+
}
|
|
110
|
+
const result = new Map();
|
|
111
|
+
for (const user_id of user_ids) {
|
|
112
|
+
const channel_map = new Map();
|
|
113
|
+
for (const channel_id of channel_ids) {
|
|
114
|
+
const candidates = all_rows.filter(r => r.user_id === user_id &&
|
|
115
|
+
r.channel_id === channel_id &&
|
|
116
|
+
(r.scope_id === scope_id || r.scope_id === null) &&
|
|
117
|
+
(r.event_type === event_type || r.event_type === null));
|
|
118
|
+
if (candidates.length === 0) {
|
|
119
|
+
channel_map.set(channel_id, default_mode);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
candidates.sort((a, b) => priority(a, scope_id, event_type) - priority(b, scope_id, event_type));
|
|
123
|
+
channel_map.set(channel_id, candidates[0].mode);
|
|
124
|
+
}
|
|
125
|
+
result.set(user_id, channel_map);
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../../src/lib/preferences/storage.ts"],"names":[],"mappings":"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"}
|