@shware/workflow 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -40
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +50 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +4 -3
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +4 -3
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +49 -7
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +15 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +15 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +29 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +15 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +15 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +35 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +35 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +46 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +340 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +58 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +58 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +336 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +66 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +31 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +31 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +66 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +14 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +10 -10
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +10 -10
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +14 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +1 -1
- package/dist/transports/cloudflare-email.mjs +1 -1
- package/dist/transports/ses.cjs +47 -6
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +47 -7
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.cjs";
|
|
2
2
|
import { PushDevice } from "../engine/ports.cjs";
|
|
3
|
+
import { ErasureState } from "./erasure.cjs";
|
|
3
4
|
import { SqlClientLike, uuidv7 } from "./sql.cjs";
|
|
4
5
|
import { DEFAULT_SCHEMA } from "./tables.cjs";
|
|
5
|
-
import { BundleVersion, DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, SegmentTriggerRoute, TriggerRoute } from "./index.cjs";
|
|
6
|
+
import { BundleVersion, DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, RunningInstance, SegmentTriggerRoute, TriggerRoute } from "./index.cjs";
|
|
6
7
|
//#region src/store/postgres.d.ts
|
|
7
8
|
export interface PostgresJourneyStoreOptions {
|
|
8
9
|
/**
|
|
@@ -19,6 +20,23 @@ export interface PostgresJourneyStoreOptions {
|
|
|
19
20
|
* guarantees come from the database: ON CONFLICT for the once policy and
|
|
20
21
|
* idempotent subscriptions, a transaction for the route swap, a jsonb merge
|
|
21
22
|
* for /identify. The tables are declared in tables.ts.
|
|
23
|
+
*
|
|
24
|
+
* Every write about a user is conditional on their erasure tombstone not existing
|
|
25
|
+
* (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that
|
|
26
|
+
* holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a
|
|
27
|
+
* profile merge, a device, a subscription, a segment membership with its starts, an entry, an
|
|
28
|
+
* admission, and the reporting's instance and message rows (postgres-reporting.ts). The
|
|
29
|
+
* erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ
|
|
30
|
+
* COMMITTED a conditional write sees only what was committed when its statement began, and an
|
|
31
|
+
* erasure sees only rows committed when it looked — so without the lock a write's transaction
|
|
32
|
+
* could commit its rows after the erase deleted everything (the user comes back), and a runner
|
|
33
|
+
* could admit itself after the tombstone (the instance runs on). With it, every write and the
|
|
34
|
+
* erasure of one user run one after another: a write either commits before the tombstone,
|
|
35
|
+
* where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is
|
|
36
|
+
* refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The
|
|
37
|
+
* conditional statement stays the in-transaction check; a write that wrote nothing reads the
|
|
38
|
+
* tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).
|
|
39
|
+
* Suppressions are the one exception: an opt-out is written whatever the user's state.
|
|
22
40
|
*/
|
|
23
41
|
export declare class PostgresJourneyStore implements JourneyStore {
|
|
24
42
|
private readonly sql;
|
|
@@ -29,25 +47,44 @@ export declare class PostgresJourneyStore implements JourneyStore {
|
|
|
29
47
|
constructor(sql: SqlClientLike, options?: PostgresJourneyStoreOptions);
|
|
30
48
|
/**
|
|
31
49
|
* What the live database lacks against tables.ts: missing tables, missing columns, a column
|
|
32
|
-
* of another type or nullability
|
|
33
|
-
*
|
|
34
|
-
*
|
|
50
|
+
* of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets
|
|
51
|
+
* — without one the first ingest fails, not the deploy). Read-only — the engine never migrates
|
|
52
|
+
* the host's database; a deploy fails with these problems listed, and the fix is to regenerate
|
|
53
|
+
* the drizzle file and run the host's migrations.
|
|
35
54
|
*/
|
|
36
55
|
checkSchema(): Promise<string[]>;
|
|
37
56
|
insertEvent(input: EventInput): Promise<boolean>;
|
|
57
|
+
/** The condition every write about a user carries: no erasure tombstone for `$user`. */
|
|
58
|
+
private notErased;
|
|
59
|
+
/**
|
|
60
|
+
* A conditional write wrote nothing: on that path only, read why. Throws when the tombstone
|
|
61
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
62
|
+
*/
|
|
63
|
+
private refuseIfErased;
|
|
64
|
+
/**
|
|
65
|
+
* The first two statements of every transaction that writes about the user (user-lock.ts):
|
|
66
|
+
* READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read
|
|
67
|
+
* about the user comes after, on a snapshot taken once the lock is held.
|
|
68
|
+
*/
|
|
69
|
+
private lockUser;
|
|
38
70
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
39
71
|
private eventWindow;
|
|
40
72
|
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
41
73
|
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
42
74
|
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
43
75
|
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
44
|
-
listUserIds(
|
|
76
|
+
listUserIds(page?: {
|
|
77
|
+
after?: string | undefined;
|
|
78
|
+
limit?: number | undefined;
|
|
79
|
+
}): Promise<string[]>;
|
|
45
80
|
listDevices(userId: string): Promise<PushDevice[]>;
|
|
46
81
|
upsertDevice(device: DeviceInput): Promise<void>;
|
|
47
82
|
requestErasure(userId: string, at: number): Promise<void>;
|
|
48
83
|
pendingErasures(): Promise<string[]>;
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
erasureState(userId: string): Promise<ErasureState>;
|
|
85
|
+
runningInstances(userId: string): Promise<RunningInstance[]>;
|
|
86
|
+
admitInstance(instanceId: string, userId: string): Promise<boolean>;
|
|
87
|
+
forgetUser(userId: string, at: number): Promise<void>;
|
|
51
88
|
isSuppressed(userId: string, channel: string): Promise<boolean>;
|
|
52
89
|
removeDevice(token: string): Promise<string | undefined>;
|
|
53
90
|
unbindDevice(token: string): Promise<string | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.cts","names":[],"sources":["../../src/store/postgres.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres.d.cts","names":[],"sources":["../../src/store/postgres.ts"],"mappings":";;;;;;;iBA4BiB;;;;;;;EAOf;;;;;;;;;;;;;;;;;;;;;;;;;;qBA4CW,gCAAgC;mBAOxB;mBANF;mBACA;WACR,WAAW;WACX,YAAY;EAErB,YACmB,KAAK,eACtB,UAAS;;;;;;;;EAkBL,eAAe;EAqDf,YAAY,OAAO,aAAa;;UA4B9B;;;;;UAQM;;;;;;UAYN;;UAKA;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EASpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAqB3D,YAAY;IAChB;IACA;MACE;EAoBJ,YAAY,iBAAiB,QAAQ;EAO/B,aAAa,QAAQ,cAAc;EAcnC,eAAe,gBAAgB,aAAa;EAa5C,mBAAmB;EAOnB,aAAa,iBAAiB,QAAQ;EAUtC,iBAAiB,iBAAiB,QAAQ;EAS1C,cAAc,oBAAoB,iBAAiB;EAmBnD,WAAW,gBAAgB,aAAa;EAoCxC,aAAa,gBAAgB,kBAAkB;EAQ/C,aAAa,gBAAgB;EAQ7B,aAAa,gBAAgB;EAW7B,gBAAgB;IAAU;IAAY,WAAW;;EAUjD,oBAAoB,aAAa,QAAQ;EASzC,aAAa,gBAAgB,QAAQ;EAoBrC,uBAAuB,QAAQ;EAe/B,cAAc,UAAU,aAAa;EAQrC,cAAc,sBAAsB,QAAQ;EASlD,cAAc,QAAQ,WAAW;EAI3B,qBAAqB,QAAQ;EAcnC,qBAAqB,QAAQ,UAAU,SAAS,gBAAgB;;UAoBlD;EAgCR,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAuBG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAatD,aAAa,OAAO,aAAa,QAAQ;UAejC;;;;;;UAqCA;EA6BR,YAAY,qBAAqB;EAIjC,eAAe,oBAAoB,iBAAiB;EAOpD,gBAAgB,gBAAgB,gBAAgB;EAQhD,UACJ,gBACA,gBACA,2BACA,aACC;EAmBG,YAAY,iBAAiB"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BundleIR, ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
2
|
import { PushDevice } from "../engine/ports.mjs";
|
|
3
|
+
import { ErasureState } from "./erasure.mjs";
|
|
3
4
|
import { SqlClientLike, uuidv7 } from "./sql.mjs";
|
|
4
5
|
import { DEFAULT_SCHEMA } from "./tables.mjs";
|
|
5
|
-
import { BundleVersion, DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, SegmentTriggerRoute, TriggerRoute } from "./index.mjs";
|
|
6
|
+
import { BundleVersion, DeviceInput, EntryInput, EntryOutcome, EventInput, EventWindow, ExecutionRegistry, JourneyReporting, JourneyStore, ProfileProps, RunningInstance, SegmentTriggerRoute, TriggerRoute } from "./index.mjs";
|
|
6
7
|
//#region src/store/postgres.d.ts
|
|
7
8
|
export interface PostgresJourneyStoreOptions {
|
|
8
9
|
/**
|
|
@@ -19,6 +20,23 @@ export interface PostgresJourneyStoreOptions {
|
|
|
19
20
|
* guarantees come from the database: ON CONFLICT for the once policy and
|
|
20
21
|
* idempotent subscriptions, a transaction for the route swap, a jsonb merge
|
|
21
22
|
* for /identify. The tables are declared in tables.ts.
|
|
23
|
+
*
|
|
24
|
+
* Every write about a user is conditional on their erasure tombstone not existing
|
|
25
|
+
* (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that
|
|
26
|
+
* holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a
|
|
27
|
+
* profile merge, a device, a subscription, a segment membership with its starts, an entry, an
|
|
28
|
+
* admission, and the reporting's instance and message rows (postgres-reporting.ts). The
|
|
29
|
+
* erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ
|
|
30
|
+
* COMMITTED a conditional write sees only what was committed when its statement began, and an
|
|
31
|
+
* erasure sees only rows committed when it looked — so without the lock a write's transaction
|
|
32
|
+
* could commit its rows after the erase deleted everything (the user comes back), and a runner
|
|
33
|
+
* could admit itself after the tombstone (the instance runs on). With it, every write and the
|
|
34
|
+
* erasure of one user run one after another: a write either commits before the tombstone,
|
|
35
|
+
* where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is
|
|
36
|
+
* refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The
|
|
37
|
+
* conditional statement stays the in-transaction check; a write that wrote nothing reads the
|
|
38
|
+
* tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).
|
|
39
|
+
* Suppressions are the one exception: an opt-out is written whatever the user's state.
|
|
22
40
|
*/
|
|
23
41
|
export declare class PostgresJourneyStore implements JourneyStore {
|
|
24
42
|
private readonly sql;
|
|
@@ -29,25 +47,44 @@ export declare class PostgresJourneyStore implements JourneyStore {
|
|
|
29
47
|
constructor(sql: SqlClientLike, options?: PostgresJourneyStoreOptions);
|
|
30
48
|
/**
|
|
31
49
|
* What the live database lacks against tables.ts: missing tables, missing columns, a column
|
|
32
|
-
* of another type or nullability
|
|
33
|
-
*
|
|
34
|
-
*
|
|
50
|
+
* of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets
|
|
51
|
+
* — without one the first ingest fails, not the deploy). Read-only — the engine never migrates
|
|
52
|
+
* the host's database; a deploy fails with these problems listed, and the fix is to regenerate
|
|
53
|
+
* the drizzle file and run the host's migrations.
|
|
35
54
|
*/
|
|
36
55
|
checkSchema(): Promise<string[]>;
|
|
37
56
|
insertEvent(input: EventInput): Promise<boolean>;
|
|
57
|
+
/** The condition every write about a user carries: no erasure tombstone for `$user`. */
|
|
58
|
+
private notErased;
|
|
59
|
+
/**
|
|
60
|
+
* A conditional write wrote nothing: on that path only, read why. Throws when the tombstone
|
|
61
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
62
|
+
*/
|
|
63
|
+
private refuseIfErased;
|
|
64
|
+
/**
|
|
65
|
+
* The first two statements of every transaction that writes about the user (user-lock.ts):
|
|
66
|
+
* READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read
|
|
67
|
+
* about the user comes after, on a snapshot taken once the lock is held.
|
|
68
|
+
*/
|
|
69
|
+
private lockUser;
|
|
38
70
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
39
71
|
private eventWindow;
|
|
40
72
|
countEvents(userId: string, event: string, window?: EventWindow): Promise<number>;
|
|
41
73
|
listEventPayloads(userId: string, event: string, window?: EventWindow): Promise<Record<string, unknown>[]>;
|
|
42
74
|
getProfile(userId: string): Promise<ProfileProps | undefined>;
|
|
43
75
|
mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps>;
|
|
44
|
-
listUserIds(
|
|
76
|
+
listUserIds(page?: {
|
|
77
|
+
after?: string | undefined;
|
|
78
|
+
limit?: number | undefined;
|
|
79
|
+
}): Promise<string[]>;
|
|
45
80
|
listDevices(userId: string): Promise<PushDevice[]>;
|
|
46
81
|
upsertDevice(device: DeviceInput): Promise<void>;
|
|
47
82
|
requestErasure(userId: string, at: number): Promise<void>;
|
|
48
83
|
pendingErasures(): Promise<string[]>;
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
erasureState(userId: string): Promise<ErasureState>;
|
|
85
|
+
runningInstances(userId: string): Promise<RunningInstance[]>;
|
|
86
|
+
admitInstance(instanceId: string, userId: string): Promise<boolean>;
|
|
87
|
+
forgetUser(userId: string, at: number): Promise<void>;
|
|
51
88
|
isSuppressed(userId: string, channel: string): Promise<boolean>;
|
|
52
89
|
removeDevice(token: string): Promise<string | undefined>;
|
|
53
90
|
unbindDevice(token: string): Promise<string | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.d.mts","names":[],"sources":["../../src/store/postgres.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"postgres.d.mts","names":[],"sources":["../../src/store/postgres.ts"],"mappings":";;;;;;;iBA4BiB;;;;;;;EAOf;;;;;;;;;;;;;;;;;;;;;;;;;;qBA4CW,gCAAgC;mBAOxB;mBANF;mBACA;WACR,WAAW;WACX,YAAY;EAErB,YACmB,KAAK,eACtB,UAAS;;;;;;;;EAkBL,eAAe;EAqDf,YAAY,OAAO,aAAa;;UA4B9B;;;;;UAQM;;;;;;UAYN;;UAKA;EAcF,YAAY,gBAAgB,eAAe,SAAS,cAAc;EASlE,kBACJ,gBACA,eACA,SAAS,cACR,QAAQ;EASL,WAAW,iBAAiB,QAAQ;EASpC,aAAa,gBAAgB,OAAO,eAAe,QAAQ;EAqB3D,YAAY;IAChB;IACA;MACE;EAoBJ,YAAY,iBAAiB,QAAQ;EAO/B,aAAa,QAAQ,cAAc;EAcnC,eAAe,gBAAgB,aAAa;EAa5C,mBAAmB;EAOnB,aAAa,iBAAiB,QAAQ;EAUtC,iBAAiB,iBAAiB,QAAQ;EAS1C,cAAc,oBAAoB,iBAAiB;EAmBnD,WAAW,gBAAgB,aAAa;EAoCxC,aAAa,gBAAgB,kBAAkB;EAQ/C,aAAa,gBAAgB;EAQ7B,aAAa,gBAAgB;EAW7B,gBAAgB;IAAU;IAAY,WAAW;;EAUjD,oBAAoB,aAAa,QAAQ;EASzC,aAAa,gBAAgB,QAAQ;EAoBrC,uBAAuB,QAAQ;EAe/B,cAAc,UAAU,aAAa;EAQrC,cAAc,sBAAsB,QAAQ;EASlD,cAAc,QAAQ,WAAW;EAI3B,qBAAqB,QAAQ;EAcnC,qBAAqB,QAAQ,UAAU,SAAS,gBAAgB;;UAoBlD;EAgCR,gBAAgB,iBAAiB,iBAAiB;EAQlD,iBACJ,iBACA,gBACA,YACA,+BACC;EAuBG,qBAAqB,iBAAiB,iBAAiB;EAQvD,qBAAqB,iBAAiB,gBAAgB,mBAAmB;EAOzE,oBAAoB,iBAAiB,iBAAiB;EAatD,aAAa,OAAO,aAAa,QAAQ;UAejC;;;;;;UAqCA;EA6BR,YAAY,qBAAqB;EAIjC,eAAe,oBAAoB,iBAAiB;EAOpD,gBAAgB,gBAAgB,gBAAgB;EAQhD,UACJ,gBACA,gBACA,2BACA,aACC;EAmBG,YAAY,iBAAiB"}
|
package/dist/store/postgres.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
2
|
+
import { UserErasedError } from "./erasure.mjs";
|
|
2
3
|
import { uuidv7 } from "./sql.mjs";
|
|
4
|
+
import { lockUserTransaction } from "./user-lock.mjs";
|
|
3
5
|
import { PostgresReporting } from "./postgres-reporting.mjs";
|
|
4
6
|
import { DEFAULT_SCHEMA, TABLES, columnName } from "./tables.mjs";
|
|
5
7
|
//#region src/store/postgres.ts
|
|
@@ -23,6 +25,23 @@ function asJson(value) {
|
|
|
23
25
|
* guarantees come from the database: ON CONFLICT for the once policy and
|
|
24
26
|
* idempotent subscriptions, a transaction for the route swap, a jsonb merge
|
|
25
27
|
* for /identify. The tables are declared in tables.ts.
|
|
28
|
+
*
|
|
29
|
+
* Every write about a user is conditional on their erasure tombstone not existing
|
|
30
|
+
* (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that
|
|
31
|
+
* holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a
|
|
32
|
+
* profile merge, a device, a subscription, a segment membership with its starts, an entry, an
|
|
33
|
+
* admission, and the reporting's instance and message rows (postgres-reporting.ts). The
|
|
34
|
+
* erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ
|
|
35
|
+
* COMMITTED a conditional write sees only what was committed when its statement began, and an
|
|
36
|
+
* erasure sees only rows committed when it looked — so without the lock a write's transaction
|
|
37
|
+
* could commit its rows after the erase deleted everything (the user comes back), and a runner
|
|
38
|
+
* could admit itself after the tombstone (the instance runs on). With it, every write and the
|
|
39
|
+
* erasure of one user run one after another: a write either commits before the tombstone,
|
|
40
|
+
* where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is
|
|
41
|
+
* refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The
|
|
42
|
+
* conditional statement stays the in-transaction check; a write that wrote nothing reads the
|
|
43
|
+
* tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).
|
|
44
|
+
* Suppressions are the one exception: an opt-out is written whatever the user's state.
|
|
26
45
|
*/
|
|
27
46
|
var PostgresJourneyStore = class {
|
|
28
47
|
sql;
|
|
@@ -35,19 +54,24 @@ var PostgresJourneyStore = class {
|
|
|
35
54
|
this.schema = options.schema ?? "workflow";
|
|
36
55
|
const qualify = (name) => `"${this.schema}"."${name}"`;
|
|
37
56
|
this.t = Object.fromEntries(Object.keys(TABLES).map((key) => [key, qualify(TABLES[key].name)]));
|
|
38
|
-
this.reporting = new PostgresReporting(sql, this.t);
|
|
57
|
+
this.reporting = new PostgresReporting(sql, this.t, this.schema);
|
|
39
58
|
this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);
|
|
40
59
|
}
|
|
41
60
|
/**
|
|
42
61
|
* What the live database lacks against tables.ts: missing tables, missing columns, a column
|
|
43
|
-
* of another type or nullability
|
|
44
|
-
*
|
|
45
|
-
*
|
|
62
|
+
* of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets
|
|
63
|
+
* — without one the first ingest fails, not the deploy). Read-only — the engine never migrates
|
|
64
|
+
* the host's database; a deploy fails with these problems listed, and the fix is to regenerate
|
|
65
|
+
* the drizzle file and run the host's migrations.
|
|
46
66
|
*/
|
|
47
67
|
async checkSchema() {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
const { columns, indexes } = await this.sql.transaction(async (tx) => ({
|
|
69
|
+
columns: await tx.query(`SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`, [this.schema]),
|
|
70
|
+
indexes: await tx.query(`SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`, [this.schema])
|
|
71
|
+
}));
|
|
72
|
+
const live = new Map(columns.map((row) => [`${row.table_name}.${row.column_name}`, row]));
|
|
73
|
+
const tables = new Set(columns.map((row) => row.table_name));
|
|
74
|
+
const liveIndexes = new Map(indexes.map((row) => [`${row.tablename}.${row.indexname}`, row]));
|
|
51
75
|
const problems = [];
|
|
52
76
|
for (const spec of Object.values(TABLES)) {
|
|
53
77
|
const where = `"${this.schema}"."${spec.name}"`;
|
|
@@ -62,20 +86,50 @@ var PostgresJourneyStore = class {
|
|
|
62
86
|
else if (row.udt_name !== UDT[column.type]) problems.push(`column ${where}."${name}" is ${row.udt_name}, expected ${UDT[column.type]}`);
|
|
63
87
|
else if (row.is_nullable === "YES" !== (column.nullable === true)) problems.push(`column ${where}."${name}" should be ${column.nullable ? "nullable" : "NOT NULL"}`);
|
|
64
88
|
}
|
|
89
|
+
for (const keys of spec.uniqueIndexes ?? []) {
|
|
90
|
+
const name = `${spec.name}_${keys.map(columnName).join("_")}_index`;
|
|
91
|
+
const row = liveIndexes.get(`${spec.name}.${name}`);
|
|
92
|
+
if (row === void 0) problems.push(`missing unique index "${this.schema}"."${name}"`);
|
|
93
|
+
else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef)) problems.push(`index "${this.schema}"."${name}" is not unique`);
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
return problems;
|
|
67
97
|
}
|
|
68
98
|
async insertEvent(input) {
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
return this.sql.transaction(async (tx) => {
|
|
100
|
+
await this.lockUser(tx, input.userId);
|
|
101
|
+
if ((await tx.query(`INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)
|
|
102
|
+
SELECT $1::uuid, $2, $3, $4, ${TS("$5")}, $6::text::jsonb ${this.notErased("$3")}
|
|
103
|
+
ON CONFLICT (user_id, event_id) DO NOTHING RETURNING id`, [
|
|
104
|
+
uuidv7(input.ts),
|
|
105
|
+
input.id,
|
|
106
|
+
input.userId,
|
|
107
|
+
input.event,
|
|
108
|
+
input.ts,
|
|
109
|
+
JSON.stringify(input.payload)
|
|
110
|
+
])).length > 0) return true;
|
|
111
|
+
await this.refuseIfErased(tx, input.userId);
|
|
112
|
+
return false;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** The condition every write about a user carries: no erasure tombstone for `$user`. */
|
|
116
|
+
notErased(user) {
|
|
117
|
+
return `WHERE NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = ${user})`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A conditional write wrote nothing: on that path only, read why. Throws when the tombstone
|
|
121
|
+
* refused it; returns when the statement had another reason (a duplicate, an entry taken).
|
|
122
|
+
*/
|
|
123
|
+
async refuseIfErased(sql, userId) {
|
|
124
|
+
if ((await sql.query(`SELECT 1 AS x FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).length > 0) throw new UserErasedError(userId);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The first two statements of every transaction that writes about the user (user-lock.ts):
|
|
128
|
+
* READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read
|
|
129
|
+
* about the user comes after, on a snapshot taken once the lock is held.
|
|
130
|
+
*/
|
|
131
|
+
lockUser(tx, userId) {
|
|
132
|
+
return lockUserTransaction(tx, this.schema, userId);
|
|
79
133
|
}
|
|
80
134
|
/** The event rows of one user and name, within the window's bounds (both inclusive). */
|
|
81
135
|
eventWindow(userId, event, window) {
|
|
@@ -108,41 +162,90 @@ var PostgresJourneyStore = class {
|
|
|
108
162
|
return row === void 0 ? void 0 : asJson(row.props);
|
|
109
163
|
}
|
|
110
164
|
async mergeProfile(userId, props) {
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
165
|
+
return this.sql.transaction(async (tx) => {
|
|
166
|
+
await this.lockUser(tx, userId);
|
|
167
|
+
const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props)
|
|
168
|
+
SELECT $1, jsonb_strip_nulls($2::text::jsonb) ${this.notErased("$1")}
|
|
169
|
+
ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)
|
|
170
|
+
RETURNING props`, [
|
|
171
|
+
userId,
|
|
172
|
+
JSON.stringify(props),
|
|
173
|
+
JSON.stringify(props)
|
|
174
|
+
]);
|
|
175
|
+
if (rows.length === 0) throw new UserErasedError(userId);
|
|
176
|
+
return asJson(rows[0].props);
|
|
177
|
+
});
|
|
118
178
|
}
|
|
119
|
-
async listUserIds() {
|
|
120
|
-
|
|
179
|
+
async listUserIds(page) {
|
|
180
|
+
const params = [];
|
|
181
|
+
let where = "";
|
|
182
|
+
if (page?.after !== void 0) {
|
|
183
|
+
params.push(page.after);
|
|
184
|
+
where = ` WHERE user_id > $${params.length}`;
|
|
185
|
+
}
|
|
186
|
+
let limit = "";
|
|
187
|
+
if (page?.limit !== void 0) {
|
|
188
|
+
params.push(page.limit);
|
|
189
|
+
limit = ` LIMIT $${params.length}`;
|
|
190
|
+
}
|
|
191
|
+
return (await this.sql.query(`SELECT user_id FROM (SELECT user_id FROM ${this.t.profile} UNION SELECT user_id FROM ${this.t.event}) u${where} ORDER BY user_id${limit}`, params)).map((row) => row.user_id);
|
|
121
192
|
}
|
|
122
193
|
listDevices(userId) {
|
|
123
194
|
return this.sql.query(`SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`, [userId]);
|
|
124
195
|
}
|
|
125
196
|
async upsertDevice(device) {
|
|
126
|
-
await this.sql.
|
|
127
|
-
|
|
128
|
-
device
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
197
|
+
await this.sql.transaction(async (tx) => {
|
|
198
|
+
await this.lockUser(tx, device.userId);
|
|
199
|
+
if ((await tx.query(`INSERT INTO ${this.t.device} (token, user_id, platform, ts)
|
|
200
|
+
SELECT $1, $2, $3, ${TS("$4")} ${this.notErased("$2")}
|
|
201
|
+
ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts
|
|
202
|
+
RETURNING token`, [
|
|
203
|
+
device.token,
|
|
204
|
+
device.userId,
|
|
205
|
+
device.platform,
|
|
206
|
+
device.ts
|
|
207
|
+
])).length === 0) throw new UserErasedError(device.userId);
|
|
208
|
+
});
|
|
133
209
|
}
|
|
134
210
|
async requestErasure(userId, at) {
|
|
135
|
-
await this.sql.
|
|
136
|
-
|
|
211
|
+
await this.sql.transaction(async (tx) => {
|
|
212
|
+
await this.lockUser(tx, userId);
|
|
213
|
+
await tx.query(`INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS("$2")})
|
|
214
|
+
ON CONFLICT DO NOTHING`, [userId, at]);
|
|
215
|
+
});
|
|
137
216
|
}
|
|
138
217
|
async pendingErasures() {
|
|
139
|
-
return (await this.sql.query(`SELECT user_id FROM ${this.t.erasure} ORDER BY requested_at`)).map((row) => row.user_id);
|
|
218
|
+
return (await this.sql.query(`SELECT user_id FROM ${this.t.erasure} WHERE erased_at IS NULL ORDER BY requested_at`)).map((row) => row.user_id);
|
|
219
|
+
}
|
|
220
|
+
async erasureState(userId) {
|
|
221
|
+
const row = (await this.sql.query(`SELECT erased_at IS NOT NULL AS erased FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).at(0);
|
|
222
|
+
if (row === void 0) return "none";
|
|
223
|
+
return row.erased ? "erased" : "erasing";
|
|
140
224
|
}
|
|
141
225
|
async runningInstances(userId) {
|
|
142
|
-
return (await this.sql.query(`SELECT instance_id
|
|
226
|
+
return (await this.sql.query(`SELECT instance_id, admitted_at IS NOT NULL AS admitted
|
|
227
|
+
FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`, [userId])).map((row) => ({
|
|
228
|
+
instanceId: row.instance_id,
|
|
229
|
+
admitted: row.admitted
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
async admitInstance(instanceId, userId) {
|
|
233
|
+
return this.sql.transaction(async (tx) => {
|
|
234
|
+
await this.lockUser(tx, userId);
|
|
235
|
+
return (await tx.query(`UPDATE ${this.t.entry} SET admitted_at = COALESCE(admitted_at, ${TS("$3")})
|
|
236
|
+
WHERE instance_id = $1 AND user_id = $2
|
|
237
|
+
AND (status = 'running' OR admitted_at IS NOT NULL)
|
|
238
|
+
AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $2)
|
|
239
|
+
RETURNING instance_id`, [
|
|
240
|
+
instanceId,
|
|
241
|
+
userId,
|
|
242
|
+
Date.now()
|
|
243
|
+
])).length > 0;
|
|
244
|
+
});
|
|
143
245
|
}
|
|
144
|
-
async forgetUser(userId) {
|
|
246
|
+
async forgetUser(userId, at) {
|
|
145
247
|
await this.sql.transaction(async (tx) => {
|
|
248
|
+
await this.lockUser(tx, userId);
|
|
146
249
|
for (const child of [
|
|
147
250
|
this.t.messageOpen,
|
|
148
251
|
this.t.messageClick,
|
|
@@ -156,12 +259,11 @@ var PostgresJourneyStore = class {
|
|
|
156
259
|
this.t.subscription,
|
|
157
260
|
this.t.segmentStart,
|
|
158
261
|
this.t.segmentMember,
|
|
159
|
-
this.t.suppression,
|
|
160
262
|
this.t.device,
|
|
161
263
|
this.t.event,
|
|
162
|
-
this.t.profile
|
|
163
|
-
this.t.erasure
|
|
264
|
+
this.t.profile
|
|
164
265
|
]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);
|
|
266
|
+
await tx.query(`UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS("$2")}) WHERE user_id = $1`, [userId, at]);
|
|
165
267
|
});
|
|
166
268
|
}
|
|
167
269
|
async isSuppressed(userId, channel) {
|
|
@@ -267,13 +369,17 @@ var PostgresJourneyStore = class {
|
|
|
267
369
|
}
|
|
268
370
|
async addSegmentMember(segment, userId, ts, workflows) {
|
|
269
371
|
await this.sql.transaction(async (tx) => {
|
|
270
|
-
await
|
|
372
|
+
await this.lockUser(tx, userId);
|
|
373
|
+
if ((await tx.query(`INSERT INTO ${this.t.segmentMember} (segment, user_id, ts)
|
|
374
|
+
SELECT $1, $2, ${TS("$3")} ${this.notErased("$2")}
|
|
375
|
+
ON CONFLICT DO NOTHING RETURNING segment`, [
|
|
271
376
|
segment,
|
|
272
377
|
userId,
|
|
273
378
|
ts
|
|
274
|
-
]);
|
|
379
|
+
])).length === 0) await this.refuseIfErased(tx, userId);
|
|
275
380
|
for (const workflow of workflows) await tx.query(`INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)
|
|
276
|
-
|
|
381
|
+
SELECT $1, $2, $3, ${TS("$4")} ${this.notErased("$3")}
|
|
382
|
+
ON CONFLICT DO NOTHING`, [
|
|
277
383
|
segment,
|
|
278
384
|
workflow,
|
|
279
385
|
userId,
|
|
@@ -299,16 +405,16 @@ var PostgresJourneyStore = class {
|
|
|
299
405
|
}
|
|
300
406
|
async enterJourney(entry) {
|
|
301
407
|
const { execution } = entry;
|
|
302
|
-
if (execution === void 0) return this.claimEntry(this.sql, entry);
|
|
303
408
|
return this.sql.transaction(async (tx) => {
|
|
409
|
+
await this.lockUser(tx, entry.userId);
|
|
304
410
|
const outcome = await this.claimEntry(tx, entry);
|
|
305
|
-
if (outcome !== null) await this.recordStart(tx, entry, execution);
|
|
411
|
+
if (outcome !== null && execution !== void 0) await this.recordStart(tx, entry, execution);
|
|
306
412
|
return outcome;
|
|
307
413
|
});
|
|
308
414
|
}
|
|
309
415
|
async claimEntry(sql, entry) {
|
|
310
416
|
if ((await sql.query(`INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts)
|
|
311
|
-
|
|
417
|
+
SELECT $1, $2, $3, $4, 'running', ${TS("$5")} ${this.notErased("$2")}
|
|
312
418
|
ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`, [
|
|
313
419
|
entry.workflow,
|
|
314
420
|
entry.userId,
|
|
@@ -316,14 +422,19 @@ var PostgresJourneyStore = class {
|
|
|
316
422
|
entry.hash,
|
|
317
423
|
entry.ts
|
|
318
424
|
])).length > 0) return "entered";
|
|
319
|
-
|
|
320
|
-
|
|
425
|
+
if ((await sql.query(`UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS("$3")},
|
|
426
|
+
admitted_at = NULL
|
|
427
|
+
WHERE workflow = $4 AND user_id = $5 AND status = 'failed'
|
|
428
|
+
AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)
|
|
429
|
+
RETURNING instance_id`, [
|
|
321
430
|
entry.instanceId,
|
|
322
431
|
entry.hash,
|
|
323
432
|
entry.ts,
|
|
324
433
|
entry.workflow,
|
|
325
434
|
entry.userId
|
|
326
|
-
])).length > 0
|
|
435
|
+
])).length > 0) return "reclaimed";
|
|
436
|
+
await this.refuseIfErased(sql, entry.userId);
|
|
437
|
+
return null;
|
|
327
438
|
}
|
|
328
439
|
/**
|
|
329
440
|
* Only an entry this call claimed records a start, so a row already there is a previous
|
|
@@ -331,13 +442,14 @@ var PostgresJourneyStore = class {
|
|
|
331
442
|
* ts). It is replaced whole: the new attempt's name and payload, nothing submitted yet.
|
|
332
443
|
*/
|
|
333
444
|
async recordStart(tx, entry, execution) {
|
|
334
|
-
await tx.query(`INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)
|
|
335
|
-
|
|
445
|
+
if ((await tx.query(`INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)
|
|
446
|
+
SELECT $1, $2, $3, $4, $5, $6, ${TS("$7")}, 0 ${this.notErased("$3")}
|
|
336
447
|
ON CONFLICT (instance_id) DO UPDATE SET
|
|
337
448
|
name = EXCLUDED.name, target = EXCLUDED.target, input = EXCLUDED.input,
|
|
338
449
|
created_at = EXCLUDED.created_at, generation = 0, submitted_at = NULL, arn = NULL,
|
|
339
450
|
unresolved_at = NULL, callback_id = NULL, deadline_at = NULL, checked_at = NULL,
|
|
340
|
-
stopped_at = NULL
|
|
451
|
+
stopped_at = NULL
|
|
452
|
+
RETURNING instance_id`, [
|
|
341
453
|
entry.instanceId,
|
|
342
454
|
entry.workflow,
|
|
343
455
|
entry.userId,
|
|
@@ -345,7 +457,7 @@ var PostgresJourneyStore = class {
|
|
|
345
457
|
execution.target,
|
|
346
458
|
execution.input,
|
|
347
459
|
Date.now()
|
|
348
|
-
]);
|
|
460
|
+
])).length === 0) throw new UserErasedError(entry.userId);
|
|
349
461
|
}
|
|
350
462
|
async removeEntry(instanceId) {
|
|
351
463
|
await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);
|
|
@@ -357,12 +469,19 @@ var PostgresJourneyStore = class {
|
|
|
357
469
|
return (await this.sql.query(`SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`, [userId, event])).map((row) => row.handle);
|
|
358
470
|
}
|
|
359
471
|
async subscribe(handle, userId, events, ts) {
|
|
360
|
-
|
|
361
|
-
userId
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
472
|
+
await this.sql.transaction(async (tx) => {
|
|
473
|
+
await this.lockUser(tx, userId);
|
|
474
|
+
let skipped = false;
|
|
475
|
+
for (const event of events) if ((await tx.query(`INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts)
|
|
476
|
+
SELECT $1, $2, $3, ${TS("$4")} ${this.notErased("$1")}
|
|
477
|
+
ON CONFLICT DO NOTHING RETURNING event`, [
|
|
478
|
+
userId,
|
|
479
|
+
event,
|
|
480
|
+
handle,
|
|
481
|
+
ts
|
|
482
|
+
])).length === 0) skipped = true;
|
|
483
|
+
if (skipped) await this.refuseIfErased(tx, userId);
|
|
484
|
+
});
|
|
366
485
|
}
|
|
367
486
|
async unsubscribe(handle) {
|
|
368
487
|
await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);
|