@shware/workflow 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +1 -1
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +1 -1
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +2 -2
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +2 -2
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +1 -1
- package/dist/dsl/bundle.d.mts +1 -1
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +1 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +1 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/refs.cjs.map +1 -1
- package/dist/dsl/refs.d.cts +5 -4
- package/dist/dsl/refs.d.cts.map +1 -1
- package/dist/dsl/refs.d.mts +5 -4
- package/dist/dsl/refs.d.mts.map +1 -1
- package/dist/dsl/refs.mjs.map +1 -1
- package/dist/dsl/template.cjs.map +1 -1
- package/dist/dsl/template.d.cts +2 -2
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +2 -2
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- 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 +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +5 -1
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +5 -1
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +5 -5
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.mjs +5 -5
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
package/dist/store/tables.cjs
CHANGED
|
@@ -170,6 +170,32 @@ const TABLES = {
|
|
|
170
170
|
primaryKey: ["wakeHandle", "event"],
|
|
171
171
|
indexes: [["userId", "event"]]
|
|
172
172
|
}),
|
|
173
|
+
execution: table({
|
|
174
|
+
name: "execution",
|
|
175
|
+
doc: "Durable executions whose start is an asynchronous submit (AWS Lambda durable functions): written with the entry, so a lost submit response is retried with the same name, target and payload instead of starting a second execution; and the wake callback the instance is parked on now. Unused on Cloudflare.",
|
|
176
|
+
columns: {
|
|
177
|
+
instanceId: text,
|
|
178
|
+
workflow: text,
|
|
179
|
+
userId: text,
|
|
180
|
+
name: text,
|
|
181
|
+
target: text,
|
|
182
|
+
input: text,
|
|
183
|
+
createdAt: ts,
|
|
184
|
+
submittedAt: nullable(ts, "The runtime accepted the start; null while it may still have to be retried."),
|
|
185
|
+
arn: nullable(text),
|
|
186
|
+
unresolvedAt: nullable(ts, "Retries ran out before the runtime confirmed the start: whether it runs is unknown, and nothing retries it."),
|
|
187
|
+
callbackId: nullable(text, "The callback the current wait parks on; a wake completes it."),
|
|
188
|
+
generation: integer,
|
|
189
|
+
deadlineAt: nullable(ts),
|
|
190
|
+
checkedAt: nullable(ts, "When maintenance last compared the runtime's status with the ledger.")
|
|
191
|
+
},
|
|
192
|
+
primaryKey: ["instanceId"],
|
|
193
|
+
indexes: [
|
|
194
|
+
["workflow", "userId"],
|
|
195
|
+
["submittedAt"],
|
|
196
|
+
["userId"]
|
|
197
|
+
]
|
|
198
|
+
}),
|
|
173
199
|
deployment: table({
|
|
174
200
|
name: "deployment",
|
|
175
201
|
doc: "The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.cjs","names":[],"sources":["../../src/store/tables.ts"],"sourcesContent":["/**\n * The engine's Postgres tables, declared once. Everything that needs their shape derives it from\n * here: the drizzle schema file a host generates and migrates with its own tooling\n * (`renderDrizzleSchema`), the plain DDL (`renderPostgresDdl`), the store's table names, and the\n * check a deploy runs against the live database (`checkPostgresSchema`).\n *\n * The house rules hold for every table: no database defaults (the store writes every value,\n * timestamps included), timestamptz for time, and a UUID v7 row id — generated by the store and\n * guarded by a CHECK — wherever a table has a surrogate key. All tables live in one Postgres\n * schema of their own (`workflow` unless the config names another), so their short names never\n * meet the app's.\n */\n\nexport type ColumnType = 'uuid' | 'text' | 'jsonb' | 'timestamptz' | 'integer' | 'boolean';\n\nexport interface ColumnSpec {\n type: ColumnType;\n /** Columns are NOT NULL unless marked. */\n nullable?: boolean;\n /** One line for the generated file. */\n doc?: string;\n}\n\nexport interface TableSpec<K extends string = string> {\n /** Physical table name inside the schema. */\n name: string;\n doc: string;\n /** Keyed by the drizzle property name; the column name is its snake_case. */\n columns: Record<K, ColumnSpec>;\n primaryKey: readonly K[];\n /** A uuid column whose values must be version 7. */\n uuidV7?: K;\n uniqueIndexes?: readonly (readonly K[])[];\n indexes?: readonly (readonly K[])[];\n /** Foreign keys, to another table's logical key. */\n references?: readonly {\n columns: readonly K[];\n table: string;\n foreignColumns: readonly string[];\n }[];\n}\n\nfunction table<K extends string>(spec: TableSpec<K>): TableSpec<K> {\n return spec;\n}\n\nconst uuid = { type: 'uuid' } as const;\nconst text = { type: 'text' } as const;\nconst jsonb = { type: 'jsonb' } as const;\nconst ts = { type: 'timestamptz' } as const;\nconst integer = { type: 'integer' } as const;\nconst boolean = { type: 'boolean' } as const;\nconst nullable = <C extends ColumnSpec>(column: C, doc?: string): ColumnSpec => ({\n ...column,\n nullable: true,\n ...(doc === undefined ? {} : { doc }),\n});\n\nexport const TABLES = {\n event: table({\n name: 'event',\n doc: \"Every ingested event, the fact base for `performed(...)` conditions. `event_id` is the occurrence's identity as the sender chose it (a retrying producer sends the same one again); its unique index makes a second delivery a no-op.\",\n columns: { id: uuid, eventId: text, userId: text, name: text, ts, payload: jsonb },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['eventId']],\n indexes: [['userId', 'name', 'ts']],\n }),\n profile: table({\n name: 'profile',\n doc: 'Identified properties per user (`/identify` merges into `props`; a null value means unset).',\n columns: { userId: text, props: jsonb },\n primaryKey: ['userId'],\n }),\n device: table({\n name: 'device',\n doc: 'Push devices, registered through the Worker (`JourneyClient.registerDevice`). Signing out unbinds a device (`user_id` null) rather than removing it; a token APNs / FCM reports as gone is removed.',\n columns: { token: text, userId: nullable(text), platform: text, ts },\n primaryKey: ['token'],\n indexes: [['userId']],\n }),\n erasure: table({\n name: 'erasure',\n doc: \"Erasures in progress (`forgetUser`): the user's data stays until every running instance is stopped, and the scheduled sweep retries what is left.\",\n columns: { userId: text, requestedAt: ts },\n primaryKey: ['userId'],\n }),\n suppression: table({\n name: 'suppression',\n doc: 'Channels a user opted out of (an email unsubscribe): the engine sends nothing there, and `reachable(channel)` is false.',\n columns: { userId: text, channel: text, ts },\n primaryKey: ['userId', 'channel'],\n }),\n segment: table({\n name: 'segment',\n doc: 'Deployed segment definitions, resolved by name at evaluation time.',\n columns: { id: text, condition: jsonb, hash: text },\n primaryKey: ['id'],\n }),\n trigger: table({\n name: 'trigger',\n doc: 'Event-trigger routes: `where` gates on the event payload, `filter` on the profile.',\n columns: {\n workflow: text,\n hash: text,\n event: text,\n where: nullable(jsonb),\n filter: nullable(jsonb),\n },\n primaryKey: ['workflow'],\n indexes: [['event']],\n }),\n segmentTrigger: table({\n name: 'segment_trigger',\n doc: 'Segment-entry trigger routes.',\n columns: { workflow: text, hash: text, segment: text },\n primaryKey: ['workflow'],\n indexes: [['segment']],\n }),\n segmentMember: table({\n name: 'segment_member',\n doc: 'Who is in each deployed segment and since when: kept by the scheduled sweep, and for trigger-routed segments on every event too.',\n columns: { segment: text, userId: text, ts },\n primaryKey: ['segment', 'userId'],\n }),\n segmentStart: table({\n name: 'segment_start',\n doc: 'Workflows a segment entry still has to start for a member: written with the membership, removed as each one starts, so a failed start is retried by the next evaluation instead of being hidden behind the membership.',\n columns: { segment: text, workflow: text, userId: text, ts },\n primaryKey: ['segment', 'workflow', 'userId'],\n }),\n segmentSnapshot: table({\n name: 'segment_snapshot',\n doc: \"Each segment's size at every sweep, the Studio's sparklines.\",\n columns: { segment: text, takenAt: ts, size: integer },\n primaryKey: ['segment', 'takenAt'],\n }),\n entry: table({\n name: 'entry',\n doc: 'Entry ledger: the (workflow, user) primary key is the once policy.',\n columns: { workflow: text, userId: text, instanceId: text, hash: text, status: text, ts },\n primaryKey: ['workflow', 'userId'],\n indexes: [['instanceId']],\n }),\n subscription: table({\n name: 'subscription',\n doc: 'wait_until wake routing; the primary key makes repeated subscribes idempotent.',\n columns: { userId: text, event: text, wakeHandle: text, ts },\n primaryKey: ['wakeHandle', 'event'],\n indexes: [['userId', 'event']],\n }),\n deployment: table({\n name: 'deployment',\n doc: \"The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.\",\n columns: { scope: text, versionId: text, versionCreatedAt: ts, appliedAt: ts },\n primaryKey: ['scope'],\n }),\n ir: table({\n name: 'ir',\n doc: 'Workflow bodies (WorkflowIR), content-addressed.',\n columns: { contentHash: text, ir: jsonb },\n primaryKey: ['contentHash'],\n }),\n instance: table({\n name: 'instance',\n doc: 'One row per journey instance, written when it enters: the node it sits on or last passed (none when a guard ended it before the first), and how it ended (`running` until then; `goal` is a conversion, at `finished_at`).',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n enteredAt: ts,\n nodeId: nullable(text),\n nodeAt: nullable(ts),\n status: text,\n finishedAt: nullable(ts),\n convertedAt: nullable(\n ts,\n 'When the goal was met, whatever the status: with `exitOnMatch: false` a converted instance ends `completed`.'\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [\n ['workflow', 'status'],\n ['workflow', 'finishedAt'],\n ],\n }),\n message: table({\n name: 'message',\n doc: \"One row per message copy (a push fans out per device). The idempotency key is the engine's copy identity, so a retried step finds the row it wrote; `channel` is where the copy landed (`email`, `push_ios`, `push_android`).\",\n columns: {\n id: uuid,\n idempotencyKey: text,\n workflow: text,\n instanceId: text,\n nodeId: text,\n template: text,\n channel: text,\n userId: text,\n sentAt: ts,\n deliveredAt: nullable(ts, 'The transport accepted it.'),\n deviceDeliveredAt: nullable(\n ts,\n \"A push: the device reported it received (iOS's notification service extension, a browser's service worker).\"\n ),\n undeliveredReason: nullable(\n text,\n 'The transport settled it without delivering (a dead push token).'\n ),\n openedAt: nullable(ts, 'First open of any kind, by a person or a machine.'),\n humanOpenedAt: nullable(ts, 'First open a person made.'),\n clickedAt: nullable(ts),\n humanClickedAt: nullable(ts),\n unsubscribedAt: nullable(ts),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['idempotencyKey']],\n indexes: [['workflow', 'sentAt'], ['instanceId']],\n }),\n messageLink: table({\n name: 'message_link',\n doc: \"A sent email's links in document order: the tracked URL names a message and a position, and the redirect goes only to the URL recorded here.\",\n columns: { messageId: uuid, position: integer, url: text },\n primaryKey: ['messageId', 'position'],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageOpen: table({\n name: 'message_open',\n doc: \"Every open-pixel load or push tap, classified when recorded: `machine` when no person caused it, with the deciding rule in `reason`; the request's network (never its address) kept for revisiting the rules.\",\n columns: {\n id: uuid,\n messageId: uuid,\n openedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageClick: table({\n name: 'message_click',\n doc: 'Every click on a tracked link, classified like opens.',\n columns: {\n id: uuid,\n messageId: uuid,\n position: integer,\n url: text,\n clickedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n} satisfies Record<string, TableSpec>;\n\nexport type TableKey = keyof typeof TABLES;\n\n/** The engine's schema when the config names none. */\nexport const DEFAULT_SCHEMA = 'workflow';\n\n/** `eventId` → `event_id`. */\nexport function columnName(key: string): string {\n return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n\nconst quote = (name: string) => `\"${name.replace(/\"/g, '\"\"')}\"`;\n\nconst SQL_TYPES: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'integer',\n boolean: 'boolean',\n};\n\n/**\n * Plain Postgres DDL for every table — for a host without a migration tool, and the tests. A\n * host that migrates with drizzle generates `renderDrizzleSchema` instead and lets drizzle-kit\n * write the migration.\n */\nexport function renderPostgresDdl(schema = DEFAULT_SCHEMA): string {\n const s = quote(schema);\n const out = [`CREATE SCHEMA IF NOT EXISTS ${s};`];\n for (const spec of Object.values(TABLES)) {\n const lines = Object.entries(spec.columns).map(\n ([key, column]) =>\n ` ${quote(columnName(key))} ${SQL_TYPES[column.type]}${column.nullable ? '' : ' NOT NULL'}`\n );\n lines.push(` PRIMARY KEY (${spec.primaryKey.map((k) => quote(columnName(k))).join(', ')})`);\n if (spec.uuidV7 !== undefined) {\n lines.push(\n ` CONSTRAINT ${quote(`${spec.name}_uuid_v7`)} CHECK (substring(${quote(columnName(spec.uuidV7))}::text, 15, 1) = '7')`\n );\n }\n for (const ref of spec.references ?? []) {\n const target = TABLES[ref.table as TableKey];\n lines.push(\n ` FOREIGN KEY (${ref.columns.map((k) => quote(columnName(k))).join(', ')}) REFERENCES ${s}.${quote(target.name)} (${ref.foreignColumns.map((k) => quote(columnName(k))).join(', ')})`\n );\n }\n out.push(`CREATE TABLE IF NOT EXISTS ${s}.${quote(spec.name)} (\\n${lines.join(',\\n')}\\n);`);\n const index = (unique: boolean) => (keys: readonly string[]) => {\n const cols = keys.map(columnName);\n out.push(\n `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quote(`${spec.name}_${cols.join('_')}_index`)} ON ${s}.${quote(spec.name)} (${cols.map(quote).join(', ')});`\n );\n };\n (spec.uniqueIndexes ?? []).forEach(index(true));\n (spec.indexes ?? []).forEach(index(false));\n }\n return `${out.join('\\n')}\\n`;\n}\n\nconst DRIZZLE_TYPES: Record<ColumnType, (column: string) => string> = {\n uuid: (c) => `uuid('${c}')`,\n text: (c) => `text('${c}')`,\n jsonb: (c) => `jsonb('${c}')`,\n timestamptz: (c) => `timestamp('${c}', { mode: 'string', withTimezone: true })`,\n integer: (c) => `integer('${c}')`,\n boolean: (c) => `boolean('${c}')`,\n};\n\nconst exportName = (key: string) => `workflow${key.charAt(0).toUpperCase()}${key.slice(1)}`;\n\n/**\n * The tables as one drizzle file: `export const workflowSchema = pgSchema(<schema>)` and every\n * table as `workflowSchema.table(...)`. The host re-exports it from its drizzle schema and\n * generates the migration with drizzle-kit, so the migration history stays the host's. The\n * output is deterministic — regenerate and diff it in CI to catch an SDK upgrade that changed the\n * tables.\n */\nexport function renderDrizzleSchema(schema = DEFAULT_SCHEMA): string {\n const specs = Object.entries(TABLES) as [TableKey, TableSpec][];\n const used = new Set<string>(['pgSchema']);\n const body: string[] = [];\n let needsSql = false;\n for (const [key, spec] of specs) {\n const columns = Object.entries(spec.columns).map(([prop, column]) => {\n used.add(column.type === 'timestamptz' ? 'timestamp' : column.type);\n const doc = column.doc === undefined ? '' : ` /** ${column.doc} */\\n`;\n const key =\n spec.primaryKey.length === 1 && spec.primaryKey[0] === prop ? '.primaryKey()' : '';\n return `${doc} ${prop}: ${DRIZZLE_TYPES[column.type](columnName(prop))}${key}${column.nullable ? '' : '.notNull()'},`;\n });\n const extras: string[] = [];\n if (spec.primaryKey.length > 1) {\n used.add('primaryKey');\n extras.push(\n `primaryKey({ columns: [${spec.primaryKey.map((k) => `table.${k}`).join(', ')}] })`\n );\n }\n for (const keys of spec.uniqueIndexes ?? []) {\n used.add('uniqueIndex');\n extras.push(`uniqueIndex().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const keys of spec.indexes ?? []) {\n used.add('index');\n extras.push(`index().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const ref of spec.references ?? []) {\n used.add('foreignKey');\n extras.push(\n `foreignKey({ columns: [${ref.columns.map((k) => `table.${k}`).join(', ')}], foreignColumns: [${ref.foreignColumns.map((k) => `${exportName(ref.table)}.${k}`).join(', ')}] })`\n );\n }\n if (spec.uuidV7 !== undefined) {\n used.add('check');\n needsSql = true;\n extras.push(\n `check('${spec.name}_uuid_v7', sql\\`substring(\\${table.${spec.uuidV7}}::text, 15, 1) = '7'\\`)`\n );\n }\n const third =\n extras.length === 0\n ? ''\n : `,\\n (table) => [\\n${extras.map((e) => ` ${e},`).join('\\n')}\\n ]`;\n body.push(\n `/** ${spec.doc} */\\nexport const ${exportName(key)} = workflowSchema.table(\\n '${spec.name}',\\n {\\n${columns.join('\\n')}\\n }${third}\\n);`\n );\n }\n const imports = [...used].sort();\n return [\n '// Generated by `workflow-ui generate` from @shware/workflow — do not edit; regenerate after',\n '// upgrading the SDK and let drizzle-kit write the migration.',\n ...(needsSql ? [\"import { sql } from 'drizzle-orm';\"] : []),\n `import {\\n${imports.map((name) => ` ${name},`).join('\\n')}\\n} from 'drizzle-orm/pg-core';`,\n '',\n `export const workflowSchema = pgSchema('${schema}');`,\n '',\n body.join('\\n\\n'),\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tables.cjs","names":[],"sources":["../../src/store/tables.ts"],"sourcesContent":["/**\n * The engine's Postgres tables, declared once. Everything that needs their shape derives it from\n * here: the drizzle schema file a host generates and migrates with its own tooling\n * (`renderDrizzleSchema`), the plain DDL (`renderPostgresDdl`), the store's table names, and the\n * check a deploy runs against the live database (`checkPostgresSchema`).\n *\n * The house rules hold for every table: no database defaults (the store writes every value,\n * timestamps included), timestamptz for time, and a UUID v7 row id — generated by the store and\n * guarded by a CHECK — wherever a table has a surrogate key. All tables live in one Postgres\n * schema of their own (`workflow` unless the config names another), so their short names never\n * meet the app's.\n */\n\nexport type ColumnType = 'uuid' | 'text' | 'jsonb' | 'timestamptz' | 'integer' | 'boolean';\n\nexport interface ColumnSpec {\n type: ColumnType;\n /** Columns are NOT NULL unless marked. */\n nullable?: boolean;\n /** One line for the generated file. */\n doc?: string;\n}\n\nexport interface TableSpec<K extends string = string> {\n /** Physical table name inside the schema. */\n name: string;\n doc: string;\n /** Keyed by the drizzle property name; the column name is its snake_case. */\n columns: Record<K, ColumnSpec>;\n primaryKey: readonly K[];\n /** A uuid column whose values must be version 7. */\n uuidV7?: K;\n uniqueIndexes?: readonly (readonly K[])[];\n indexes?: readonly (readonly K[])[];\n /** Foreign keys, to another table's logical key. */\n references?: readonly {\n columns: readonly K[];\n table: string;\n foreignColumns: readonly string[];\n }[];\n}\n\nfunction table<K extends string>(spec: TableSpec<K>): TableSpec<K> {\n return spec;\n}\n\nconst uuid = { type: 'uuid' } as const;\nconst text = { type: 'text' } as const;\nconst jsonb = { type: 'jsonb' } as const;\nconst ts = { type: 'timestamptz' } as const;\nconst integer = { type: 'integer' } as const;\nconst boolean = { type: 'boolean' } as const;\nconst nullable = <C extends ColumnSpec>(column: C, doc?: string): ColumnSpec => ({\n ...column,\n nullable: true,\n ...(doc === undefined ? {} : { doc }),\n});\n\nexport const TABLES = {\n event: table({\n name: 'event',\n doc: \"Every ingested event, the fact base for `performed(...)` conditions. `event_id` is the occurrence's identity as the sender chose it (a retrying producer sends the same one again); its unique index makes a second delivery a no-op.\",\n columns: { id: uuid, eventId: text, userId: text, name: text, ts, payload: jsonb },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['eventId']],\n indexes: [['userId', 'name', 'ts']],\n }),\n profile: table({\n name: 'profile',\n doc: 'Identified properties per user (`/identify` merges into `props`; a null value means unset).',\n columns: { userId: text, props: jsonb },\n primaryKey: ['userId'],\n }),\n device: table({\n name: 'device',\n doc: 'Push devices, registered through the Worker (`JourneyClient.registerDevice`). Signing out unbinds a device (`user_id` null) rather than removing it; a token APNs / FCM reports as gone is removed.',\n columns: { token: text, userId: nullable(text), platform: text, ts },\n primaryKey: ['token'],\n indexes: [['userId']],\n }),\n erasure: table({\n name: 'erasure',\n doc: \"Erasures in progress (`forgetUser`): the user's data stays until every running instance is stopped, and the scheduled sweep retries what is left.\",\n columns: { userId: text, requestedAt: ts },\n primaryKey: ['userId'],\n }),\n suppression: table({\n name: 'suppression',\n doc: 'Channels a user opted out of (an email unsubscribe): the engine sends nothing there, and `reachable(channel)` is false.',\n columns: { userId: text, channel: text, ts },\n primaryKey: ['userId', 'channel'],\n }),\n segment: table({\n name: 'segment',\n doc: 'Deployed segment definitions, resolved by name at evaluation time.',\n columns: { id: text, condition: jsonb, hash: text },\n primaryKey: ['id'],\n }),\n trigger: table({\n name: 'trigger',\n doc: 'Event-trigger routes: `where` gates on the event payload, `filter` on the profile.',\n columns: {\n workflow: text,\n hash: text,\n event: text,\n where: nullable(jsonb),\n filter: nullable(jsonb),\n },\n primaryKey: ['workflow'],\n indexes: [['event']],\n }),\n segmentTrigger: table({\n name: 'segment_trigger',\n doc: 'Segment-entry trigger routes.',\n columns: { workflow: text, hash: text, segment: text },\n primaryKey: ['workflow'],\n indexes: [['segment']],\n }),\n segmentMember: table({\n name: 'segment_member',\n doc: 'Who is in each deployed segment and since when: kept by the scheduled sweep, and for trigger-routed segments on every event too.',\n columns: { segment: text, userId: text, ts },\n primaryKey: ['segment', 'userId'],\n }),\n segmentStart: table({\n name: 'segment_start',\n doc: 'Workflows a segment entry still has to start for a member: written with the membership, removed as each one starts, so a failed start is retried by the next evaluation instead of being hidden behind the membership.',\n columns: { segment: text, workflow: text, userId: text, ts },\n primaryKey: ['segment', 'workflow', 'userId'],\n }),\n segmentSnapshot: table({\n name: 'segment_snapshot',\n doc: \"Each segment's size at every sweep, the Studio's sparklines.\",\n columns: { segment: text, takenAt: ts, size: integer },\n primaryKey: ['segment', 'takenAt'],\n }),\n entry: table({\n name: 'entry',\n doc: 'Entry ledger: the (workflow, user) primary key is the once policy.',\n columns: { workflow: text, userId: text, instanceId: text, hash: text, status: text, ts },\n primaryKey: ['workflow', 'userId'],\n indexes: [['instanceId']],\n }),\n subscription: table({\n name: 'subscription',\n doc: 'wait_until wake routing; the primary key makes repeated subscribes idempotent.',\n columns: { userId: text, event: text, wakeHandle: text, ts },\n primaryKey: ['wakeHandle', 'event'],\n indexes: [['userId', 'event']],\n }),\n execution: table({\n name: 'execution',\n doc: 'Durable executions whose start is an asynchronous submit (AWS Lambda durable functions): written with the entry, so a lost submit response is retried with the same name, target and payload instead of starting a second execution; and the wake callback the instance is parked on now. Unused on Cloudflare.',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n name: text,\n target: text,\n input: text,\n createdAt: ts,\n submittedAt: nullable(\n ts,\n 'The runtime accepted the start; null while it may still have to be retried.'\n ),\n arn: nullable(text),\n unresolvedAt: nullable(\n ts,\n 'Retries ran out before the runtime confirmed the start: whether it runs is unknown, and nothing retries it.'\n ),\n callbackId: nullable(text, 'The callback the current wait parks on; a wake completes it.'),\n generation: integer,\n deadlineAt: nullable(ts),\n checkedAt: nullable(\n ts,\n \"When maintenance last compared the runtime's status with the ledger.\"\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [['workflow', 'userId'], ['submittedAt'], ['userId']],\n }),\n deployment: table({\n name: 'deployment',\n doc: \"The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.\",\n columns: { scope: text, versionId: text, versionCreatedAt: ts, appliedAt: ts },\n primaryKey: ['scope'],\n }),\n ir: table({\n name: 'ir',\n doc: 'Workflow bodies (WorkflowIR), content-addressed.',\n columns: { contentHash: text, ir: jsonb },\n primaryKey: ['contentHash'],\n }),\n instance: table({\n name: 'instance',\n doc: 'One row per journey instance, written when it enters: the node it sits on or last passed (none when a guard ended it before the first), and how it ended (`running` until then; `goal` is a conversion, at `finished_at`).',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n enteredAt: ts,\n nodeId: nullable(text),\n nodeAt: nullable(ts),\n status: text,\n finishedAt: nullable(ts),\n convertedAt: nullable(\n ts,\n 'When the goal was met, whatever the status: with `exitOnMatch: false` a converted instance ends `completed`.'\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [\n ['workflow', 'status'],\n ['workflow', 'finishedAt'],\n ],\n }),\n message: table({\n name: 'message',\n doc: \"One row per message copy (a push fans out per device). The idempotency key is the engine's copy identity, so a retried step finds the row it wrote; `channel` is where the copy landed (`email`, `push_ios`, `push_android`).\",\n columns: {\n id: uuid,\n idempotencyKey: text,\n workflow: text,\n instanceId: text,\n nodeId: text,\n template: text,\n channel: text,\n userId: text,\n sentAt: ts,\n deliveredAt: nullable(ts, 'The transport accepted it.'),\n deviceDeliveredAt: nullable(\n ts,\n \"A push: the device reported it received (iOS's notification service extension, a browser's service worker).\"\n ),\n undeliveredReason: nullable(\n text,\n 'The transport settled it without delivering (a dead push token).'\n ),\n openedAt: nullable(ts, 'First open of any kind, by a person or a machine.'),\n humanOpenedAt: nullable(ts, 'First open a person made.'),\n clickedAt: nullable(ts),\n humanClickedAt: nullable(ts),\n unsubscribedAt: nullable(ts),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['idempotencyKey']],\n indexes: [['workflow', 'sentAt'], ['instanceId']],\n }),\n messageLink: table({\n name: 'message_link',\n doc: \"A sent email's links in document order: the tracked URL names a message and a position, and the redirect goes only to the URL recorded here.\",\n columns: { messageId: uuid, position: integer, url: text },\n primaryKey: ['messageId', 'position'],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageOpen: table({\n name: 'message_open',\n doc: \"Every open-pixel load or push tap, classified when recorded: `machine` when no person caused it, with the deciding rule in `reason`; the request's network (never its address) kept for revisiting the rules.\",\n columns: {\n id: uuid,\n messageId: uuid,\n openedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageClick: table({\n name: 'message_click',\n doc: 'Every click on a tracked link, classified like opens.',\n columns: {\n id: uuid,\n messageId: uuid,\n position: integer,\n url: text,\n clickedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n} satisfies Record<string, TableSpec>;\n\nexport type TableKey = keyof typeof TABLES;\n\n/** The engine's schema when the config names none. */\nexport const DEFAULT_SCHEMA = 'workflow';\n\n/** `eventId` → `event_id`. */\nexport function columnName(key: string): string {\n return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n\nconst quote = (name: string) => `\"${name.replace(/\"/g, '\"\"')}\"`;\n\nconst SQL_TYPES: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'integer',\n boolean: 'boolean',\n};\n\n/**\n * Plain Postgres DDL for every table — for a host without a migration tool, and the tests. A\n * host that migrates with drizzle generates `renderDrizzleSchema` instead and lets drizzle-kit\n * write the migration.\n */\nexport function renderPostgresDdl(schema = DEFAULT_SCHEMA): string {\n const s = quote(schema);\n const out = [`CREATE SCHEMA IF NOT EXISTS ${s};`];\n for (const spec of Object.values(TABLES)) {\n const lines = Object.entries(spec.columns).map(\n ([key, column]) =>\n ` ${quote(columnName(key))} ${SQL_TYPES[column.type]}${column.nullable ? '' : ' NOT NULL'}`\n );\n lines.push(` PRIMARY KEY (${spec.primaryKey.map((k) => quote(columnName(k))).join(', ')})`);\n if (spec.uuidV7 !== undefined) {\n lines.push(\n ` CONSTRAINT ${quote(`${spec.name}_uuid_v7`)} CHECK (substring(${quote(columnName(spec.uuidV7))}::text, 15, 1) = '7')`\n );\n }\n for (const ref of spec.references ?? []) {\n const target = TABLES[ref.table as TableKey];\n lines.push(\n ` FOREIGN KEY (${ref.columns.map((k) => quote(columnName(k))).join(', ')}) REFERENCES ${s}.${quote(target.name)} (${ref.foreignColumns.map((k) => quote(columnName(k))).join(', ')})`\n );\n }\n out.push(`CREATE TABLE IF NOT EXISTS ${s}.${quote(spec.name)} (\\n${lines.join(',\\n')}\\n);`);\n const index = (unique: boolean) => (keys: readonly string[]) => {\n const cols = keys.map(columnName);\n out.push(\n `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quote(`${spec.name}_${cols.join('_')}_index`)} ON ${s}.${quote(spec.name)} (${cols.map(quote).join(', ')});`\n );\n };\n (spec.uniqueIndexes ?? []).forEach(index(true));\n (spec.indexes ?? []).forEach(index(false));\n }\n return `${out.join('\\n')}\\n`;\n}\n\nconst DRIZZLE_TYPES: Record<ColumnType, (column: string) => string> = {\n uuid: (c) => `uuid('${c}')`,\n text: (c) => `text('${c}')`,\n jsonb: (c) => `jsonb('${c}')`,\n timestamptz: (c) => `timestamp('${c}', { mode: 'string', withTimezone: true })`,\n integer: (c) => `integer('${c}')`,\n boolean: (c) => `boolean('${c}')`,\n};\n\nconst exportName = (key: string) => `workflow${key.charAt(0).toUpperCase()}${key.slice(1)}`;\n\n/**\n * The tables as one drizzle file: `export const workflowSchema = pgSchema(<schema>)` and every\n * table as `workflowSchema.table(...)`. The host re-exports it from its drizzle schema and\n * generates the migration with drizzle-kit, so the migration history stays the host's. The\n * output is deterministic — regenerate and diff it in CI to catch an SDK upgrade that changed the\n * tables.\n */\nexport function renderDrizzleSchema(schema = DEFAULT_SCHEMA): string {\n const specs = Object.entries(TABLES) as [TableKey, TableSpec][];\n const used = new Set<string>(['pgSchema']);\n const body: string[] = [];\n let needsSql = false;\n for (const [key, spec] of specs) {\n const columns = Object.entries(spec.columns).map(([prop, column]) => {\n used.add(column.type === 'timestamptz' ? 'timestamp' : column.type);\n const doc = column.doc === undefined ? '' : ` /** ${column.doc} */\\n`;\n const key =\n spec.primaryKey.length === 1 && spec.primaryKey[0] === prop ? '.primaryKey()' : '';\n return `${doc} ${prop}: ${DRIZZLE_TYPES[column.type](columnName(prop))}${key}${column.nullable ? '' : '.notNull()'},`;\n });\n const extras: string[] = [];\n if (spec.primaryKey.length > 1) {\n used.add('primaryKey');\n extras.push(\n `primaryKey({ columns: [${spec.primaryKey.map((k) => `table.${k}`).join(', ')}] })`\n );\n }\n for (const keys of spec.uniqueIndexes ?? []) {\n used.add('uniqueIndex');\n extras.push(`uniqueIndex().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const keys of spec.indexes ?? []) {\n used.add('index');\n extras.push(`index().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const ref of spec.references ?? []) {\n used.add('foreignKey');\n extras.push(\n `foreignKey({ columns: [${ref.columns.map((k) => `table.${k}`).join(', ')}], foreignColumns: [${ref.foreignColumns.map((k) => `${exportName(ref.table)}.${k}`).join(', ')}] })`\n );\n }\n if (spec.uuidV7 !== undefined) {\n used.add('check');\n needsSql = true;\n extras.push(\n `check('${spec.name}_uuid_v7', sql\\`substring(\\${table.${spec.uuidV7}}::text, 15, 1) = '7'\\`)`\n );\n }\n const third =\n extras.length === 0\n ? ''\n : `,\\n (table) => [\\n${extras.map((e) => ` ${e},`).join('\\n')}\\n ]`;\n body.push(\n `/** ${spec.doc} */\\nexport const ${exportName(key)} = workflowSchema.table(\\n '${spec.name}',\\n {\\n${columns.join('\\n')}\\n }${third}\\n);`\n );\n }\n const imports = [...used].sort();\n return [\n '// Generated by `workflow-ui generate` from @shware/workflow — do not edit; regenerate after',\n '// upgrading the SDK and let drizzle-kit write the migration.',\n ...(needsSql ? [\"import { sql } from 'drizzle-orm';\"] : []),\n `import {\\n${imports.map((name) => ` ${name},`).join('\\n')}\\n} from 'drizzle-orm/pg-core';`,\n '',\n `export const workflowSchema = pgSchema('${schema}');`,\n '',\n body.join('\\n\\n'),\n '',\n ].join('\\n');\n}\n"],"mappings":";;AA0CA,SAAS,MAAwB,MAAkC;CACjE,OAAO;AACT;AAEA,MAAM,OAAO,EAAE,MAAM,OAAO;AAC5B,MAAM,OAAO,EAAE,MAAM,OAAO;AAC5B,MAAM,QAAQ,EAAE,MAAM,QAAQ;AAC9B,MAAM,KAAK,EAAE,MAAM,cAAc;AACjC,MAAM,UAAU,EAAE,MAAM,UAAU;AAClC,MAAM,UAAU,EAAE,MAAM,UAAU;AAClC,MAAM,YAAkC,QAAW,SAA8B;CAC/E,GAAG;CACH,UAAU;CACV,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;AACrC;AAEA,MAAa,SAAS;CACpB,OAAO,MAAM;EACX,MAAM;EACN,KAAK;EACL,SAAS;GAAE,IAAI;GAAM,SAAS;GAAM,QAAQ;GAAM,MAAM;GAAM;GAAI,SAAS;EAAM;EACjF,YAAY,CAAC,IAAI;EACjB,QAAQ;EACR,eAAe,CAAC,CAAC,SAAS,CAAC;EAC3B,SAAS,CAAC;GAAC;GAAU;GAAQ;EAAI,CAAC;CACpC,CAAC;CACD,SAAS,MAAM;EACb,MAAM;EACN,KAAK;EACL,SAAS;GAAE,QAAQ;GAAM,OAAO;EAAM;EACtC,YAAY,CAAC,QAAQ;CACvB,CAAC;CACD,QAAQ,MAAM;EACZ,MAAM;EACN,KAAK;EACL,SAAS;GAAE,OAAO;GAAM,QAAQ,SAAS,IAAI;GAAG,UAAU;GAAM;EAAG;EACnE,YAAY,CAAC,OAAO;EACpB,SAAS,CAAC,CAAC,QAAQ,CAAC;CACtB,CAAC;CACD,SAAS,MAAM;EACb,MAAM;EACN,KAAK;EACL,SAAS;GAAE,QAAQ;GAAM,aAAa;EAAG;EACzC,YAAY,CAAC,QAAQ;CACvB,CAAC;CACD,aAAa,MAAM;EACjB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,QAAQ;GAAM,SAAS;GAAM;EAAG;EAC3C,YAAY,CAAC,UAAU,SAAS;CAClC,CAAC;CACD,SAAS,MAAM;EACb,MAAM;EACN,KAAK;EACL,SAAS;GAAE,IAAI;GAAM,WAAW;GAAO,MAAM;EAAK;EAClD,YAAY,CAAC,IAAI;CACnB,CAAC;CACD,SAAS,MAAM;EACb,MAAM;EACN,KAAK;EACL,SAAS;GACP,UAAU;GACV,MAAM;GACN,OAAO;GACP,OAAO,SAAS,KAAK;GACrB,QAAQ,SAAS,KAAK;EACxB;EACA,YAAY,CAAC,UAAU;EACvB,SAAS,CAAC,CAAC,OAAO,CAAC;CACrB,CAAC;CACD,gBAAgB,MAAM;EACpB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,UAAU;GAAM,MAAM;GAAM,SAAS;EAAK;EACrD,YAAY,CAAC,UAAU;EACvB,SAAS,CAAC,CAAC,SAAS,CAAC;CACvB,CAAC;CACD,eAAe,MAAM;EACnB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,SAAS;GAAM,QAAQ;GAAM;EAAG;EAC3C,YAAY,CAAC,WAAW,QAAQ;CAClC,CAAC;CACD,cAAc,MAAM;EAClB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,SAAS;GAAM,UAAU;GAAM,QAAQ;GAAM;EAAG;EAC3D,YAAY;GAAC;GAAW;GAAY;EAAQ;CAC9C,CAAC;CACD,iBAAiB,MAAM;EACrB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,SAAS;GAAM,SAAS;GAAI,MAAM;EAAQ;EACrD,YAAY,CAAC,WAAW,SAAS;CACnC,CAAC;CACD,OAAO,MAAM;EACX,MAAM;EACN,KAAK;EACL,SAAS;GAAE,UAAU;GAAM,QAAQ;GAAM,YAAY;GAAM,MAAM;GAAM,QAAQ;GAAM;EAAG;EACxF,YAAY,CAAC,YAAY,QAAQ;EACjC,SAAS,CAAC,CAAC,YAAY,CAAC;CAC1B,CAAC;CACD,cAAc,MAAM;EAClB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,QAAQ;GAAM,OAAO;GAAM,YAAY;GAAM;EAAG;EAC3D,YAAY,CAAC,cAAc,OAAO;EAClC,SAAS,CAAC,CAAC,UAAU,OAAO,CAAC;CAC/B,CAAC;CACD,WAAW,MAAM;EACf,MAAM;EACN,KAAK;EACL,SAAS;GACP,YAAY;GACZ,UAAU;GACV,QAAQ;GACR,MAAM;GACN,QAAQ;GACR,OAAO;GACP,WAAW;GACX,aAAa,SACX,IACA,6EACF;GACA,KAAK,SAAS,IAAI;GAClB,cAAc,SACZ,IACA,6GACF;GACA,YAAY,SAAS,MAAM,8DAA8D;GACzF,YAAY;GACZ,YAAY,SAAS,EAAE;GACvB,WAAW,SACT,IACA,sEACF;EACF;EACA,YAAY,CAAC,YAAY;EACzB,SAAS;GAAC,CAAC,YAAY,QAAQ;GAAG,CAAC,aAAa;GAAG,CAAC,QAAQ;EAAC;CAC/D,CAAC;CACD,YAAY,MAAM;EAChB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,OAAO;GAAM,WAAW;GAAM,kBAAkB;GAAI,WAAW;EAAG;EAC7E,YAAY,CAAC,OAAO;CACtB,CAAC;CACD,IAAI,MAAM;EACR,MAAM;EACN,KAAK;EACL,SAAS;GAAE,aAAa;GAAM,IAAI;EAAM;EACxC,YAAY,CAAC,aAAa;CAC5B,CAAC;CACD,UAAU,MAAM;EACd,MAAM;EACN,KAAK;EACL,SAAS;GACP,YAAY;GACZ,UAAU;GACV,QAAQ;GACR,WAAW;GACX,QAAQ,SAAS,IAAI;GACrB,QAAQ,SAAS,EAAE;GACnB,QAAQ;GACR,YAAY,SAAS,EAAE;GACvB,aAAa,SACX,IACA,8GACF;EACF;EACA,YAAY,CAAC,YAAY;EACzB,SAAS,CACP,CAAC,YAAY,QAAQ,GACrB,CAAC,YAAY,YAAY,CAC3B;CACF,CAAC;CACD,SAAS,MAAM;EACb,MAAM;EACN,KAAK;EACL,SAAS;GACP,IAAI;GACJ,gBAAgB;GAChB,UAAU;GACV,YAAY;GACZ,QAAQ;GACR,UAAU;GACV,SAAS;GACT,QAAQ;GACR,QAAQ;GACR,aAAa,SAAS,IAAI,4BAA4B;GACtD,mBAAmB,SACjB,IACA,6GACF;GACA,mBAAmB,SACjB,MACA,kEACF;GACA,UAAU,SAAS,IAAI,mDAAmD;GAC1E,eAAe,SAAS,IAAI,2BAA2B;GACvD,WAAW,SAAS,EAAE;GACtB,gBAAgB,SAAS,EAAE;GAC3B,gBAAgB,SAAS,EAAE;EAC7B;EACA,YAAY,CAAC,IAAI;EACjB,QAAQ;EACR,eAAe,CAAC,CAAC,gBAAgB,CAAC;EAClC,SAAS,CAAC,CAAC,YAAY,QAAQ,GAAG,CAAC,YAAY,CAAC;CAClD,CAAC;CACD,aAAa,MAAM;EACjB,MAAM;EACN,KAAK;EACL,SAAS;GAAE,WAAW;GAAM,UAAU;GAAS,KAAK;EAAK;EACzD,YAAY,CAAC,aAAa,UAAU;EACpC,YAAY,CAAC;GAAE,SAAS,CAAC,WAAW;GAAG,OAAO;GAAW,gBAAgB,CAAC,IAAI;EAAE,CAAC;CACnF,CAAC;CACD,aAAa,MAAM;EACjB,MAAM;EACN,KAAK;EACL,SAAS;GACP,IAAI;GACJ,WAAW;GACX,UAAU;GACV,SAAS;GACT,QAAQ,SAAS,IAAI;GACrB,WAAW,SAAS,IAAI;GACxB,KAAK,SAAS,OAAO;GACrB,gBAAgB,SAAS,IAAI;GAC7B,SAAS,SAAS,IAAI;EACxB;EACA,YAAY,CAAC,IAAI;EACjB,QAAQ;EACR,SAAS,CAAC,CAAC,WAAW,CAAC;EACvB,YAAY,CAAC;GAAE,SAAS,CAAC,WAAW;GAAG,OAAO;GAAW,gBAAgB,CAAC,IAAI;EAAE,CAAC;CACnF,CAAC;CACD,cAAc,MAAM;EAClB,MAAM;EACN,KAAK;EACL,SAAS;GACP,IAAI;GACJ,WAAW;GACX,UAAU;GACV,KAAK;GACL,WAAW;GACX,SAAS;GACT,QAAQ,SAAS,IAAI;GACrB,WAAW,SAAS,IAAI;GACxB,KAAK,SAAS,OAAO;GACrB,gBAAgB,SAAS,IAAI;GAC7B,SAAS,SAAS,IAAI;EACxB;EACA,YAAY,CAAC,IAAI;EACjB,QAAQ;EACR,SAAS,CAAC,CAAC,WAAW,CAAC;EACvB,YAAY,CAAC;GAAE,SAAS,CAAC,WAAW;GAAG,OAAO;GAAW,gBAAgB,CAAC,IAAI;EAAE,CAAC;CACnF,CAAC;AACH;;AAKA,MAAa,iBAAiB;;AAG9B,SAAgB,WAAW,KAAqB;CAC9C,OAAO,IAAI,QAAQ,WAAW,MAAM,IAAI,EAAE,YAAY,GAAG;AAC3D;AAEA,MAAM,SAAS,SAAiB,IAAI,KAAK,QAAQ,MAAM,MAAI,EAAE;AAE7D,MAAM,YAAwC;CAC5C,MAAM;CACN,MAAM;CACN,OAAO;CACP,aAAa;CACb,SAAS;CACT,SAAS;AACX;;;;;;AAOA,SAAgB,kBAAkB,SAAS,gBAAwB;CACjE,MAAM,IAAI,MAAM,MAAM;CACtB,MAAM,MAAM,CAAC,+BAA+B,EAAE,EAAE;CAChD,KAAK,MAAM,QAAQ,OAAO,OAAO,MAAM,GAAG;EACxC,MAAM,QAAQ,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,KACxC,CAAC,KAAK,YACL,KAAK,MAAM,WAAW,GAAG,CAAC,EAAE,GAAG,UAAU,OAAO,QAAQ,OAAO,WAAW,KAAK,aACnF;EACA,MAAM,KAAK,kBAAkB,KAAK,WAAW,KAAK,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;EAC3F,IAAI,KAAK,WAAW,KAAA,GAClB,MAAM,KACJ,gBAAgB,MAAM,GAAG,KAAK,KAAK,SAAS,EAAE,oBAAoB,MAAM,WAAW,KAAK,MAAM,CAAC,EAAE,sBACnG;EAEF,KAAK,MAAM,OAAO,KAAK,cAAc,CAAC,GAAG;GACvC,MAAM,SAAS,OAAO,IAAI;GAC1B,MAAM,KACJ,kBAAkB,IAAI,QAAQ,KAAK,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,eAAe,EAAE,GAAG,MAAM,OAAO,IAAI,EAAE,IAAI,IAAI,eAAe,KAAK,MAAM,MAAM,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,EACtL;EACF;EACA,IAAI,KAAK,8BAA8B,EAAE,GAAG,MAAM,KAAK,IAAI,EAAE,MAAM,MAAM,KAAK,KAAK,EAAE,KAAK;EAC1F,MAAM,SAAS,YAAqB,SAA4B;GAC9D,MAAM,OAAO,KAAK,IAAI,UAAU;GAChC,IAAI,KACF,UAAU,SAAS,YAAY,GAAG,sBAAsB,MAAM,GAAG,KAAK,KAAK,GAAG,KAAK,KAAK,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,IAAI,EAAE,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,GACnK;EACF;EACA,CAAC,KAAK,iBAAiB,CAAC,EAAA,CAAG,QAAQ,MAAM,IAAI,CAAC;EAC9C,CAAC,KAAK,WAAW,CAAC,EAAA,CAAG,QAAQ,MAAM,KAAK,CAAC;CAC3C;CACA,OAAO,GAAG,IAAI,KAAK,IAAI,EAAE;AAC3B;AAEA,MAAM,gBAAgE;CACpE,OAAO,MAAM,SAAS,EAAE;CACxB,OAAO,MAAM,SAAS,EAAE;CACxB,QAAQ,MAAM,UAAU,EAAE;CAC1B,cAAc,MAAM,cAAc,EAAE;CACpC,UAAU,MAAM,YAAY,EAAE;CAC9B,UAAU,MAAM,YAAY,EAAE;AAChC;AAEA,MAAM,cAAc,QAAgB,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,MAAM,CAAC;;;;;;;;AASxF,SAAgB,oBAAoB,SAAS,gBAAwB;CACnE,MAAM,QAAQ,OAAO,QAAQ,MAAM;CACnC,MAAM,uBAAO,IAAI,IAAY,CAAC,UAAU,CAAC;CACzC,MAAM,OAAiB,CAAC;CACxB,IAAI,WAAW;CACf,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO;EAC/B,MAAM,UAAU,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,YAAY;GACnE,KAAK,IAAI,OAAO,SAAS,gBAAgB,cAAc,OAAO,IAAI;GAClE,MAAM,MAAM,OAAO,QAAQ,KAAA,IAAY,KAAK,WAAW,OAAO,IAAI;GAClE,MAAM,MACJ,KAAK,WAAW,WAAW,KAAK,KAAK,WAAW,OAAO,OAAO,kBAAkB;GAClF,OAAO,GAAG,IAAI,MAAM,KAAK,IAAI,cAAc,OAAO,KAAK,CAAC,WAAW,IAAI,CAAC,IAAI,MAAM,OAAO,WAAW,KAAK,aAAa;EACxH,CAAC;EACD,MAAM,SAAmB,CAAC;EAC1B,IAAI,KAAK,WAAW,SAAS,GAAG;GAC9B,KAAK,IAAI,YAAY;GACrB,OAAO,KACL,0BAA0B,KAAK,WAAW,KAAK,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,KAChF;EACF;EACA,KAAK,MAAM,QAAQ,KAAK,iBAAiB,CAAC,GAAG;GAC3C,KAAK,IAAI,aAAa;GACtB,OAAO,KAAK,oBAAoB,KAAK,KAAK,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;EAC7E;EACA,KAAK,MAAM,QAAQ,KAAK,WAAW,CAAC,GAAG;GACrC,KAAK,IAAI,OAAO;GAChB,OAAO,KAAK,cAAc,KAAK,KAAK,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE;EACvE;EACA,KAAK,MAAM,OAAO,KAAK,cAAc,CAAC,GAAG;GACvC,KAAK,IAAI,YAAY;GACrB,OAAO,KACL,0BAA0B,IAAI,QAAQ,KAAK,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,sBAAsB,IAAI,eAAe,KAAK,MAAM,GAAG,WAAW,IAAI,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,KAC5K;EACF;EACA,IAAI,KAAK,WAAW,KAAA,GAAW;GAC7B,KAAK,IAAI,OAAO;GAChB,WAAW;GACX,OAAO,KACL,UAAU,KAAK,KAAK,qCAAqC,KAAK,OAAO,yBACvE;EACF;EACA,MAAM,QACJ,OAAO,WAAW,IACd,KACA,sBAAsB,OAAO,KAAK,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EACtE,KAAK,KACH,OAAO,KAAK,IAAI,oBAAoB,WAAW,GAAG,EAAE,+BAA+B,KAAK,KAAK,WAAW,QAAQ,KAAK,IAAI,EAAE,OAAO,MAAM,KAC1I;CACF;CACA,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK;CAC/B,OAAO;EACL;EACA;EACA,GAAI,WAAW,CAAC,oCAAoC,IAAI,CAAC;EACzD,aAAa,QAAQ,KAAK,SAAS,KAAK,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EAC5D;EACA,2CAA2C,OAAO;EAClD;EACA,KAAK,KAAK,MAAM;EAChB;CACF,CAAC,CAAC,KAAK,IAAI;AACb"}
|
package/dist/store/tables.d.cts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.cts","names":[],"sources":["../../src/store/tables.ts"],"mappings":";;;;;;;;;;;;;YAaY;iBAEK;EACf,MAAM;;EAEN;;EAEA;;iBAGe,UAAU;;EAEzB;EACA;;EAEA,SAAS,OAAO,GAAG;EACnB,qBAAqB;;EAErB,SAAS;EACT,mCAAmC;EACnC,6BAA6B;;EAE7B;IACE,kBAAkB;IAClB;IACA;;;qBAoBS
|
|
1
|
+
{"version":3,"file":"tables.d.cts","names":[],"sources":["../../src/store/tables.ts"],"mappings":";;;;;;;;;;;;;YAaY;iBAEK;EACf,MAAM;;EAEN;;EAEA;;iBAGe,UAAU;;EAEzB;EACA;;EAEA,SAAS,OAAO,GAAG;EACnB,qBAAqB;;EAErB,SAAS;EACT,mCAAmC;EACnC,6BAA6B;;EAE7B;IACE,kBAAkB;IAClB;IACA;;;qBAoBS;;;;;;;;;;;;;;;;;;;;;;;YAiPD,wBAAwB;;qBAGvB;;wBAGG,WAAW;;;;;;wBAoBX,kBAAkB;;;;;;;;wBAmDlB,oBAAoB"}
|
package/dist/store/tables.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.mts","names":[],"sources":["../../src/store/tables.ts"],"mappings":";;;;;;;;;;;;;YAaY;iBAEK;EACf,MAAM;;EAEN;;EAEA;;iBAGe,UAAU;;EAEzB;EACA;;EAEA,SAAS,OAAO,GAAG;EACnB,qBAAqB;;EAErB,SAAS;EACT,mCAAmC;EACnC,6BAA6B;;EAE7B;IACE,kBAAkB;IAClB;IACA;;;qBAoBS
|
|
1
|
+
{"version":3,"file":"tables.d.mts","names":[],"sources":["../../src/store/tables.ts"],"mappings":";;;;;;;;;;;;;YAaY;iBAEK;EACf,MAAM;;EAEN;;EAEA;;iBAGe,UAAU;;EAEzB;EACA;;EAEA,SAAS,OAAO,GAAG;EACnB,qBAAqB;;EAErB,SAAS;EACT,mCAAmC;EACnC,6BAA6B;;EAE7B;IACE,kBAAkB;IAClB;IACA;;;qBAoBS;;;;;;;;;;;;;;;;;;;;;;;YAiPD,wBAAwB;;qBAGvB;;wBAGG,WAAW;;;;;;wBAoBX,kBAAkB;;;;;;;;wBAmDlB,oBAAoB"}
|
package/dist/store/tables.mjs
CHANGED
|
@@ -169,6 +169,32 @@ const TABLES = {
|
|
|
169
169
|
primaryKey: ["wakeHandle", "event"],
|
|
170
170
|
indexes: [["userId", "event"]]
|
|
171
171
|
}),
|
|
172
|
+
execution: table({
|
|
173
|
+
name: "execution",
|
|
174
|
+
doc: "Durable executions whose start is an asynchronous submit (AWS Lambda durable functions): written with the entry, so a lost submit response is retried with the same name, target and payload instead of starting a second execution; and the wake callback the instance is parked on now. Unused on Cloudflare.",
|
|
175
|
+
columns: {
|
|
176
|
+
instanceId: text,
|
|
177
|
+
workflow: text,
|
|
178
|
+
userId: text,
|
|
179
|
+
name: text,
|
|
180
|
+
target: text,
|
|
181
|
+
input: text,
|
|
182
|
+
createdAt: ts,
|
|
183
|
+
submittedAt: nullable(ts, "The runtime accepted the start; null while it may still have to be retried."),
|
|
184
|
+
arn: nullable(text),
|
|
185
|
+
unresolvedAt: nullable(ts, "Retries ran out before the runtime confirmed the start: whether it runs is unknown, and nothing retries it."),
|
|
186
|
+
callbackId: nullable(text, "The callback the current wait parks on; a wake completes it."),
|
|
187
|
+
generation: integer,
|
|
188
|
+
deadlineAt: nullable(ts),
|
|
189
|
+
checkedAt: nullable(ts, "When maintenance last compared the runtime's status with the ledger.")
|
|
190
|
+
},
|
|
191
|
+
primaryKey: ["instanceId"],
|
|
192
|
+
indexes: [
|
|
193
|
+
["workflow", "userId"],
|
|
194
|
+
["submittedAt"],
|
|
195
|
+
["userId"]
|
|
196
|
+
]
|
|
197
|
+
}),
|
|
172
198
|
deployment: table({
|
|
173
199
|
name: "deployment",
|
|
174
200
|
doc: "The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.mjs","names":[],"sources":["../../src/store/tables.ts"],"sourcesContent":["/**\n * The engine's Postgres tables, declared once. Everything that needs their shape derives it from\n * here: the drizzle schema file a host generates and migrates with its own tooling\n * (`renderDrizzleSchema`), the plain DDL (`renderPostgresDdl`), the store's table names, and the\n * check a deploy runs against the live database (`checkPostgresSchema`).\n *\n * The house rules hold for every table: no database defaults (the store writes every value,\n * timestamps included), timestamptz for time, and a UUID v7 row id — generated by the store and\n * guarded by a CHECK — wherever a table has a surrogate key. All tables live in one Postgres\n * schema of their own (`workflow` unless the config names another), so their short names never\n * meet the app's.\n */\n\nexport type ColumnType = 'uuid' | 'text' | 'jsonb' | 'timestamptz' | 'integer' | 'boolean';\n\nexport interface ColumnSpec {\n type: ColumnType;\n /** Columns are NOT NULL unless marked. */\n nullable?: boolean;\n /** One line for the generated file. */\n doc?: string;\n}\n\nexport interface TableSpec<K extends string = string> {\n /** Physical table name inside the schema. */\n name: string;\n doc: string;\n /** Keyed by the drizzle property name; the column name is its snake_case. */\n columns: Record<K, ColumnSpec>;\n primaryKey: readonly K[];\n /** A uuid column whose values must be version 7. */\n uuidV7?: K;\n uniqueIndexes?: readonly (readonly K[])[];\n indexes?: readonly (readonly K[])[];\n /** Foreign keys, to another table's logical key. */\n references?: readonly {\n columns: readonly K[];\n table: string;\n foreignColumns: readonly string[];\n }[];\n}\n\nfunction table<K extends string>(spec: TableSpec<K>): TableSpec<K> {\n return spec;\n}\n\nconst uuid = { type: 'uuid' } as const;\nconst text = { type: 'text' } as const;\nconst jsonb = { type: 'jsonb' } as const;\nconst ts = { type: 'timestamptz' } as const;\nconst integer = { type: 'integer' } as const;\nconst boolean = { type: 'boolean' } as const;\nconst nullable = <C extends ColumnSpec>(column: C, doc?: string): ColumnSpec => ({\n ...column,\n nullable: true,\n ...(doc === undefined ? {} : { doc }),\n});\n\nexport const TABLES = {\n event: table({\n name: 'event',\n doc: \"Every ingested event, the fact base for `performed(...)` conditions. `event_id` is the occurrence's identity as the sender chose it (a retrying producer sends the same one again); its unique index makes a second delivery a no-op.\",\n columns: { id: uuid, eventId: text, userId: text, name: text, ts, payload: jsonb },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['eventId']],\n indexes: [['userId', 'name', 'ts']],\n }),\n profile: table({\n name: 'profile',\n doc: 'Identified properties per user (`/identify` merges into `props`; a null value means unset).',\n columns: { userId: text, props: jsonb },\n primaryKey: ['userId'],\n }),\n device: table({\n name: 'device',\n doc: 'Push devices, registered through the Worker (`JourneyClient.registerDevice`). Signing out unbinds a device (`user_id` null) rather than removing it; a token APNs / FCM reports as gone is removed.',\n columns: { token: text, userId: nullable(text), platform: text, ts },\n primaryKey: ['token'],\n indexes: [['userId']],\n }),\n erasure: table({\n name: 'erasure',\n doc: \"Erasures in progress (`forgetUser`): the user's data stays until every running instance is stopped, and the scheduled sweep retries what is left.\",\n columns: { userId: text, requestedAt: ts },\n primaryKey: ['userId'],\n }),\n suppression: table({\n name: 'suppression',\n doc: 'Channels a user opted out of (an email unsubscribe): the engine sends nothing there, and `reachable(channel)` is false.',\n columns: { userId: text, channel: text, ts },\n primaryKey: ['userId', 'channel'],\n }),\n segment: table({\n name: 'segment',\n doc: 'Deployed segment definitions, resolved by name at evaluation time.',\n columns: { id: text, condition: jsonb, hash: text },\n primaryKey: ['id'],\n }),\n trigger: table({\n name: 'trigger',\n doc: 'Event-trigger routes: `where` gates on the event payload, `filter` on the profile.',\n columns: {\n workflow: text,\n hash: text,\n event: text,\n where: nullable(jsonb),\n filter: nullable(jsonb),\n },\n primaryKey: ['workflow'],\n indexes: [['event']],\n }),\n segmentTrigger: table({\n name: 'segment_trigger',\n doc: 'Segment-entry trigger routes.',\n columns: { workflow: text, hash: text, segment: text },\n primaryKey: ['workflow'],\n indexes: [['segment']],\n }),\n segmentMember: table({\n name: 'segment_member',\n doc: 'Who is in each deployed segment and since when: kept by the scheduled sweep, and for trigger-routed segments on every event too.',\n columns: { segment: text, userId: text, ts },\n primaryKey: ['segment', 'userId'],\n }),\n segmentStart: table({\n name: 'segment_start',\n doc: 'Workflows a segment entry still has to start for a member: written with the membership, removed as each one starts, so a failed start is retried by the next evaluation instead of being hidden behind the membership.',\n columns: { segment: text, workflow: text, userId: text, ts },\n primaryKey: ['segment', 'workflow', 'userId'],\n }),\n segmentSnapshot: table({\n name: 'segment_snapshot',\n doc: \"Each segment's size at every sweep, the Studio's sparklines.\",\n columns: { segment: text, takenAt: ts, size: integer },\n primaryKey: ['segment', 'takenAt'],\n }),\n entry: table({\n name: 'entry',\n doc: 'Entry ledger: the (workflow, user) primary key is the once policy.',\n columns: { workflow: text, userId: text, instanceId: text, hash: text, status: text, ts },\n primaryKey: ['workflow', 'userId'],\n indexes: [['instanceId']],\n }),\n subscription: table({\n name: 'subscription',\n doc: 'wait_until wake routing; the primary key makes repeated subscribes idempotent.',\n columns: { userId: text, event: text, wakeHandle: text, ts },\n primaryKey: ['wakeHandle', 'event'],\n indexes: [['userId', 'event']],\n }),\n deployment: table({\n name: 'deployment',\n doc: \"The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.\",\n columns: { scope: text, versionId: text, versionCreatedAt: ts, appliedAt: ts },\n primaryKey: ['scope'],\n }),\n ir: table({\n name: 'ir',\n doc: 'Workflow bodies (WorkflowIR), content-addressed.',\n columns: { contentHash: text, ir: jsonb },\n primaryKey: ['contentHash'],\n }),\n instance: table({\n name: 'instance',\n doc: 'One row per journey instance, written when it enters: the node it sits on or last passed (none when a guard ended it before the first), and how it ended (`running` until then; `goal` is a conversion, at `finished_at`).',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n enteredAt: ts,\n nodeId: nullable(text),\n nodeAt: nullable(ts),\n status: text,\n finishedAt: nullable(ts),\n convertedAt: nullable(\n ts,\n 'When the goal was met, whatever the status: with `exitOnMatch: false` a converted instance ends `completed`.'\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [\n ['workflow', 'status'],\n ['workflow', 'finishedAt'],\n ],\n }),\n message: table({\n name: 'message',\n doc: \"One row per message copy (a push fans out per device). The idempotency key is the engine's copy identity, so a retried step finds the row it wrote; `channel` is where the copy landed (`email`, `push_ios`, `push_android`).\",\n columns: {\n id: uuid,\n idempotencyKey: text,\n workflow: text,\n instanceId: text,\n nodeId: text,\n template: text,\n channel: text,\n userId: text,\n sentAt: ts,\n deliveredAt: nullable(ts, 'The transport accepted it.'),\n deviceDeliveredAt: nullable(\n ts,\n \"A push: the device reported it received (iOS's notification service extension, a browser's service worker).\"\n ),\n undeliveredReason: nullable(\n text,\n 'The transport settled it without delivering (a dead push token).'\n ),\n openedAt: nullable(ts, 'First open of any kind, by a person or a machine.'),\n humanOpenedAt: nullable(ts, 'First open a person made.'),\n clickedAt: nullable(ts),\n humanClickedAt: nullable(ts),\n unsubscribedAt: nullable(ts),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['idempotencyKey']],\n indexes: [['workflow', 'sentAt'], ['instanceId']],\n }),\n messageLink: table({\n name: 'message_link',\n doc: \"A sent email's links in document order: the tracked URL names a message and a position, and the redirect goes only to the URL recorded here.\",\n columns: { messageId: uuid, position: integer, url: text },\n primaryKey: ['messageId', 'position'],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageOpen: table({\n name: 'message_open',\n doc: \"Every open-pixel load or push tap, classified when recorded: `machine` when no person caused it, with the deciding rule in `reason`; the request's network (never its address) kept for revisiting the rules.\",\n columns: {\n id: uuid,\n messageId: uuid,\n openedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageClick: table({\n name: 'message_click',\n doc: 'Every click on a tracked link, classified like opens.',\n columns: {\n id: uuid,\n messageId: uuid,\n position: integer,\n url: text,\n clickedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n} satisfies Record<string, TableSpec>;\n\nexport type TableKey = keyof typeof TABLES;\n\n/** The engine's schema when the config names none. */\nexport const DEFAULT_SCHEMA = 'workflow';\n\n/** `eventId` → `event_id`. */\nexport function columnName(key: string): string {\n return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n\nconst quote = (name: string) => `\"${name.replace(/\"/g, '\"\"')}\"`;\n\nconst SQL_TYPES: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'integer',\n boolean: 'boolean',\n};\n\n/**\n * Plain Postgres DDL for every table — for a host without a migration tool, and the tests. A\n * host that migrates with drizzle generates `renderDrizzleSchema` instead and lets drizzle-kit\n * write the migration.\n */\nexport function renderPostgresDdl(schema = DEFAULT_SCHEMA): string {\n const s = quote(schema);\n const out = [`CREATE SCHEMA IF NOT EXISTS ${s};`];\n for (const spec of Object.values(TABLES)) {\n const lines = Object.entries(spec.columns).map(\n ([key, column]) =>\n ` ${quote(columnName(key))} ${SQL_TYPES[column.type]}${column.nullable ? '' : ' NOT NULL'}`\n );\n lines.push(` PRIMARY KEY (${spec.primaryKey.map((k) => quote(columnName(k))).join(', ')})`);\n if (spec.uuidV7 !== undefined) {\n lines.push(\n ` CONSTRAINT ${quote(`${spec.name}_uuid_v7`)} CHECK (substring(${quote(columnName(spec.uuidV7))}::text, 15, 1) = '7')`\n );\n }\n for (const ref of spec.references ?? []) {\n const target = TABLES[ref.table as TableKey];\n lines.push(\n ` FOREIGN KEY (${ref.columns.map((k) => quote(columnName(k))).join(', ')}) REFERENCES ${s}.${quote(target.name)} (${ref.foreignColumns.map((k) => quote(columnName(k))).join(', ')})`\n );\n }\n out.push(`CREATE TABLE IF NOT EXISTS ${s}.${quote(spec.name)} (\\n${lines.join(',\\n')}\\n);`);\n const index = (unique: boolean) => (keys: readonly string[]) => {\n const cols = keys.map(columnName);\n out.push(\n `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quote(`${spec.name}_${cols.join('_')}_index`)} ON ${s}.${quote(spec.name)} (${cols.map(quote).join(', ')});`\n );\n };\n (spec.uniqueIndexes ?? []).forEach(index(true));\n (spec.indexes ?? []).forEach(index(false));\n }\n return `${out.join('\\n')}\\n`;\n}\n\nconst DRIZZLE_TYPES: Record<ColumnType, (column: string) => string> = {\n uuid: (c) => `uuid('${c}')`,\n text: (c) => `text('${c}')`,\n jsonb: (c) => `jsonb('${c}')`,\n timestamptz: (c) => `timestamp('${c}', { mode: 'string', withTimezone: true })`,\n integer: (c) => `integer('${c}')`,\n boolean: (c) => `boolean('${c}')`,\n};\n\nconst exportName = (key: string) => `workflow${key.charAt(0).toUpperCase()}${key.slice(1)}`;\n\n/**\n * The tables as one drizzle file: `export const workflowSchema = pgSchema(<schema>)` and every\n * table as `workflowSchema.table(...)`. The host re-exports it from its drizzle schema and\n * generates the migration with drizzle-kit, so the migration history stays the host's. The\n * output is deterministic — regenerate and diff it in CI to catch an SDK upgrade that changed the\n * tables.\n */\nexport function renderDrizzleSchema(schema = DEFAULT_SCHEMA): string {\n const specs = Object.entries(TABLES) as [TableKey, TableSpec][];\n const used = new Set<string>(['pgSchema']);\n const body: string[] = [];\n let needsSql = false;\n for (const [key, spec] of specs) {\n const columns = Object.entries(spec.columns).map(([prop, column]) => {\n used.add(column.type === 'timestamptz' ? 'timestamp' : column.type);\n const doc = column.doc === undefined ? '' : ` /** ${column.doc} */\\n`;\n const key =\n spec.primaryKey.length === 1 && spec.primaryKey[0] === prop ? '.primaryKey()' : '';\n return `${doc} ${prop}: ${DRIZZLE_TYPES[column.type](columnName(prop))}${key}${column.nullable ? '' : '.notNull()'},`;\n });\n const extras: string[] = [];\n if (spec.primaryKey.length > 1) {\n used.add('primaryKey');\n extras.push(\n `primaryKey({ columns: [${spec.primaryKey.map((k) => `table.${k}`).join(', ')}] })`\n );\n }\n for (const keys of spec.uniqueIndexes ?? []) {\n used.add('uniqueIndex');\n extras.push(`uniqueIndex().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const keys of spec.indexes ?? []) {\n used.add('index');\n extras.push(`index().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const ref of spec.references ?? []) {\n used.add('foreignKey');\n extras.push(\n `foreignKey({ columns: [${ref.columns.map((k) => `table.${k}`).join(', ')}], foreignColumns: [${ref.foreignColumns.map((k) => `${exportName(ref.table)}.${k}`).join(', ')}] })`\n );\n }\n if (spec.uuidV7 !== undefined) {\n used.add('check');\n needsSql = true;\n extras.push(\n `check('${spec.name}_uuid_v7', sql\\`substring(\\${table.${spec.uuidV7}}::text, 15, 1) = '7'\\`)`\n );\n }\n const third =\n extras.length === 0\n ? ''\n : `,\\n (table) => [\\n${extras.map((e) => ` ${e},`).join('\\n')}\\n ]`;\n body.push(\n `/** ${spec.doc} */\\nexport const ${exportName(key)} = workflowSchema.table(\\n '${spec.name}',\\n {\\n${columns.join('\\n')}\\n }${third}\\n);`\n );\n }\n const imports = [...used].sort();\n return [\n '// Generated by `workflow-ui generate` from @shware/workflow — do not edit; regenerate after',\n '// upgrading the SDK and let drizzle-kit write the migration.',\n ...(needsSql ? [\"import { sql } from 'drizzle-orm';\"] : []),\n `import {\\n${imports.map((name) => ` ${name},`).join('\\n')}\\n} from 'drizzle-orm/pg-core';`,\n '',\n `export const workflowSchema = pgSchema('${schema}');`,\n '',\n body.join('\\n\\n'),\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tables.mjs","names":[],"sources":["../../src/store/tables.ts"],"sourcesContent":["/**\n * The engine's Postgres tables, declared once. Everything that needs their shape derives it from\n * here: the drizzle schema file a host generates and migrates with its own tooling\n * (`renderDrizzleSchema`), the plain DDL (`renderPostgresDdl`), the store's table names, and the\n * check a deploy runs against the live database (`checkPostgresSchema`).\n *\n * The house rules hold for every table: no database defaults (the store writes every value,\n * timestamps included), timestamptz for time, and a UUID v7 row id — generated by the store and\n * guarded by a CHECK — wherever a table has a surrogate key. All tables live in one Postgres\n * schema of their own (`workflow` unless the config names another), so their short names never\n * meet the app's.\n */\n\nexport type ColumnType = 'uuid' | 'text' | 'jsonb' | 'timestamptz' | 'integer' | 'boolean';\n\nexport interface ColumnSpec {\n type: ColumnType;\n /** Columns are NOT NULL unless marked. */\n nullable?: boolean;\n /** One line for the generated file. */\n doc?: string;\n}\n\nexport interface TableSpec<K extends string = string> {\n /** Physical table name inside the schema. */\n name: string;\n doc: string;\n /** Keyed by the drizzle property name; the column name is its snake_case. */\n columns: Record<K, ColumnSpec>;\n primaryKey: readonly K[];\n /** A uuid column whose values must be version 7. */\n uuidV7?: K;\n uniqueIndexes?: readonly (readonly K[])[];\n indexes?: readonly (readonly K[])[];\n /** Foreign keys, to another table's logical key. */\n references?: readonly {\n columns: readonly K[];\n table: string;\n foreignColumns: readonly string[];\n }[];\n}\n\nfunction table<K extends string>(spec: TableSpec<K>): TableSpec<K> {\n return spec;\n}\n\nconst uuid = { type: 'uuid' } as const;\nconst text = { type: 'text' } as const;\nconst jsonb = { type: 'jsonb' } as const;\nconst ts = { type: 'timestamptz' } as const;\nconst integer = { type: 'integer' } as const;\nconst boolean = { type: 'boolean' } as const;\nconst nullable = <C extends ColumnSpec>(column: C, doc?: string): ColumnSpec => ({\n ...column,\n nullable: true,\n ...(doc === undefined ? {} : { doc }),\n});\n\nexport const TABLES = {\n event: table({\n name: 'event',\n doc: \"Every ingested event, the fact base for `performed(...)` conditions. `event_id` is the occurrence's identity as the sender chose it (a retrying producer sends the same one again); its unique index makes a second delivery a no-op.\",\n columns: { id: uuid, eventId: text, userId: text, name: text, ts, payload: jsonb },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['eventId']],\n indexes: [['userId', 'name', 'ts']],\n }),\n profile: table({\n name: 'profile',\n doc: 'Identified properties per user (`/identify` merges into `props`; a null value means unset).',\n columns: { userId: text, props: jsonb },\n primaryKey: ['userId'],\n }),\n device: table({\n name: 'device',\n doc: 'Push devices, registered through the Worker (`JourneyClient.registerDevice`). Signing out unbinds a device (`user_id` null) rather than removing it; a token APNs / FCM reports as gone is removed.',\n columns: { token: text, userId: nullable(text), platform: text, ts },\n primaryKey: ['token'],\n indexes: [['userId']],\n }),\n erasure: table({\n name: 'erasure',\n doc: \"Erasures in progress (`forgetUser`): the user's data stays until every running instance is stopped, and the scheduled sweep retries what is left.\",\n columns: { userId: text, requestedAt: ts },\n primaryKey: ['userId'],\n }),\n suppression: table({\n name: 'suppression',\n doc: 'Channels a user opted out of (an email unsubscribe): the engine sends nothing there, and `reachable(channel)` is false.',\n columns: { userId: text, channel: text, ts },\n primaryKey: ['userId', 'channel'],\n }),\n segment: table({\n name: 'segment',\n doc: 'Deployed segment definitions, resolved by name at evaluation time.',\n columns: { id: text, condition: jsonb, hash: text },\n primaryKey: ['id'],\n }),\n trigger: table({\n name: 'trigger',\n doc: 'Event-trigger routes: `where` gates on the event payload, `filter` on the profile.',\n columns: {\n workflow: text,\n hash: text,\n event: text,\n where: nullable(jsonb),\n filter: nullable(jsonb),\n },\n primaryKey: ['workflow'],\n indexes: [['event']],\n }),\n segmentTrigger: table({\n name: 'segment_trigger',\n doc: 'Segment-entry trigger routes.',\n columns: { workflow: text, hash: text, segment: text },\n primaryKey: ['workflow'],\n indexes: [['segment']],\n }),\n segmentMember: table({\n name: 'segment_member',\n doc: 'Who is in each deployed segment and since when: kept by the scheduled sweep, and for trigger-routed segments on every event too.',\n columns: { segment: text, userId: text, ts },\n primaryKey: ['segment', 'userId'],\n }),\n segmentStart: table({\n name: 'segment_start',\n doc: 'Workflows a segment entry still has to start for a member: written with the membership, removed as each one starts, so a failed start is retried by the next evaluation instead of being hidden behind the membership.',\n columns: { segment: text, workflow: text, userId: text, ts },\n primaryKey: ['segment', 'workflow', 'userId'],\n }),\n segmentSnapshot: table({\n name: 'segment_snapshot',\n doc: \"Each segment's size at every sweep, the Studio's sparklines.\",\n columns: { segment: text, takenAt: ts, size: integer },\n primaryKey: ['segment', 'takenAt'],\n }),\n entry: table({\n name: 'entry',\n doc: 'Entry ledger: the (workflow, user) primary key is the once policy.',\n columns: { workflow: text, userId: text, instanceId: text, hash: text, status: text, ts },\n primaryKey: ['workflow', 'userId'],\n indexes: [['instanceId']],\n }),\n subscription: table({\n name: 'subscription',\n doc: 'wait_until wake routing; the primary key makes repeated subscribes idempotent.',\n columns: { userId: text, event: text, wakeHandle: text, ts },\n primaryKey: ['wakeHandle', 'event'],\n indexes: [['userId', 'event']],\n }),\n execution: table({\n name: 'execution',\n doc: 'Durable executions whose start is an asynchronous submit (AWS Lambda durable functions): written with the entry, so a lost submit response is retried with the same name, target and payload instead of starting a second execution; and the wake callback the instance is parked on now. Unused on Cloudflare.',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n name: text,\n target: text,\n input: text,\n createdAt: ts,\n submittedAt: nullable(\n ts,\n 'The runtime accepted the start; null while it may still have to be retried.'\n ),\n arn: nullable(text),\n unresolvedAt: nullable(\n ts,\n 'Retries ran out before the runtime confirmed the start: whether it runs is unknown, and nothing retries it.'\n ),\n callbackId: nullable(text, 'The callback the current wait parks on; a wake completes it.'),\n generation: integer,\n deadlineAt: nullable(ts),\n checkedAt: nullable(\n ts,\n \"When maintenance last compared the runtime's status with the ledger.\"\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [['workflow', 'userId'], ['submittedAt'], ['userId']],\n }),\n deployment: table({\n name: 'deployment',\n doc: \"The Worker version whose compiled bundle the routes came from, one row (`scope` = 'routes'): journeyWorker applies its own bundle only over an older version, so one still serving during a rollout cannot put its routes back. A forced `POST /bundle/deploy` leaves it as it is.\",\n columns: { scope: text, versionId: text, versionCreatedAt: ts, appliedAt: ts },\n primaryKey: ['scope'],\n }),\n ir: table({\n name: 'ir',\n doc: 'Workflow bodies (WorkflowIR), content-addressed.',\n columns: { contentHash: text, ir: jsonb },\n primaryKey: ['contentHash'],\n }),\n instance: table({\n name: 'instance',\n doc: 'One row per journey instance, written when it enters: the node it sits on or last passed (none when a guard ended it before the first), and how it ended (`running` until then; `goal` is a conversion, at `finished_at`).',\n columns: {\n instanceId: text,\n workflow: text,\n userId: text,\n enteredAt: ts,\n nodeId: nullable(text),\n nodeAt: nullable(ts),\n status: text,\n finishedAt: nullable(ts),\n convertedAt: nullable(\n ts,\n 'When the goal was met, whatever the status: with `exitOnMatch: false` a converted instance ends `completed`.'\n ),\n },\n primaryKey: ['instanceId'],\n indexes: [\n ['workflow', 'status'],\n ['workflow', 'finishedAt'],\n ],\n }),\n message: table({\n name: 'message',\n doc: \"One row per message copy (a push fans out per device). The idempotency key is the engine's copy identity, so a retried step finds the row it wrote; `channel` is where the copy landed (`email`, `push_ios`, `push_android`).\",\n columns: {\n id: uuid,\n idempotencyKey: text,\n workflow: text,\n instanceId: text,\n nodeId: text,\n template: text,\n channel: text,\n userId: text,\n sentAt: ts,\n deliveredAt: nullable(ts, 'The transport accepted it.'),\n deviceDeliveredAt: nullable(\n ts,\n \"A push: the device reported it received (iOS's notification service extension, a browser's service worker).\"\n ),\n undeliveredReason: nullable(\n text,\n 'The transport settled it without delivering (a dead push token).'\n ),\n openedAt: nullable(ts, 'First open of any kind, by a person or a machine.'),\n humanOpenedAt: nullable(ts, 'First open a person made.'),\n clickedAt: nullable(ts),\n humanClickedAt: nullable(ts),\n unsubscribedAt: nullable(ts),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n uniqueIndexes: [['idempotencyKey']],\n indexes: [['workflow', 'sentAt'], ['instanceId']],\n }),\n messageLink: table({\n name: 'message_link',\n doc: \"A sent email's links in document order: the tracked URL names a message and a position, and the redirect goes only to the URL recorded here.\",\n columns: { messageId: uuid, position: integer, url: text },\n primaryKey: ['messageId', 'position'],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageOpen: table({\n name: 'message_open',\n doc: \"Every open-pixel load or push tap, classified when recorded: `machine` when no person caused it, with the deciding rule in `reason`; the request's network (never its address) kept for revisiting the rules.\",\n columns: {\n id: uuid,\n messageId: uuid,\n openedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n messageClick: table({\n name: 'message_click',\n doc: 'Every click on a tracked link, classified like opens.',\n columns: {\n id: uuid,\n messageId: uuid,\n position: integer,\n url: text,\n clickedAt: ts,\n machine: boolean,\n reason: nullable(text),\n userAgent: nullable(text),\n asn: nullable(integer),\n asOrganization: nullable(text),\n country: nullable(text),\n },\n primaryKey: ['id'],\n uuidV7: 'id',\n indexes: [['messageId']],\n references: [{ columns: ['messageId'], table: 'message', foreignColumns: ['id'] }],\n }),\n} satisfies Record<string, TableSpec>;\n\nexport type TableKey = keyof typeof TABLES;\n\n/** The engine's schema when the config names none. */\nexport const DEFAULT_SCHEMA = 'workflow';\n\n/** `eventId` → `event_id`. */\nexport function columnName(key: string): string {\n return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n\nconst quote = (name: string) => `\"${name.replace(/\"/g, '\"\"')}\"`;\n\nconst SQL_TYPES: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'integer',\n boolean: 'boolean',\n};\n\n/**\n * Plain Postgres DDL for every table — for a host without a migration tool, and the tests. A\n * host that migrates with drizzle generates `renderDrizzleSchema` instead and lets drizzle-kit\n * write the migration.\n */\nexport function renderPostgresDdl(schema = DEFAULT_SCHEMA): string {\n const s = quote(schema);\n const out = [`CREATE SCHEMA IF NOT EXISTS ${s};`];\n for (const spec of Object.values(TABLES)) {\n const lines = Object.entries(spec.columns).map(\n ([key, column]) =>\n ` ${quote(columnName(key))} ${SQL_TYPES[column.type]}${column.nullable ? '' : ' NOT NULL'}`\n );\n lines.push(` PRIMARY KEY (${spec.primaryKey.map((k) => quote(columnName(k))).join(', ')})`);\n if (spec.uuidV7 !== undefined) {\n lines.push(\n ` CONSTRAINT ${quote(`${spec.name}_uuid_v7`)} CHECK (substring(${quote(columnName(spec.uuidV7))}::text, 15, 1) = '7')`\n );\n }\n for (const ref of spec.references ?? []) {\n const target = TABLES[ref.table as TableKey];\n lines.push(\n ` FOREIGN KEY (${ref.columns.map((k) => quote(columnName(k))).join(', ')}) REFERENCES ${s}.${quote(target.name)} (${ref.foreignColumns.map((k) => quote(columnName(k))).join(', ')})`\n );\n }\n out.push(`CREATE TABLE IF NOT EXISTS ${s}.${quote(spec.name)} (\\n${lines.join(',\\n')}\\n);`);\n const index = (unique: boolean) => (keys: readonly string[]) => {\n const cols = keys.map(columnName);\n out.push(\n `CREATE ${unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS ${quote(`${spec.name}_${cols.join('_')}_index`)} ON ${s}.${quote(spec.name)} (${cols.map(quote).join(', ')});`\n );\n };\n (spec.uniqueIndexes ?? []).forEach(index(true));\n (spec.indexes ?? []).forEach(index(false));\n }\n return `${out.join('\\n')}\\n`;\n}\n\nconst DRIZZLE_TYPES: Record<ColumnType, (column: string) => string> = {\n uuid: (c) => `uuid('${c}')`,\n text: (c) => `text('${c}')`,\n jsonb: (c) => `jsonb('${c}')`,\n timestamptz: (c) => `timestamp('${c}', { mode: 'string', withTimezone: true })`,\n integer: (c) => `integer('${c}')`,\n boolean: (c) => `boolean('${c}')`,\n};\n\nconst exportName = (key: string) => `workflow${key.charAt(0).toUpperCase()}${key.slice(1)}`;\n\n/**\n * The tables as one drizzle file: `export const workflowSchema = pgSchema(<schema>)` and every\n * table as `workflowSchema.table(...)`. The host re-exports it from its drizzle schema and\n * generates the migration with drizzle-kit, so the migration history stays the host's. The\n * output is deterministic — regenerate and diff it in CI to catch an SDK upgrade that changed the\n * tables.\n */\nexport function renderDrizzleSchema(schema = DEFAULT_SCHEMA): string {\n const specs = Object.entries(TABLES) as [TableKey, TableSpec][];\n const used = new Set<string>(['pgSchema']);\n const body: string[] = [];\n let needsSql = false;\n for (const [key, spec] of specs) {\n const columns = Object.entries(spec.columns).map(([prop, column]) => {\n used.add(column.type === 'timestamptz' ? 'timestamp' : column.type);\n const doc = column.doc === undefined ? '' : ` /** ${column.doc} */\\n`;\n const key =\n spec.primaryKey.length === 1 && spec.primaryKey[0] === prop ? '.primaryKey()' : '';\n return `${doc} ${prop}: ${DRIZZLE_TYPES[column.type](columnName(prop))}${key}${column.nullable ? '' : '.notNull()'},`;\n });\n const extras: string[] = [];\n if (spec.primaryKey.length > 1) {\n used.add('primaryKey');\n extras.push(\n `primaryKey({ columns: [${spec.primaryKey.map((k) => `table.${k}`).join(', ')}] })`\n );\n }\n for (const keys of spec.uniqueIndexes ?? []) {\n used.add('uniqueIndex');\n extras.push(`uniqueIndex().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const keys of spec.indexes ?? []) {\n used.add('index');\n extras.push(`index().on(${keys.map((k) => `table.${k}`).join(', ')})`);\n }\n for (const ref of spec.references ?? []) {\n used.add('foreignKey');\n extras.push(\n `foreignKey({ columns: [${ref.columns.map((k) => `table.${k}`).join(', ')}], foreignColumns: [${ref.foreignColumns.map((k) => `${exportName(ref.table)}.${k}`).join(', ')}] })`\n );\n }\n if (spec.uuidV7 !== undefined) {\n used.add('check');\n needsSql = true;\n extras.push(\n `check('${spec.name}_uuid_v7', sql\\`substring(\\${table.${spec.uuidV7}}::text, 15, 1) = '7'\\`)`\n );\n }\n const third =\n extras.length === 0\n ? ''\n : `,\\n (table) => [\\n${extras.map((e) => ` ${e},`).join('\\n')}\\n ]`;\n body.push(\n `/** ${spec.doc} */\\nexport const ${exportName(key)} = workflowSchema.table(\\n '${spec.name}',\\n {\\n${columns.join('\\n')}\\n }${third}\\n);`\n );\n }\n const imports = [...used].sort();\n return [\n '// Generated by `workflow-ui generate` from @shware/workflow — do not edit; regenerate after',\n '// upgrading the SDK and let drizzle-kit write the migration.',\n ...(needsSql ? [\"import { sql } from 'drizzle-orm';\"] : []),\n `import {\\n${imports.map((name) => ` ${name},`).join('\\n')}\\n} from 'drizzle-orm/pg-core';`,\n '',\n `export const workflowSchema = pgSchema('${schema}');`,\n '',\n body.join('\\n\\n'),\n '',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
package/dist/templates.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.cjs","names":["z"],"sources":["../src/templates.ts"],"sourcesContent":["import type { Block, KnownBlock } from '@slack/types';\nimport type { RESTPostAPIWebhookWithTokenJSONBody } from 'discord-api-types/v10';\nimport * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EmailAddress, assertEmailMetadata } from './email';\nimport type { ScalarIR } from './ir';\n\n/**\n * Template definitions — `emailTemplate`, `pushTemplate`, `discordTemplate` and `slackTemplate`. One\n * object per template, identified by its own `id` (unique per channel): workflows take the\n * definition itself (`.email(welcome, props)`), the config registers it, and the IR, the manifest,\n * the senders and the studio all find it by that id. Its Zod `props` schema is the only source of the\n * props type: the DSL's `.email()/.push()` arguments, the `preview`, and the\n * `component`/`content` parameter are all derived from it, and every render\n * parses the props against it first (types alone check nothing at run time).\n *\n * Kept free of React and of any renderer: the definitions are plain data plus\n * a function, so the DSL, the Worker and the studio can all import them; the\n * React rendering lives in `@shware/workflow/react-email`.\n *\n * Supported schemas (v1): a strict object (`z.strictObject`) whose fields are\n * string / number / boolean — optionally as enums or literals, optionally\n * `.optional()`, with the built-in checks (min, email, …). That is exactly\n * what IR message props can carry, and it keeps input and output identical, so\n * producer, studio and sender never disagree about a transformed value.\n * Anything else (nested objects, arrays, null, default/transform/coerce,\n * custom refinements) is rejected when the template is declared.\n */\n\n/** A strict Zod object schema of scalar fields — the accepted shape of `props`. */\nexport type PropsSchema = core.$ZodObject;\n\n/** What a push template's `content` returns: the notification itself. */\nexport interface PushContent {\n title: string;\n body: string;\n /** Rich image URL (iOS attachment / Android BigPicture). */\n image?: string;\n /** Where a tap leads: the page a browser push opens (the site's root when absent). */\n url?: string;\n}\n\n/** A Discord webhook message (`discord-api-types`): content, embeds, username, allowed mentions… */\nexport type DiscordContent = RESTPostAPIWebhookWithTokenJSONBody;\n\n/** A Slack incoming-webhook message: the fallback `text` and, usually, Block Kit `blocks`. */\nexport interface SlackContent {\n /** Shown in notifications and wherever blocks cannot be; required even with blocks. */\n text: string;\n blocks?: (KnownBlock | Block)[];\n username?: string;\n icon_emoji?: string;\n icon_url?: string;\n mrkdwn?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n}\n\n/** The channels a template can be declared for. */\nexport type TemplateChannel = 'email' | 'push' | 'discord' | 'slack';\n\ninterface TemplateBase<S extends PropsSchema> {\n /**\n * Wire identity, unique per channel: message nodes, the template manifest, every send and the\n * studio's preview links name the template by it. Not something to rename for readability —\n * `title` is; the property a registry holds it under is only a JavaScript name.\n */\n readonly id: string;\n /** Display name in the studio (not the email subject, not the notification title). */\n readonly title: string;\n readonly description?: string | undefined;\n readonly props: S;\n /** Sample props for the studio; parsed against `props` when the template is declared. */\n readonly preview: core.input<S>;\n}\n\n/**\n * Who an email comes from and where it leads: declared on each template, never inherited from a\n * global sender. Metadata, not props — the component never receives it; its layout reads the\n * resolved unsubscribe link with `useEmailContext()` (`@shware/workflow/email-context`).\n */\nexport interface EmailMetadata {\n readonly from: EmailAddress;\n /** Where replies go; left out, the email has no Reply-To (it is not `from`, and not inherited). */\n readonly replyTo?: EmailAddress | undefined;\n /** This template's unsubscribe link; left out, the config's `email.unsubscribeUrl` (if any). */\n readonly unsubscribeUrl?: string | undefined;\n}\n\nexport interface EmailTemplate<S extends PropsSchema = PropsSchema>\n extends TemplateBase<S>, EmailMetadata {\n readonly kind: 'email-template';\n readonly version: 1;\n readonly subject: string;\n /** The email body: a component over the parsed props (rendered by `@shware/workflow/react-email`). */\n readonly component: (props: Readonly<core.output<S>>) => unknown;\n}\n\nexport interface PushTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'push-template';\n readonly version: 1;\n /** The notification, from the parsed props. Synchronous and pure; its result is checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => PushContent;\n}\n\n/**\n * A message posted to a team channel through an incoming webhook (Discord, Slack). The webhook\n * URL is a secret, so it lives in the Worker's environment and the template reads it from there\n * at send time: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`, with `Env` the host's\n * zod-validated environment (`journeyWorker({ env: schema })`). The studio never calls it.\n */\ninterface WebhookTemplateBase<S extends PropsSchema, Env> extends TemplateBase<S> {\n readonly version: 1;\n // A method, so a template typed by the host's Env still fits a registry of any Env\n webhook(env: Env): string | undefined;\n}\n\nexport interface DiscordTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'discord-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => DiscordContent;\n}\n\nexport interface SlackTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'slack-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => SlackContent;\n}\n\n/** Strips a Promise return out of the accepted callback types: async content is rejected at the type level. */\ntype Sync<T> = T extends Promise<unknown> ? never : T;\n\nexport interface EmailTemplateDefinition<S extends PropsSchema> {\n id: string;\n title: string;\n description?: string;\n from: EmailAddress;\n replyTo?: EmailAddress;\n unsubscribeUrl?: string;\n props: S;\n subject: string;\n preview: NoInfer<core.input<S>>;\n component: (props: NoInfer<Readonly<core.output<S>>>) => unknown;\n}\n\nexport interface PushTemplateDefinition<S extends PropsSchema, R> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<R> & PushContent;\n}\n\nexport interface WebhookTemplateDefinition<S extends PropsSchema, Env, C> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n /** The webhook URL, read from the Worker's environment when a message is sent. */\n webhook: (env: Env) => string | undefined;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<C>;\n}\n\n/** Declare a Discord webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function discordTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, DiscordContent>\n): DiscordTemplate<S, Env> {\n assertTemplateId(definition.id, 'discordTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'discord', definition.title);\n return { kind: 'discord-template', version: 1, ...definition };\n}\n\n/** Declare a Slack webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function slackTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, SlackContent>\n): SlackTemplate<S, Env> {\n assertTemplateId(definition.id, 'slackTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'slack', definition.title);\n return { kind: 'slack-template', version: 1, ...definition };\n}\n\n/**\n * Declare an email template. Throws when `from` is missing or invalid, `replyTo` or `unsubscribeUrl`\n * is invalid, `props` uses an unsupported schema or `preview` does not parse.\n */\nexport function emailTemplate<S extends PropsSchema>(\n definition: EmailTemplateDefinition<S>\n): EmailTemplate<S> {\n assertTemplateId(definition.id, 'emailTemplate');\n assertEmailMetadata(definition, `emailTemplate('${definition.id}')`);\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'email', definition.title);\n return { kind: 'email-template', version: 1, ...definition };\n}\n\n/** Declare a push template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function pushTemplate<S extends PropsSchema, R extends PushContent>(\n definition: PushTemplateDefinition<S, R>\n): PushTemplate<S> {\n assertTemplateId(definition.id, 'pushTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'push', definition.title);\n return { kind: 'push-template', version: 1, ...definition } as unknown as PushTemplate<S>;\n}\n\nfunction assertTemplateId(id: unknown, factory: string): void {\n if (!isTemplateId(id)) {\n throw new TypeError(`${factory}(): 'id' must be a non-empty string`);\n }\n}\n\nfunction isTemplateId(id: unknown): id is string {\n return typeof id === 'string' && id.trim() !== '';\n}\n\nexport function isEmailTemplate(value: unknown): value is EmailTemplate {\n return isMarked(value, 'email-template');\n}\n\nexport function isPushTemplate(value: unknown): value is PushTemplate {\n return isMarked(value, 'push-template');\n}\n\nexport function isDiscordTemplate(value: unknown): value is DiscordTemplate {\n return isMarked(value, 'discord-template');\n}\n\nexport function isSlackTemplate(value: unknown): value is SlackTemplate {\n return isMarked(value, 'slack-template');\n}\n\n/** Any declared template. */\nexport type AnyTemplate = EmailTemplate | PushTemplate | DiscordTemplate | SlackTemplate;\n\nexport const CHANNEL_OF: Record<AnyTemplate['kind'], TemplateChannel> = {\n 'email-template': 'email',\n 'push-template': 'push',\n 'discord-template': 'discord',\n 'slack-template': 'slack',\n};\n\nfunction isMarked(value: unknown, kind: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === kind &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/** Any template definition, whatever its channel. */\nexport function isTemplate(value: unknown): value is AnyTemplate {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.hasOwn(CHANNEL_OF, (value as { kind?: unknown }).kind as string) &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/* --------------------------------- registry -------------------------------- */\n\n/**\n * The template registry: per channel, the definitions the project registers. Each channel is a\n * plain object whose property names are JavaScript names only — a template is identified by its\n * own `id` (`indexTemplates`).\n */\nexport interface TemplateRegistry {\n readonly email?: Readonly<Record<string, EmailTemplate>>;\n readonly push?: Readonly<Record<string, PushTemplate>>;\n /** Discord webhook templates, each typed by the host's Env its `webhook` reads. */\n readonly discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>>;\n readonly slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>>;\n}\n\n/** The template a channel's registry holds. */\nexport interface ChannelTemplate {\n email: EmailTemplate;\n push: PushTemplate;\n discord: DiscordTemplate;\n slack: SlackTemplate;\n}\n\nconst IS_TEMPLATE: Record<TemplateChannel, (value: unknown) => boolean> = {\n email: isEmailTemplate,\n push: isPushTemplate,\n discord: isDiscordTemplate,\n slack: isSlackTemplate,\n};\n\n/**\n * A channel's registry by template id — how everything that turns an id back into a definition\n * (the compiler, the validator, the renderers, the senders, the studio) looks it up. Throws when\n * an entry is not a template of the channel, has no id, or shares its id with another entry: an id\n * is registered once per channel, so two registrations (even of the same object) are an error,\n * not a silent last-wins. Insertion order is the registry's.\n */\nexport function indexTemplates<C extends TemplateChannel>(\n channel: C,\n registry: Readonly<Record<string, unknown>> | undefined\n): ReadonlyMap<string, ChannelTemplate[C]> {\n const index = new Map<string, ChannelTemplate[C]>();\n const paths = new Map<string, string>();\n for (const [name, entry] of Object.entries(registry ?? {})) {\n const path = `templates.${channel}.${name}`;\n if (!IS_TEMPLATE[channel](entry)) {\n throw new TypeError(`${path} is not a ${channel} template`);\n }\n const id = (entry as AnyTemplate).id;\n if (!isTemplateId(id)) throw new TypeError(`${path} has no id`);\n const previous = paths.get(id);\n if (previous !== undefined) {\n throw new TypeError(\n `${channel} template id '${id}' is registered twice: ${previous} and ${path}`\n );\n }\n paths.set(id, path);\n index.set(id, entry as ChannelTemplate[C]);\n }\n return index;\n}\n\n/** A registered template by id; throws naming the channel and id when there is none. */\nexport function lookupTemplate<T>(index: ReadonlyMap<string, T>, channel: string, id: string): T {\n const template = index.get(id);\n if (template === undefined) throw new Error(`${channel} template '${id}' is not registered`);\n return template;\n}\n\n/* ------------------------------- validation -------------------------------- */\n\n/** One failed field, with the dotted path the studio and the logs show. */\nexport interface TemplateIssue {\n path: string;\n message: string;\n}\n\n/**\n * A template refused its input (`phase: 'props'` — the component/content was\n * never called) or its content function returned something that is not a\n * notification (`phase: 'content'` — called, but nothing is sent). Never a\n * provider failure: a sender must not read it as a dead device token.\n */\nexport class TemplateValidationError extends Error {\n override readonly name = 'TemplateValidationError';\n constructor(\n readonly channel: TemplateChannel,\n readonly template: string,\n readonly phase: 'props' | 'content',\n readonly issues: TemplateIssue[]\n ) {\n super(\n `${channel} template '${template}' ${phase === 'props' ? 'rejected its props' : 'returned invalid content'}: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\nfunction issuesOf(error: core.$ZodError): TemplateIssue[] {\n return error.issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }));\n}\n\n/**\n * Parse message props against a template's schema and return the parsed value\n * — the only value anything downstream may use. Throws TemplateValidationError.\n */\nexport function parseTemplateProps<S extends PropsSchema>(\n template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>,\n props: Record<string, unknown>\n): core.output<S> {\n const channel = CHANNEL_OF[template.kind];\n const result = z.safeParse(template.props, props);\n if (!result.success) {\n throw new TemplateValidationError(channel, template.id, 'props', issuesOf(result.error));\n }\n return result.data;\n}\n\nconst pushContentSchema = z.strictObject({\n title: z.string(),\n body: z.string(),\n image: z.optional(z.string()),\n url: z.optional(z.string()),\n});\n\n/** Run a push template's content over parsed props and check the notification it returns. */\nexport function renderPushContent(\n template: PushTemplate,\n props: Record<string, unknown>\n): PushContent {\n const parsed = parseTemplateProps(template, props);\n const output: unknown = template.content(parsed);\n // A Promise (async content smuggled past the types) is an object without title/body: refused here\n const result = z.safeParse(pushContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('push', template.id, 'content', issuesOf(result.error));\n }\n return result.data;\n}\n\n/**\n * What a Discord webhook accepts at all: something to show (content, an embed, a poll or\n * components) and content within Discord's 2000 characters. The rest of the body is Discord's to\n * judge; its types come from `discord-api-types`.\n */\nconst discordContentSchema = z.looseObject({\n content: z.optional(z.string().check(z.maxLength(2000))),\n embeds: z.optional(z.array(z.looseObject({}))),\n});\n\nconst slackContentSchema = z.looseObject({\n text: z.string().check(z.minLength(1)),\n blocks: z.optional(z.array(z.looseObject({ type: z.string() }))),\n});\n\n/** Run a Discord template's content over parsed props and check the message it returns. */\nexport function renderDiscordContent(\n template: DiscordTemplate,\n props: Record<string, unknown>\n): DiscordContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(discordContentSchema, output);\n const empty =\n result.success &&\n !result.data.content &&\n (result.data.embeds?.length ?? 0) === 0 &&\n (output as { poll?: unknown; components?: unknown[] }).poll === undefined &&\n ((output as { components?: unknown[] }).components?.length ?? 0) === 0;\n if (!result.success || empty) {\n const issues = result.success\n ? [{ path: '', message: 'a Discord message needs content, an embed, a poll or components' }]\n : issuesOf(result.error);\n throw new TemplateValidationError('discord', template.id, 'content', issues);\n }\n return output as DiscordContent;\n}\n\n/** Run a Slack template's content over parsed props and check the message it returns. */\nexport function renderSlackContent(\n template: SlackTemplate,\n props: Record<string, unknown>\n): SlackContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(slackContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('slack', template.id, 'content', issuesOf(result.error));\n }\n return output as SlackContent;\n}\n\nfunction parsePreview(\n schema: PropsSchema,\n preview: unknown,\n channel: TemplateChannel,\n title: string\n) {\n const result = z.safeParse(schema, preview);\n if (!result.success) {\n throw new TemplateValidationError(\n channel,\n title,\n 'props',\n issuesOf(result.error).map((i) => Object.assign(i, { message: `preview: ${i.message}` }))\n );\n }\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\nconst SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'enum']);\n\n/** Throws when `schema` is outside the v1 subset (see the header). */\nexport function assertSupportedSchema(schema: unknown, where: string): void {\n const fail = (reason: string): never => {\n throw new TypeError(`template '${where}': unsupported props schema — ${reason}`);\n };\n const def = (schema as core.$ZodType | undefined)?._zod?.def;\n if (def?.type !== 'object') fail('props must be a z.strictObject({...})');\n const object = def as core.$ZodObjectDef;\n if (object.catchall?._zod.def.type !== 'never') {\n fail(\n 'props must be strict (z.strictObject): unknown fields would be dropped or passed through'\n );\n }\n rejectCustomChecks(object, where, fail);\n for (const [field, value] of Object.entries(object.shape)) {\n let fieldDef = value._zod.def;\n if (fieldDef.type === 'optional') {\n // The wrapper carries checks of its own (`.optional().overwrite(...)`): refuse them too\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n fieldDef = (fieldDef as core.$ZodOptionalDef).innerType._zod.def;\n }\n if (fieldDef.type === 'literal') {\n const values = (fieldDef as core.$ZodLiteralDef<core.util.Literal>).values;\n if (!values.every((v) => ['string', 'number', 'boolean'].includes(typeof v))) {\n fail(`field '${field}' is a literal of a non-scalar value`);\n }\n } else if (!SCALAR_TYPES.has(fieldDef.type)) {\n fail(\n `field '${field}' is ${fieldDef.type}; only string, number, boolean, enum and literal fields (optionally .optional()) are supported`\n );\n }\n if ((fieldDef as { coerce?: boolean }).coerce === true) {\n fail(`field '${field}' coerces its input (z.coerce), which would change what the props mean`);\n }\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n }\n}\n\n/**\n * Checks the subset refuses: `custom` runs arbitrary code (`.refine()`), and\n * `overwrite` changes the value (`.overwrite()`, `.trim()`, `.toLowerCase()`…)\n * — the props are parsed more than once on the way to a send (the validator,\n * then the renderer), so a value-changing check would apply twice and the\n * email would differ from the preview.\n */\nfunction rejectCustomChecks(def: core.$ZodTypeDef, where: string, fail: (reason: string) => never) {\n for (const check of def.checks ?? []) {\n if (check._zod.def.check === 'custom') {\n fail(`'${where}' uses a custom refinement, which the template subset does not support`);\n }\n if (check._zod.def.check === 'overwrite') {\n fail(\n `'${where}' changes the value (.overwrite, .trim, .toLowerCase, …), which the template subset does not support`\n );\n }\n }\n}\n\n/** The scalar props a template accepts, as the IR resolves them before parsing. */\nexport type ResolvedProps = Record<string, ScalarIR | undefined>;\n\n/**\n * The engine's MessageValidator over the template registries: the message's\n * props are parsed by its template (the parsed value is what gets sent); a\n * push's content is also run once and checked, so an invalid notification\n * stops the send before any device; Discord and Slack content likewise. Channels without a\n * registry here (sms, …) pass through to their app-supplied senders.\n */\nexport function registryValidator(registry: TemplateRegistry): {\n validate(message: { channel: string; template: string; props: ResolvedProps }): ResolvedProps;\n} {\n const email = indexTemplates('email', registry.email);\n const push = indexTemplates('push', registry.push);\n const discord = indexTemplates('discord', registry.discord);\n const slack = indexTemplates('slack', registry.slack);\n return {\n validate({ channel, template, props }) {\n if (channel === 'email') {\n const entry = lookupTemplate(email, 'email', template);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'push') {\n const entry = lookupTemplate(push, 'push', template);\n renderPushContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'discord') {\n const entry = lookupTemplate(discord, 'discord', template);\n renderDiscordContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'slack') {\n const entry = lookupTemplate(slack, 'slack', template);\n renderSlackContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n return props;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"templates.cjs","names":["z"],"sources":["../src/templates.ts"],"sourcesContent":["import type { Block, KnownBlock } from '@slack/types';\nimport type { RESTPostAPIWebhookWithTokenJSONBody } from 'discord-api-types/v10';\nimport * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EmailAddress, assertEmailMetadata } from './email';\nimport type { ScalarIR } from './ir';\n\n/**\n * Template definitions — `emailTemplate`, `pushTemplate`, `discordTemplate` and `slackTemplate`. One\n * object per template, identified by its own `id` (unique per channel): workflows take the\n * definition itself (`.email(welcome, props)`), the config registers it, and the IR, the manifest,\n * the senders and the studio all find it by that id. Its Zod `props` schema is the only source of the\n * props type: the DSL's `.email()/.push()` arguments, the `preview`, and the\n * `component`/`content` parameter are all derived from it, and every render\n * parses the props against it first (types alone check nothing at run time).\n *\n * Kept free of React and of any renderer: the definitions are plain data plus\n * a function, so the DSL, the Worker and the studio can all import them; the\n * React rendering lives in `@shware/workflow/react-email`.\n *\n * Supported schemas (v1): a strict object (`z.strictObject`) whose fields are\n * string / number / boolean — optionally as enums or literals, optionally\n * `.optional()`, with the built-in checks (min, email, …). That is exactly\n * what IR message props can carry, and it keeps input and output identical, so\n * producer, studio and sender never disagree about a transformed value.\n * Anything else (nested objects, arrays, null, default/transform/coerce,\n * custom refinements) is rejected when the template is declared.\n */\n\n/** A strict Zod object schema of scalar fields — the accepted shape of `props`. */\nexport type PropsSchema = core.$ZodObject;\n\n/** What a push template's `content` returns: the notification itself. */\nexport interface PushContent {\n title: string;\n body: string;\n /** Rich image URL (iOS attachment / Android BigPicture). */\n image?: string;\n /** Where a tap leads: the page a browser push opens (the site's root when absent). */\n url?: string;\n}\n\n/** A Discord webhook message (`discord-api-types`): content, embeds, username, allowed mentions… */\nexport type DiscordContent = RESTPostAPIWebhookWithTokenJSONBody;\n\n/** A Slack incoming-webhook message: the fallback `text` and, usually, Block Kit `blocks`. */\nexport interface SlackContent {\n /** Shown in notifications and wherever blocks cannot be; required even with blocks. */\n text: string;\n blocks?: (KnownBlock | Block)[];\n username?: string;\n icon_emoji?: string;\n icon_url?: string;\n mrkdwn?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n}\n\n/** The channels a template can be declared for. */\nexport type TemplateChannel = 'email' | 'push' | 'discord' | 'slack';\n\ninterface TemplateBase<S extends PropsSchema> {\n /**\n * Wire identity, unique per channel: message nodes, the template manifest, every send and the\n * studio's preview links name the template by it. Not something to rename for readability —\n * `title` is; the property a registry holds it under is only a JavaScript name.\n */\n readonly id: string;\n /** Display name in the studio (not the email subject, not the notification title). */\n readonly title: string;\n readonly description?: string | undefined;\n readonly props: S;\n /** Sample props for the studio; parsed against `props` when the template is declared. */\n readonly preview: core.input<S>;\n}\n\n/**\n * Who an email comes from and where it leads: declared on each template, never inherited from a\n * global sender. Metadata, not props — the component never receives it; its layout reads the\n * resolved unsubscribe link with `useEmailContext()` (`@shware/workflow/email-context`).\n */\nexport interface EmailMetadata {\n readonly from: EmailAddress;\n /** Where replies go; left out, the email has no Reply-To (it is not `from`, and not inherited). */\n readonly replyTo?: EmailAddress | undefined;\n /** This template's unsubscribe link; left out, the config's `email.unsubscribeUrl` (if any). */\n readonly unsubscribeUrl?: string | undefined;\n}\n\nexport interface EmailTemplate<S extends PropsSchema = PropsSchema>\n extends TemplateBase<S>, EmailMetadata {\n readonly kind: 'email-template';\n readonly version: 1;\n readonly subject: string;\n /** The email body: a component over the parsed props (rendered by `@shware/workflow/react-email`). */\n readonly component: (props: Readonly<core.output<S>>) => unknown;\n}\n\nexport interface PushTemplate<S extends PropsSchema = PropsSchema> extends TemplateBase<S> {\n readonly kind: 'push-template';\n readonly version: 1;\n /** The notification, from the parsed props. Synchronous and pure; its result is checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => PushContent;\n}\n\n/**\n * A message posted to a team channel through an incoming webhook (Discord, Slack). The webhook\n * URL is a secret, so it lives in the Worker's environment and the template reads it from there\n * at send time: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`, with `Env` the host's\n * zod-validated environment (`journeyWorker({ env: schema })`). The studio never calls it.\n */\ninterface WebhookTemplateBase<S extends PropsSchema, Env> extends TemplateBase<S> {\n readonly version: 1;\n // A method, so a template typed by the host's Env still fits a registry of any Env\n webhook(env: Env): string | undefined;\n}\n\nexport interface DiscordTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'discord-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => DiscordContent;\n}\n\nexport interface SlackTemplate<\n S extends PropsSchema = PropsSchema,\n Env = Record<string, unknown>,\n> extends WebhookTemplateBase<S, Env> {\n readonly kind: 'slack-template';\n /** The message, from the parsed props. Synchronous and pure; checked before sending. */\n readonly content: (props: Readonly<core.output<S>>) => SlackContent;\n}\n\n/** Strips a Promise return out of the accepted callback types: async content is rejected at the type level. */\ntype Sync<T> = T extends Promise<unknown> ? never : T;\n\nexport interface EmailTemplateDefinition<S extends PropsSchema> {\n id: string;\n title: string;\n description?: string;\n from: EmailAddress;\n replyTo?: EmailAddress;\n unsubscribeUrl?: string;\n props: S;\n subject: string;\n preview: NoInfer<core.input<S>>;\n component: (props: NoInfer<Readonly<core.output<S>>>) => unknown;\n}\n\nexport interface PushTemplateDefinition<S extends PropsSchema, R> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<R> & PushContent;\n}\n\nexport interface WebhookTemplateDefinition<S extends PropsSchema, Env, C> {\n id: string;\n title: string;\n description?: string;\n props: S;\n preview: NoInfer<core.input<S>>;\n /** The webhook URL, read from the Worker's environment when a message is sent. */\n webhook: (env: Env) => string | undefined;\n content: (props: NoInfer<Readonly<core.output<S>>>) => Sync<C>;\n}\n\n/** Declare a Discord webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function discordTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, DiscordContent>\n): DiscordTemplate<S, Env> {\n assertTemplateId(definition.id, 'discordTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'discord', definition.title);\n return { kind: 'discord-template', version: 1, ...definition };\n}\n\n/** Declare a Slack webhook template. Throws when `props` is unsupported or `preview` does not parse. */\nexport function slackTemplate<S extends PropsSchema, Env = Record<string, unknown>>(\n definition: WebhookTemplateDefinition<S, Env, SlackContent>\n): SlackTemplate<S, Env> {\n assertTemplateId(definition.id, 'slackTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'slack', definition.title);\n return { kind: 'slack-template', version: 1, ...definition };\n}\n\n/**\n * Declare an email template. Throws when `from` is missing or invalid, `replyTo` or `unsubscribeUrl`\n * is invalid, `props` uses an unsupported schema or `preview` does not parse.\n */\nexport function emailTemplate<S extends PropsSchema>(\n definition: EmailTemplateDefinition<S>\n): EmailTemplate<S> {\n assertTemplateId(definition.id, 'emailTemplate');\n assertEmailMetadata(definition, `emailTemplate('${definition.id}')`);\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'email', definition.title);\n return { kind: 'email-template', version: 1, ...definition };\n}\n\n/** Declare a push template. Throws when `props` uses an unsupported schema or `preview` does not parse. */\nexport function pushTemplate<S extends PropsSchema, R extends PushContent>(\n definition: PushTemplateDefinition<S, R>\n): PushTemplate<S> {\n assertTemplateId(definition.id, 'pushTemplate');\n assertSupportedSchema(definition.props, definition.title);\n parsePreview(definition.props, definition.preview, 'push', definition.title);\n return { kind: 'push-template', version: 1, ...definition } as unknown as PushTemplate<S>;\n}\n\nfunction assertTemplateId(id: unknown, factory: string): void {\n if (!isTemplateId(id)) {\n throw new TypeError(`${factory}(): 'id' must be a non-empty string`);\n }\n}\n\nfunction isTemplateId(id: unknown): id is string {\n return typeof id === 'string' && id.trim() !== '';\n}\n\nexport function isEmailTemplate(value: unknown): value is EmailTemplate {\n return isMarked(value, 'email-template');\n}\n\nexport function isPushTemplate(value: unknown): value is PushTemplate {\n return isMarked(value, 'push-template');\n}\n\nexport function isDiscordTemplate(value: unknown): value is DiscordTemplate {\n return isMarked(value, 'discord-template');\n}\n\nexport function isSlackTemplate(value: unknown): value is SlackTemplate {\n return isMarked(value, 'slack-template');\n}\n\n/** Any declared template. */\nexport type AnyTemplate = EmailTemplate | PushTemplate | DiscordTemplate | SlackTemplate;\n\nexport const CHANNEL_OF: Record<AnyTemplate['kind'], TemplateChannel> = {\n 'email-template': 'email',\n 'push-template': 'push',\n 'discord-template': 'discord',\n 'slack-template': 'slack',\n};\n\nfunction isMarked(value: unknown, kind: string): boolean {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === kind &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/** Any template definition, whatever its channel. */\nexport function isTemplate(value: unknown): value is AnyTemplate {\n return (\n typeof value === 'object' &&\n value !== null &&\n Object.hasOwn(CHANNEL_OF, (value as { kind?: unknown }).kind as string) &&\n (value as { version?: unknown }).version === 1\n );\n}\n\n/* --------------------------------- registry -------------------------------- */\n\n/**\n * The template registry: per channel, the definitions the project registers. Each channel is a\n * plain object whose property names are JavaScript names only — a template is identified by its\n * own `id` (`indexTemplates`).\n */\nexport interface TemplateRegistry {\n readonly email?: Readonly<Record<string, EmailTemplate>>;\n readonly push?: Readonly<Record<string, PushTemplate>>;\n /** Discord webhook templates, each typed by the host's Env its `webhook` reads. */\n readonly discord?: Readonly<Record<string, DiscordTemplate<PropsSchema, never>>>;\n readonly slack?: Readonly<Record<string, SlackTemplate<PropsSchema, never>>>;\n}\n\n/** The template a channel's registry holds. */\nexport interface ChannelTemplate {\n email: EmailTemplate;\n push: PushTemplate;\n discord: DiscordTemplate;\n slack: SlackTemplate;\n}\n\nconst IS_TEMPLATE: Record<TemplateChannel, (value: unknown) => boolean> = {\n email: isEmailTemplate,\n push: isPushTemplate,\n discord: isDiscordTemplate,\n slack: isSlackTemplate,\n};\n\n/**\n * A channel's registry by template id — how everything that turns an id back into a definition\n * (the compiler, the validator, the renderers, the senders, the studio) looks it up. Throws when\n * an entry is not a template of the channel, has no id, or shares its id with another entry: an id\n * is registered once per channel, so two registrations (even of the same object) are an error,\n * not a silent last-wins. Insertion order is the registry's.\n */\nexport function indexTemplates<C extends TemplateChannel>(\n channel: C,\n registry: Readonly<Record<string, unknown>> | undefined\n): ReadonlyMap<string, ChannelTemplate[C]> {\n const index = new Map<string, ChannelTemplate[C]>();\n const paths = new Map<string, string>();\n for (const [name, entry] of Object.entries(registry ?? {})) {\n const path = `templates.${channel}.${name}`;\n if (!IS_TEMPLATE[channel](entry)) {\n throw new TypeError(`${path} is not a ${channel} template`);\n }\n const id = (entry as AnyTemplate).id;\n if (!isTemplateId(id)) throw new TypeError(`${path} has no id`);\n const previous = paths.get(id);\n if (previous !== undefined) {\n throw new TypeError(\n `${channel} template id '${id}' is registered twice: ${previous} and ${path}`\n );\n }\n paths.set(id, path);\n index.set(id, entry as ChannelTemplate[C]);\n }\n return index;\n}\n\n/** A registered template by id; throws naming the channel and id when there is none. */\nexport function lookupTemplate<T>(index: ReadonlyMap<string, T>, channel: string, id: string): T {\n const template = index.get(id);\n if (template === undefined) throw new Error(`${channel} template '${id}' is not registered`);\n return template;\n}\n\n/* ------------------------------- validation -------------------------------- */\n\n/** One failed field, with the dotted path the studio and the logs show. */\nexport interface TemplateIssue {\n path: string;\n message: string;\n}\n\n/**\n * A template refused its input (`phase: 'props'` — the component/content was\n * never called) or its content function returned something that is not a\n * notification (`phase: 'content'` — called, but nothing is sent). Never a\n * provider failure: a sender must not read it as a dead device token.\n */\nexport class TemplateValidationError extends Error {\n override readonly name = 'TemplateValidationError';\n constructor(\n readonly channel: TemplateChannel,\n readonly template: string,\n readonly phase: 'props' | 'content',\n readonly issues: TemplateIssue[]\n ) {\n super(\n `${channel} template '${template}' ${phase === 'props' ? 'rejected its props' : 'returned invalid content'}: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\nfunction issuesOf(error: core.$ZodError): TemplateIssue[] {\n return error.issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }));\n}\n\n/**\n * Parse message props against a template's schema and return the parsed value\n * — the only value anything downstream may use. Throws TemplateValidationError.\n */\nexport function parseTemplateProps<S extends PropsSchema>(\n template: EmailTemplate<S> | PushTemplate<S> | DiscordTemplate<S> | SlackTemplate<S>,\n props: Record<string, unknown>\n): core.output<S> {\n const channel = CHANNEL_OF[template.kind];\n const result = z.safeParse(template.props, props);\n if (!result.success) {\n throw new TemplateValidationError(channel, template.id, 'props', issuesOf(result.error));\n }\n return result.data;\n}\n\nconst pushContentSchema = z.strictObject({\n title: z.string(),\n body: z.string(),\n image: z.optional(z.string()),\n url: z.optional(z.string()),\n});\n\n/** Run a push template's content over parsed props and check the notification it returns. */\nexport function renderPushContent(\n template: PushTemplate,\n props: Record<string, unknown>\n): PushContent {\n const parsed = parseTemplateProps(template, props);\n const output: unknown = template.content(parsed);\n // A Promise (async content smuggled past the types) is an object without title/body: refused here\n const result = z.safeParse(pushContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('push', template.id, 'content', issuesOf(result.error));\n }\n return result.data;\n}\n\n/**\n * What a Discord webhook accepts at all: something to show (content, an embed, a poll or\n * components) and content within Discord's 2000 characters. The rest of the body is Discord's to\n * judge; its types come from `discord-api-types`.\n */\nconst discordContentSchema = z.looseObject({\n content: z.optional(z.string().check(z.maxLength(2000))),\n embeds: z.optional(z.array(z.looseObject({}))),\n});\n\nconst slackContentSchema = z.looseObject({\n text: z.string().check(z.minLength(1)),\n blocks: z.optional(z.array(z.looseObject({ type: z.string() }))),\n});\n\n/** Run a Discord template's content over parsed props and check the message it returns. */\nexport function renderDiscordContent(\n template: DiscordTemplate,\n props: Record<string, unknown>\n): DiscordContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(discordContentSchema, output);\n const empty =\n result.success &&\n !result.data.content &&\n (result.data.embeds?.length ?? 0) === 0 &&\n (output as { poll?: unknown; components?: unknown[] }).poll === undefined &&\n ((output as { components?: unknown[] }).components?.length ?? 0) === 0;\n if (!result.success || empty) {\n const issues = result.success\n ? [{ path: '', message: 'a Discord message needs content, an embed, a poll or components' }]\n : issuesOf(result.error);\n throw new TemplateValidationError('discord', template.id, 'content', issues);\n }\n return output as DiscordContent;\n}\n\n/** Run a Slack template's content over parsed props and check the message it returns. */\nexport function renderSlackContent(\n template: SlackTemplate,\n props: Record<string, unknown>\n): SlackContent {\n const output: unknown = template.content(parseTemplateProps(template, props));\n const result = z.safeParse(slackContentSchema, output);\n if (!result.success) {\n throw new TemplateValidationError('slack', template.id, 'content', issuesOf(result.error));\n }\n return output as SlackContent;\n}\n\nfunction parsePreview(\n schema: PropsSchema,\n preview: unknown,\n channel: TemplateChannel,\n title: string\n) {\n const result = z.safeParse(schema, preview);\n if (!result.success) {\n throw new TemplateValidationError(\n channel,\n title,\n 'props',\n issuesOf(result.error).map((i) => Object.assign(i, { message: `preview: ${i.message}` }))\n );\n }\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\nconst SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'enum']);\n\n/** Throws when `schema` is outside the v1 subset (see the header). */\nexport function assertSupportedSchema(schema: unknown, where: string): void {\n const fail = (reason: string): never => {\n throw new TypeError(`template '${where}': unsupported props schema — ${reason}`);\n };\n const def = (schema as { _zod?: { def?: core.$ZodTypeDef } } | null | undefined)?._zod?.def;\n if (def?.type !== 'object') fail('props must be a z.strictObject({...})');\n const object = def as core.$ZodObjectDef;\n if (object.catchall?._zod.def.type !== 'never') {\n fail(\n 'props must be strict (z.strictObject): unknown fields would be dropped or passed through'\n );\n }\n rejectCustomChecks(object, where, fail);\n for (const [field, value] of Object.entries(object.shape)) {\n let fieldDef = value._zod.def;\n if (fieldDef.type === 'optional') {\n // The wrapper carries checks of its own (`.optional().overwrite(...)`): refuse them too\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n fieldDef = (fieldDef as core.$ZodOptionalDef).innerType._zod.def;\n }\n if (fieldDef.type === 'literal') {\n const values = (fieldDef as core.$ZodLiteralDef<core.util.Literal>).values;\n if (!values.every((v) => ['string', 'number', 'boolean'].includes(typeof v))) {\n fail(`field '${field}' is a literal of a non-scalar value`);\n }\n } else if (!SCALAR_TYPES.has(fieldDef.type)) {\n fail(\n `field '${field}' is ${fieldDef.type}; only string, number, boolean, enum and literal fields (optionally .optional()) are supported`\n );\n }\n if ((fieldDef as { coerce?: boolean }).coerce === true) {\n fail(`field '${field}' coerces its input (z.coerce), which would change what the props mean`);\n }\n rejectCustomChecks(fieldDef, `${where}.${field}`, fail);\n }\n}\n\n/**\n * Checks the subset refuses: `custom` runs arbitrary code (`.refine()`), and\n * `overwrite` changes the value (`.overwrite()`, `.trim()`, `.toLowerCase()`…)\n * — the props are parsed more than once on the way to a send (the validator,\n * then the renderer), so a value-changing check would apply twice and the\n * email would differ from the preview.\n */\nfunction rejectCustomChecks(def: core.$ZodTypeDef, where: string, fail: (reason: string) => never) {\n for (const check of def.checks ?? []) {\n if (check._zod.def.check === 'custom') {\n fail(`'${where}' uses a custom refinement, which the template subset does not support`);\n }\n if (check._zod.def.check === 'overwrite') {\n fail(\n `'${where}' changes the value (.overwrite, .trim, .toLowerCase, …), which the template subset does not support`\n );\n }\n }\n}\n\n/** The scalar props a template accepts, as the IR resolves them before parsing. */\nexport type ResolvedProps = Record<string, ScalarIR | undefined>;\n\n/**\n * The engine's MessageValidator over the template registries: the message's\n * props are parsed by its template (the parsed value is what gets sent); a\n * push's content is also run once and checked, so an invalid notification\n * stops the send before any device; Discord and Slack content likewise. Channels without a\n * registry here (sms, …) pass through to their app-supplied senders.\n */\nexport function registryValidator(registry: TemplateRegistry): {\n validate(message: { channel: string; template: string; props: ResolvedProps }): ResolvedProps;\n} {\n const email = indexTemplates('email', registry.email);\n const push = indexTemplates('push', registry.push);\n const discord = indexTemplates('discord', registry.discord);\n const slack = indexTemplates('slack', registry.slack);\n return {\n validate({ channel, template, props }) {\n if (channel === 'email') {\n const entry = lookupTemplate(email, 'email', template);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'push') {\n const entry = lookupTemplate(push, 'push', template);\n renderPushContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'discord') {\n const entry = lookupTemplate(discord, 'discord', template);\n renderDiscordContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n if (channel === 'slack') {\n const entry = lookupTemplate(slack, 'slack', template);\n renderSlackContent(entry, props);\n return parseTemplateProps(entry, props) as ResolvedProps;\n }\n return props;\n },\n };\n}\n"],"mappings":"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"}
|