@shware/workflow 2.0.1 → 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.
Files changed (228) hide show
  1. package/README.md +96 -2
  2. package/dist/apns-relay.cjs +4 -17
  3. package/dist/apns-relay.cjs.map +1 -1
  4. package/dist/apns-relay.mjs +4 -17
  5. package/dist/apns-relay.mjs.map +1 -1
  6. package/dist/aws/engine-step.cjs +104 -0
  7. package/dist/aws/engine-step.cjs.map +1 -0
  8. package/dist/aws/engine-step.d.cts +28 -0
  9. package/dist/aws/engine-step.d.cts.map +1 -0
  10. package/dist/aws/engine-step.d.mts +28 -0
  11. package/dist/aws/engine-step.d.mts.map +1 -0
  12. package/dist/aws/engine-step.mjs +102 -0
  13. package/dist/aws/engine-step.mjs.map +1 -0
  14. package/dist/aws/function-url.cjs +62 -0
  15. package/dist/aws/function-url.cjs.map +1 -0
  16. package/dist/aws/function-url.d.cts +38 -0
  17. package/dist/aws/function-url.d.cts.map +1 -0
  18. package/dist/aws/function-url.d.mts +38 -0
  19. package/dist/aws/function-url.d.mts.map +1 -0
  20. package/dist/aws/function-url.mjs +59 -0
  21. package/dist/aws/function-url.mjs.map +1 -0
  22. package/dist/aws/index.cjs +160 -0
  23. package/dist/aws/index.cjs.map +1 -0
  24. package/dist/aws/index.d.cts +62 -0
  25. package/dist/aws/index.d.cts.map +1 -0
  26. package/dist/aws/index.d.mts +62 -0
  27. package/dist/aws/index.d.mts.map +1 -0
  28. package/dist/aws/index.mjs +148 -0
  29. package/dist/aws/index.mjs.map +1 -0
  30. package/dist/aws/instances.cjs +176 -0
  31. package/dist/aws/instances.cjs.map +1 -0
  32. package/dist/aws/instances.d.cts +75 -0
  33. package/dist/aws/instances.d.cts.map +1 -0
  34. package/dist/aws/instances.d.mts +75 -0
  35. package/dist/aws/instances.d.mts.map +1 -0
  36. package/dist/aws/instances.mjs +172 -0
  37. package/dist/aws/instances.mjs.map +1 -0
  38. package/dist/aws/lambda-api.cjs +89 -0
  39. package/dist/aws/lambda-api.cjs.map +1 -0
  40. package/dist/aws/lambda-api.d.cts +43 -0
  41. package/dist/aws/lambda-api.d.cts.map +1 -0
  42. package/dist/aws/lambda-api.d.mts +43 -0
  43. package/dist/aws/lambda-api.d.mts.map +1 -0
  44. package/dist/aws/lambda-api.mjs +88 -0
  45. package/dist/aws/lambda-api.mjs.map +1 -0
  46. package/dist/client.d.cts +1 -1
  47. package/dist/client.d.mts +1 -1
  48. package/dist/cloudflare/bindings.cjs +19 -0
  49. package/dist/cloudflare/bindings.cjs.map +1 -1
  50. package/dist/cloudflare/bindings.d.cts +37 -0
  51. package/dist/cloudflare/bindings.d.cts.map +1 -1
  52. package/dist/cloudflare/bindings.d.mts +37 -0
  53. package/dist/cloudflare/bindings.d.mts.map +1 -1
  54. package/dist/cloudflare/bindings.mjs +18 -1
  55. package/dist/cloudflare/bindings.mjs.map +1 -1
  56. package/dist/cloudflare/index.cjs +32 -28
  57. package/dist/cloudflare/index.d.cts +8 -6
  58. package/dist/cloudflare/index.d.mts +8 -6
  59. package/dist/cloudflare/index.mjs +12 -10
  60. package/dist/cloudflare/runner.cjs +27 -76
  61. package/dist/cloudflare/runner.cjs.map +1 -1
  62. package/dist/cloudflare/runner.d.cts +7 -2
  63. package/dist/cloudflare/runner.d.cts.map +1 -1
  64. package/dist/cloudflare/runner.d.mts +7 -2
  65. package/dist/cloudflare/runner.d.mts.map +1 -1
  66. package/dist/cloudflare/runner.mjs +26 -75
  67. package/dist/cloudflare/runner.mjs.map +1 -1
  68. package/dist/cloudflare/worker.cjs +22 -203
  69. package/dist/cloudflare/worker.cjs.map +1 -1
  70. package/dist/cloudflare/worker.d.cts +10 -56
  71. package/dist/cloudflare/worker.d.cts.map +1 -1
  72. package/dist/cloudflare/worker.d.mts +10 -56
  73. package/dist/cloudflare/worker.d.mts.map +1 -1
  74. package/dist/cloudflare/worker.mjs +20 -199
  75. package/dist/cloudflare/worker.mjs.map +1 -1
  76. package/dist/email-sender.cjs +62 -0
  77. package/dist/email-sender.cjs.map +1 -0
  78. package/dist/email-sender.d.cts +70 -0
  79. package/dist/email-sender.d.cts.map +1 -0
  80. package/dist/email-sender.d.mts +70 -0
  81. package/dist/email-sender.d.mts.map +1 -0
  82. package/dist/email-sender.mjs +60 -0
  83. package/dist/email-sender.mjs.map +1 -0
  84. package/dist/email.cjs +21 -0
  85. package/dist/email.cjs.map +1 -1
  86. package/dist/email.d.cts +6 -0
  87. package/dist/email.d.cts.map +1 -1
  88. package/dist/email.d.mts +6 -0
  89. package/dist/email.d.mts.map +1 -1
  90. package/dist/email.mjs +21 -1
  91. package/dist/email.mjs.map +1 -1
  92. package/dist/engine/index.cjs +1 -1
  93. package/dist/engine/index.mjs +1 -1
  94. package/dist/engine/interpreter.cjs +1 -1
  95. package/dist/engine/interpreter.cjs.map +1 -1
  96. package/dist/engine/interpreter.mjs +1 -1
  97. package/dist/engine/interpreter.mjs.map +1 -1
  98. package/dist/engine/ports.d.cts +6 -1
  99. package/dist/engine/ports.d.cts.map +1 -1
  100. package/dist/engine/ports.d.mts +6 -1
  101. package/dist/engine/ports.d.mts.map +1 -1
  102. package/dist/{cloudflare → host}/channels.cjs +13 -11
  103. package/dist/host/channels.cjs.map +1 -0
  104. package/dist/{cloudflare → host}/channels.d.cts +8 -3
  105. package/dist/host/channels.d.cts.map +1 -0
  106. package/dist/{cloudflare → host}/channels.d.mts +8 -3
  107. package/dist/host/channels.d.mts.map +1 -0
  108. package/dist/{cloudflare → host}/channels.mjs +9 -7
  109. package/dist/host/channels.mjs.map +1 -0
  110. package/dist/host/execute.cjs +82 -0
  111. package/dist/host/execute.cjs.map +1 -0
  112. package/dist/host/execute.d.cts +41 -0
  113. package/dist/host/execute.d.cts.map +1 -0
  114. package/dist/host/execute.d.mts +41 -0
  115. package/dist/host/execute.d.mts.map +1 -0
  116. package/dist/host/execute.mjs +81 -0
  117. package/dist/host/execute.mjs.map +1 -0
  118. package/dist/host/journey-host.cjs +214 -0
  119. package/dist/host/journey-host.cjs.map +1 -0
  120. package/dist/host/journey-host.d.cts +105 -0
  121. package/dist/host/journey-host.d.cts.map +1 -0
  122. package/dist/host/journey-host.d.mts +105 -0
  123. package/dist/host/journey-host.d.mts.map +1 -0
  124. package/dist/host/journey-host.mjs +210 -0
  125. package/dist/host/journey-host.mjs.map +1 -0
  126. package/dist/{cloudflare → host}/router.cjs +28 -9
  127. package/dist/host/router.cjs.map +1 -0
  128. package/dist/{cloudflare → host}/router.d.cts +2 -2
  129. package/dist/host/router.d.cts.map +1 -0
  130. package/dist/{cloudflare → host}/router.d.mts +2 -2
  131. package/dist/host/router.d.mts.map +1 -0
  132. package/dist/{cloudflare → host}/router.mjs +28 -9
  133. package/dist/host/router.mjs.map +1 -0
  134. package/dist/host/senders.cjs +63 -0
  135. package/dist/host/senders.cjs.map +1 -0
  136. package/dist/host/senders.d.cts +31 -0
  137. package/dist/host/senders.d.cts.map +1 -0
  138. package/dist/host/senders.d.mts +31 -0
  139. package/dist/host/senders.d.mts.map +1 -0
  140. package/dist/host/senders.mjs +59 -0
  141. package/dist/host/senders.mjs.map +1 -0
  142. package/dist/http2-client.cjs +88 -0
  143. package/dist/http2-client.cjs.map +1 -0
  144. package/dist/http2-client.d.cts +44 -0
  145. package/dist/http2-client.d.cts.map +1 -0
  146. package/dist/http2-client.d.mts +44 -0
  147. package/dist/http2-client.d.mts.map +1 -0
  148. package/dist/http2-client.mjs +86 -0
  149. package/dist/http2-client.mjs.map +1 -0
  150. package/dist/index.cjs +1 -0
  151. package/dist/index.d.cts +2 -2
  152. package/dist/index.d.mts +2 -2
  153. package/dist/index.mjs +2 -2
  154. package/dist/react-email.cjs.map +1 -1
  155. package/dist/react-email.d.cts +1 -2
  156. package/dist/react-email.d.cts.map +1 -1
  157. package/dist/react-email.d.mts +1 -2
  158. package/dist/react-email.d.mts.map +1 -1
  159. package/dist/react-email.mjs.map +1 -1
  160. package/dist/store/d1.cjs +1 -0
  161. package/dist/store/d1.cjs.map +1 -1
  162. package/dist/store/d1.d.cts.map +1 -1
  163. package/dist/store/d1.d.mts.map +1 -1
  164. package/dist/store/d1.mjs +1 -0
  165. package/dist/store/d1.mjs.map +1 -1
  166. package/dist/store/index.cjs.map +1 -1
  167. package/dist/store/index.d.cts +63 -0
  168. package/dist/store/index.d.cts.map +1 -1
  169. package/dist/store/index.d.mts +63 -0
  170. package/dist/store/index.d.mts.map +1 -1
  171. package/dist/store/index.mjs.map +1 -1
  172. package/dist/store/postgres.cjs +143 -2
  173. package/dist/store/postgres.cjs.map +1 -1
  174. package/dist/store/postgres.d.cts +4 -1
  175. package/dist/store/postgres.d.cts.map +1 -1
  176. package/dist/store/postgres.d.mts +4 -1
  177. package/dist/store/postgres.d.mts.map +1 -1
  178. package/dist/store/postgres.mjs +143 -2
  179. package/dist/store/postgres.mjs.map +1 -1
  180. package/dist/store/tables.cjs +26 -0
  181. package/dist/store/tables.cjs.map +1 -1
  182. package/dist/store/tables.d.cts +1 -0
  183. package/dist/store/tables.d.cts.map +1 -1
  184. package/dist/store/tables.d.mts +1 -0
  185. package/dist/store/tables.d.mts.map +1 -1
  186. package/dist/store/tables.mjs +26 -0
  187. package/dist/store/tables.mjs.map +1 -1
  188. package/dist/transports/cloudflare-email.cjs +59 -0
  189. package/dist/transports/cloudflare-email.cjs.map +1 -0
  190. package/dist/transports/cloudflare-email.d.cts +38 -0
  191. package/dist/transports/cloudflare-email.d.cts.map +1 -0
  192. package/dist/transports/cloudflare-email.d.mts +38 -0
  193. package/dist/transports/cloudflare-email.d.mts.map +1 -0
  194. package/dist/transports/cloudflare-email.mjs +57 -0
  195. package/dist/transports/cloudflare-email.mjs.map +1 -0
  196. package/dist/transports/ses.cjs +91 -0
  197. package/dist/transports/ses.cjs.map +1 -0
  198. package/dist/transports/ses.d.cts +39 -0
  199. package/dist/transports/ses.d.cts.map +1 -0
  200. package/dist/transports/ses.d.mts +39 -0
  201. package/dist/transports/ses.d.mts.map +1 -0
  202. package/dist/transports/ses.mjs +89 -0
  203. package/dist/transports/ses.mjs.map +1 -0
  204. package/dist/transports/sigv4.cjs +83 -0
  205. package/dist/transports/sigv4.cjs.map +1 -0
  206. package/dist/transports/sigv4.d.cts +27 -0
  207. package/dist/transports/sigv4.d.cts.map +1 -0
  208. package/dist/transports/sigv4.d.mts +27 -0
  209. package/dist/transports/sigv4.d.mts.map +1 -0
  210. package/dist/transports/sigv4.mjs +82 -0
  211. package/dist/transports/sigv4.mjs.map +1 -0
  212. package/package.json +53 -1
  213. package/dist/cloudflare/channels.cjs.map +0 -1
  214. package/dist/cloudflare/channels.d.cts.map +0 -1
  215. package/dist/cloudflare/channels.d.mts.map +0 -1
  216. package/dist/cloudflare/channels.mjs.map +0 -1
  217. package/dist/cloudflare/router.cjs.map +0 -1
  218. package/dist/cloudflare/router.d.cts.map +0 -1
  219. package/dist/cloudflare/router.d.mts.map +0 -1
  220. package/dist/cloudflare/router.mjs.map +0 -1
  221. package/dist/cloudflare/senders.cjs +0 -121
  222. package/dist/cloudflare/senders.cjs.map +0 -1
  223. package/dist/cloudflare/senders.d.cts +0 -72
  224. package/dist/cloudflare/senders.d.cts.map +0 -1
  225. package/dist/cloudflare/senders.d.mts +0 -72
  226. package/dist/cloudflare/senders.d.mts.map +0 -1
  227. package/dist/cloudflare/senders.mjs +0 -115
  228. package/dist/cloudflare/senders.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.mjs","names":[],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport type {\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport { type ColumnType, DEFAULT_SCHEMA, TABLES, type TableKey, columnName } from './tables';\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * Journey data plane on Postgres. Same contract as D1JourneyStore, native\n * types where Postgres has them (jsonb, timestamptz), and the concurrency\n * guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability. Read-only — the engine never migrates the host's database; a\n * deploy fails with these problems listed, and the fix is to regenerate the drizzle file and\n * run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const rows = await this.sql.transaction((tx) =>\n tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n )\n );\n const live = new Map(rows.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(rows.map((row) => row.table_name));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, whose unique index is what\n // makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n const rows = await this.sql.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n VALUES ($1, $2, $3, $4, ${TS('$5')}, $6::text::jsonb)\n ON CONFLICT (event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n return rows.length > 0;\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ c: number | string }>(\n `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ payload: unknown }>(\n `SELECT payload FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return rows.map((row) => asJson<Record<string, unknown>>(row.payload));\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // RFC 7386 merge-patch in one statement: jsonb `||` overwrites key by key\n // and jsonb_strip_nulls then drops the keys the patch set to null (the\n // API's only way to unset a property). Atomic in the database, so two\n // concurrent identifies cannot lose each other's keys.\n const rows = await this.sql.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props) VALUES ($1, jsonb_strip_nulls($2::text::jsonb))\n ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(props), JSON.stringify(props)]\n );\n return asJson<ProfileProps>(rows.at(0)?.props ?? {});\n }\n\n async listUserIds(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.profile} UNION SELECT DISTINCT user_id FROM ${this.t.event}`\n );\n return rows.map((row) => row.user_id);\n }\n\n listDevices(userId: string): Promise<PushDevice[]> {\n return this.sql.query<PushDevice>(\n `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`,\n [userId]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.device} (token, user_id, platform, ts) VALUES ($1, $2, $3, ${TS('$4')})\n ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts`,\n [device.token, device.userId, device.platform, device.ts]\n );\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async runningInstances(userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ instance_id: string }>(\n `SELECT instance_id FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => row.instance_id);\n }\n\n async forgetUser(userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [userId]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n this.t.suppression,\n this.t.device,\n this.t.event,\n this.t.profile,\n // The request goes with the data it asked to erase\n this.t.erasure,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);\n }\n });\n }\n\n async isSuppressed(userId: string, channel: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2`,\n [userId, channel]\n );\n return rows.length > 0;\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the null\n const rows = await this.sql.query<{ user_id: string | null }>(\n `WITH old AS (SELECT token, user_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token\n RETURNING old.user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n }));\n }\n\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n return this.sql.query<SegmentTriggerRoute>(\n `SELECT workflow, hash, segment FROM ${this.t.segmentTrigger}`\n );\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR): Promise<void> {\n return this.sql.transaction((tx) => this.swapRoutes(tx, bundle));\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle);\n return true;\n });\n }\n\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment) VALUES ($1, $2, $3)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts) VALUES ($1, $2, ${TS('$3')}) ON CONFLICT DO NOTHING`,\n [segment, userId, ts]\n );\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n VALUES ($1, $2, $3, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const inserted = await this.sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts)\n VALUES ($1, $2, $3, $4, 'running', ${TS('$5')})\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back.\n const claimed = await this.sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')}\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed' RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId]\n );\n return claimed.length > 0 ? 'reclaimed' : null;\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n for (const event of events) {\n await this.sql.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts) VALUES ($1, $2, $3, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [userId, event, handle, ts]\n );\n }\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"postgres.mjs","names":[],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport type {\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport { type ColumnType, DEFAULT_SCHEMA, TABLES, type TableKey, columnName } from './tables';\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * Journey data plane on Postgres. Same contract as D1JourneyStore, native\n * types where Postgres has them (jsonb, timestamptz), and the concurrency\n * guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n readonly executions: ExecutionRegistry;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t);\n this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability. Read-only — the engine never migrates the host's database; a\n * deploy fails with these problems listed, and the fix is to regenerate the drizzle file and\n * run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const rows = await this.sql.transaction((tx) =>\n tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n )\n );\n const live = new Map(rows.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(rows.map((row) => row.table_name));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, whose unique index is what\n // makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n const rows = await this.sql.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n VALUES ($1, $2, $3, $4, ${TS('$5')}, $6::text::jsonb)\n ON CONFLICT (event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n return rows.length > 0;\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ c: number | string }>(\n `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ payload: unknown }>(\n `SELECT payload FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return rows.map((row) => asJson<Record<string, unknown>>(row.payload));\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // RFC 7386 merge-patch in one statement: jsonb `||` overwrites key by key\n // and jsonb_strip_nulls then drops the keys the patch set to null (the\n // API's only way to unset a property). Atomic in the database, so two\n // concurrent identifies cannot lose each other's keys.\n const rows = await this.sql.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props) VALUES ($1, jsonb_strip_nulls($2::text::jsonb))\n ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(props), JSON.stringify(props)]\n );\n return asJson<ProfileProps>(rows.at(0)?.props ?? {});\n }\n\n async listUserIds(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.profile} UNION SELECT DISTINCT user_id FROM ${this.t.event}`\n );\n return rows.map((row) => row.user_id);\n }\n\n listDevices(userId: string): Promise<PushDevice[]> {\n return this.sql.query<PushDevice>(\n `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`,\n [userId]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.device} (token, user_id, platform, ts) VALUES ($1, $2, $3, ${TS('$4')})\n ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts`,\n [device.token, device.userId, device.platform, device.ts]\n );\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async runningInstances(userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ instance_id: string }>(\n `SELECT instance_id FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => row.instance_id);\n }\n\n async forgetUser(userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [userId]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n this.t.suppression,\n this.t.device,\n this.t.event,\n this.t.profile,\n // The request goes with the data it asked to erase\n this.t.erasure,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);\n }\n });\n }\n\n async isSuppressed(userId: string, channel: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2`,\n [userId, channel]\n );\n return rows.length > 0;\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the null\n const rows = await this.sql.query<{ user_id: string | null }>(\n `WITH old AS (SELECT token, user_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token\n RETURNING old.user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n }));\n }\n\n listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n return this.sql.query<SegmentTriggerRoute>(\n `SELECT workflow, hash, segment FROM ${this.t.segmentTrigger}`\n );\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR): Promise<void> {\n return this.sql.transaction((tx) => this.swapRoutes(tx, bundle));\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle);\n return true;\n });\n }\n\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment) VALUES ($1, $2, $3)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts) VALUES ($1, $2, ${TS('$3')}) ON CONFLICT DO NOTHING`,\n [segment, userId, ts]\n );\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n VALUES ($1, $2, $3, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const { execution } = entry;\n if (execution === undefined) return this.claimEntry(this.sql, entry);\n // The entry and the start it owes commit together: a crash between them would leave a\n // running entry no retry could ever start\n return this.sql.transaction(async (tx) => {\n const outcome = await this.claimEntry(tx, entry);\n if (outcome !== null) await this.recordStart(tx, entry, execution);\n return outcome;\n });\n }\n\n private async claimEntry(sql: SqlClientLike, entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const inserted = await sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts)\n VALUES ($1, $2, $3, $4, 'running', ${TS('$5')})\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back.\n const claimed = await sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')}\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed' RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId]\n );\n return claimed.length > 0 ? 'reclaimed' : null;\n }\n\n private async recordStart(\n tx: SqlClientLike,\n entry: EntryInput,\n execution: ExecutionStart\n ): Promise<void> {\n await tx.query(\n `INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)\n VALUES ($1, $2, $3, $4, $5, $6, ${TS('$7')}, 0) ON CONFLICT (instance_id) DO NOTHING`,\n [\n entry.instanceId,\n entry.workflow,\n entry.userId,\n execution.name,\n execution.target,\n execution.input,\n // The start's own age (the retry budget), not the event's ts, which the producer chose\n Date.now(),\n ]\n );\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n for (const event of events) {\n await this.sql.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts) VALUES ($1, $2, $3, ${TS('$4')}) ON CONFLICT DO NOTHING`,\n [userId, event, handle, ts]\n );\n }\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n\ninterface ExecutionRow {\n instance_id: string;\n workflow: string;\n user_id: string;\n name: string;\n target: string;\n input: string;\n // float8 comes back as a number from most drivers, as a string from some\n created_at: number | string;\n submitted_at: number | string | null;\n arn: string | null;\n unresolved_at: number | string | null;\n callback_id: string | null;\n generation: number | string;\n deadline_at: number | string | null;\n checked_at: number | string | null;\n}\n\nconst PLAIN_COLUMNS = ['instance_id', 'workflow', 'user_id', 'name', 'target', 'input', 'arn'];\nconst MORE_PLAIN = ['callback_id', 'generation'];\nconst TIME_COLUMNS = ['created_at', 'submitted_at', 'unresolved_at', 'deadline_at', 'checked_at'];\n\n/** The execution columns, timestamps as epoch ms; `alias` qualifies them in a join. */\nfunction executionColumns(alias = ''): string {\n const q = alias === '' ? '' : `${alias}.`;\n return [\n ...[...PLAIN_COLUMNS, ...MORE_PLAIN].map((column) => `${q}${column}`),\n ...TIME_COLUMNS.map(\n (column) => `(extract(epoch from ${q}${column}) * 1000)::double precision AS ${column}`\n ),\n ].join(', ');\n}\n\nconst optional = <T>(value: T | null): T | undefined => value ?? undefined;\nconst ms = (value: number | string | null): number | undefined =>\n value === null ? undefined : Number(value);\n\nfunction toRecord(row: ExecutionRow): ExecutionRecord {\n return {\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n name: row.name,\n target: row.target,\n input: row.input,\n createdAt: Number(row.created_at),\n submittedAt: ms(row.submitted_at),\n arn: optional(row.arn),\n unresolvedAt: ms(row.unresolved_at),\n callbackId: optional(row.callback_id),\n generation: Number(row.generation),\n deadlineAt: ms(row.deadline_at),\n checkedAt: ms(row.checked_at),\n };\n}\n\n/** The execution table (tables.ts): starts to confirm, and the callback each wait parks on. */\nclass PostgresExecutions implements ExecutionRegistry {\n constructor(\n private readonly sql: SqlClientLike,\n private readonly table: string,\n private readonly entry: string\n ) {}\n\n async get(instanceId: string): Promise<ExecutionRecord | undefined> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns()} FROM ${this.table} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows.length === 0 ? undefined : toRecord(rows[0]);\n }\n\n async pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]> {\n const where = ['x.submitted_at IS NULL', 'x.unresolved_at IS NULL', \"e.status = 'running'\"];\n const params: unknown[] = [];\n if (filter.workflow !== undefined) {\n params.push(filter.workflow);\n where.push(`x.workflow = $${params.length}`);\n }\n if (filter.userId !== undefined) {\n params.push(filter.userId);\n where.push(`x.user_id = $${params.length}`);\n }\n if (filter.createdBefore !== undefined) {\n params.push(filter.createdBefore);\n where.push(`x.created_at < ${TS(`$${params.length}`)}`);\n }\n params.push(filter.limit);\n // Only the start the entry still names: a reclaimed entry's old start is history\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE ${where.join(' AND ')} ORDER BY x.created_at LIMIT $${params.length}`,\n params\n );\n return rows.map(toRecord);\n }\n\n async markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET submitted_at = COALESCE(submitted_at, ${TS('$2')}), arn = COALESCE($3, arn) WHERE instance_id = $1`,\n [instanceId, at, arn ?? null]\n );\n }\n\n async markUnresolved(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET unresolved_at = ${TS('$2')} WHERE instance_id = $1 AND submitted_at IS NULL`,\n [instanceId, at]\n );\n }\n\n async setArn(instanceId: string, arn: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.table} SET arn = $2 WHERE instance_id = $1`, [\n instanceId,\n arn,\n ]);\n }\n\n async armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean> {\n // The generation test is inside the UPDATE: a retried arm of an older wait cannot overwrite\n // the callback a newer one parks on\n const rows = await this.sql.query(\n `UPDATE ${this.table} SET callback_id = $2, generation = $3,\n deadline_at = CASE WHEN $4::double precision IS NULL THEN NULL ELSE ${TS('$4')} END\n WHERE instance_id = $1 AND generation <= $3 RETURNING instance_id`,\n [instanceId, callback.id, callback.generation, callback.deadlineAt ?? null]\n );\n return rows.length > 0;\n }\n\n async clearCallback(instanceId: string, generation: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET callback_id = NULL, deadline_at = NULL WHERE instance_id = $1 AND generation = $2`,\n [instanceId, generation]\n );\n }\n\n async toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE e.status = 'running' AND x.submitted_at IS NOT NULL\n AND (x.checked_at IS NULL OR x.checked_at < ${TS('$1')})\n ORDER BY x.checked_at NULLS FIRST LIMIT $2`,\n [checkedBefore, limit]\n );\n return rows.map(toRecord);\n }\n\n async markChecked(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET checked_at = ${TS('$2')} WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n}\n"],"mappings":"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"}
@@ -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":"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"}
@@ -51,6 +51,7 @@ export declare const TABLES: {
51
51
  segmentSnapshot: TableSpec<string>;
52
52
  entry: TableSpec<string>;
53
53
  subscription: TableSpec<string>;
54
+ execution: TableSpec<string>;
54
55
  deployment: TableSpec<string>;
55
56
  ir: TableSpec<string>;
56
57
  instance: TableSpec<string>;
@@ -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;;;;;;;;;;;;;;;;;;;;;;YAkND,wBAAwB;;qBAGvB;;wBAGG,WAAW;;;;;;wBAoBX,kBAAkB;;;;;;;;wBAmDlB,oBAAoB"}
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"}
@@ -51,6 +51,7 @@ export declare const TABLES: {
51
51
  segmentSnapshot: TableSpec<string>;
52
52
  entry: TableSpec<string>;
53
53
  subscription: TableSpec<string>;
54
+ execution: TableSpec<string>;
54
55
  deployment: TableSpec<string>;
55
56
  ir: TableSpec<string>;
56
57
  instance: TableSpec<string>;
@@ -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;;;;;;;;;;;;;;;;;;;;;;YAkND,wBAAwB;;qBAGvB;;wBAGG,WAAW;;;;;;wBAoBX,kBAAkB;;;;;;;;wBAmDlB,oBAAoB"}
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"}
@@ -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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"}
@@ -0,0 +1,59 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_email = require("../email.cjs");
3
+ //#region src/transports/cloudflare-email.ts
4
+ /** The REST API refused the email: `errors` are Cloudflare's `{ code, message }` pairs. */
5
+ var CloudflareEmailError = class extends Error {
6
+ status;
7
+ errors;
8
+ name = "CloudflareEmailError";
9
+ constructor(status, errors) {
10
+ const said = errors.map((e) => `${e.code ?? "?"} ${e.message ?? ""}`.trim()).join("; ");
11
+ super(`Cloudflare Email send failed (${status})${said === "" ? "" : `: ${said}`}`);
12
+ this.status = status;
13
+ this.errors = errors;
14
+ }
15
+ };
16
+ function cloudflareEmailTransport(options) {
17
+ if (options.binding !== void 0) {
18
+ const { binding } = options;
19
+ return { async send(message) {
20
+ const id = (await binding.send(message))?.messageId;
21
+ return typeof id === "string" ? { providerMessageId: id } : {};
22
+ } };
23
+ }
24
+ const given = options;
25
+ if (typeof given.accountId !== "string" || typeof given.apiToken !== "string") throw new TypeError("cloudflareEmailTransport needs { binding } or { accountId, apiToken }");
26
+ const { accountId, apiToken } = options;
27
+ const endpoint = (options.endpoint ?? "https://api.cloudflare.com/client/v4").replace(/\/$/, "");
28
+ const fetchImpl = options.fetch ?? fetch;
29
+ return { async send(message) {
30
+ const response = await fetchImpl(`${endpoint}/accounts/${encodeURIComponent(accountId)}/email/sending/send`, {
31
+ method: "POST",
32
+ headers: {
33
+ authorization: `Bearer ${apiToken}`,
34
+ "content-type": "application/json"
35
+ },
36
+ body: JSON.stringify({
37
+ from: require_email.mimeAddress(message.from),
38
+ to: message.to,
39
+ ...message.replyTo === void 0 ? {} : { reply_to: require_email.mimeAddress(message.replyTo) },
40
+ subject: message.subject,
41
+ html: message.html
42
+ })
43
+ });
44
+ let body = {};
45
+ try {
46
+ body = await response.json();
47
+ } catch {
48
+ body = {};
49
+ }
50
+ if (!response.ok || body.success !== true) throw new CloudflareEmailError(response.status, Array.isArray(body.errors) ? body.errors : []);
51
+ const id = body.result?.message_id;
52
+ return typeof id === "string" ? { providerMessageId: id } : {};
53
+ } };
54
+ }
55
+ //#endregion
56
+ exports.CloudflareEmailError = CloudflareEmailError;
57
+ exports.cloudflareEmailTransport = cloudflareEmailTransport;
58
+
59
+ //# sourceMappingURL=cloudflare-email.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-email.cjs","names":["mimeAddress"],"sources":["../../src/transports/cloudflare-email.ts"],"sourcesContent":["import type { EmailBindingLike } from '../cloudflare/bindings';\nimport { mimeAddress } from '../email';\nimport type { EmailMessage, EmailTransport } from '../email-sender';\n\n/**\n * `@shware/workflow/transports/cloudflare-email` — Cloudflare Email Service, through the\n * Worker's `send_email` binding or, from anywhere else (a Lambda), its REST API. Both take the\n * same finished email; only the call and the way each reports a failure differ.\n */\n\nexport type CloudflareEmailTransportOptions =\n | {\n /** The Worker's `send_email` binding. */\n binding: EmailBindingLike;\n accountId?: never;\n apiToken?: never;\n }\n | {\n binding?: never;\n accountId: string;\n /** An API token allowed to send email for the account. */\n apiToken: string;\n /** Defaults to `https://api.cloudflare.com/client/v4`. */\n endpoint?: string;\n fetch?: typeof fetch;\n };\n\n/** The REST API refused the email: `errors` are Cloudflare's `{ code, message }` pairs. */\nexport class CloudflareEmailError extends Error {\n override readonly name = 'CloudflareEmailError';\n constructor(\n readonly status: number,\n readonly errors: readonly { code?: number; message?: string }[]\n ) {\n const said = errors.map((e) => `${e.code ?? '?'} ${e.message ?? ''}`.trim()).join('; ');\n super(`Cloudflare Email send failed (${status})${said === '' ? '' : `: ${said}`}`);\n }\n}\n\nexport function cloudflareEmailTransport(options: CloudflareEmailTransportOptions): EmailTransport {\n if (options.binding !== undefined) {\n const { binding } = options;\n return {\n async send(message) {\n // The binding takes the key and drops the duplicate a retry or replay produces\n const result: unknown = await binding.send(message);\n const id = (result as { messageId?: unknown } | null | undefined)?.messageId;\n return typeof id === 'string' ? { providerMessageId: id } : {};\n },\n };\n }\n // Checked at runtime too: a JS caller can pass neither\n const given = options as { accountId?: unknown; apiToken?: unknown };\n if (typeof given.accountId !== 'string' || typeof given.apiToken !== 'string') {\n throw new TypeError('cloudflareEmailTransport needs { binding } or { accountId, apiToken }');\n }\n const { accountId, apiToken } = options;\n const endpoint = (options.endpoint ?? 'https://api.cloudflare.com/client/v4').replace(/\\/$/, '');\n const fetchImpl = options.fetch ?? fetch;\n return {\n async send(message: EmailMessage) {\n const response = await fetchImpl(\n `${endpoint}/accounts/${encodeURIComponent(accountId)}/email/sending/send`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${apiToken}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n from: mimeAddress(message.from),\n to: message.to,\n ...(message.replyTo === undefined ? {} : { reply_to: mimeAddress(message.replyTo) }),\n subject: message.subject,\n html: message.html,\n }),\n }\n );\n let body: { success?: unknown; errors?: unknown; result?: { message_id?: unknown } } = {};\n try {\n body = (await response.json()) as typeof body;\n } catch {\n body = {};\n }\n if (!response.ok || body.success !== true) {\n throw new CloudflareEmailError(\n response.status,\n Array.isArray(body.errors) ? (body.errors as { code?: number; message?: string }[]) : []\n );\n }\n // No server-side de-duplication is documented for the REST API: a retry after an accepted\n // send can deliver twice (at least once, like SES)\n const id = body.result?.message_id;\n return typeof id === 'string' ? { providerMessageId: id } : {};\n },\n };\n}\n"],"mappings":";;;;AA4BA,IAAa,uBAAb,cAA0C,MAAM;CAGnC;CACA;CAHX,OAAyB;CACzB,YACE,QACA,QACA;EACA,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG,EAAE,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;EACtF,MAAM,iCAAiC,OAAO,GAAG,SAAS,KAAK,KAAK,KAAK,QAAQ;EAJxE,KAAA,SAAA;EACA,KAAA,SAAA;CAIX;AACF;AAEA,SAAgB,yBAAyB,SAA0D;CACjG,IAAI,QAAQ,YAAY,KAAA,GAAW;EACjC,MAAM,EAAE,YAAY;EACpB,OAAO,EACL,MAAM,KAAK,SAAS;GAGlB,MAAM,MAAM,MADkB,QAAQ,KAAK,OAAO,EAAA,EACiB;GACnE,OAAO,OAAO,OAAO,WAAW,EAAE,mBAAmB,GAAG,IAAI,CAAC;EAC/D,EACF;CACF;CAEA,MAAM,QAAQ;CACd,IAAI,OAAO,MAAM,cAAc,YAAY,OAAO,MAAM,aAAa,UACnE,MAAM,IAAI,UAAU,uEAAuE;CAE7F,MAAM,EAAE,WAAW,aAAa;CAChC,MAAM,YAAY,QAAQ,YAAY,uCAAA,CAAwC,QAAQ,OAAO,EAAE;CAC/F,MAAM,YAAY,QAAQ,SAAS;CACnC,OAAO,EACL,MAAM,KAAK,SAAuB;EAChC,MAAM,WAAW,MAAM,UACrB,GAAG,SAAS,YAAY,mBAAmB,SAAS,EAAE,sBACtD;GACE,QAAQ;GACR,SAAS;IACP,eAAe,UAAU;IACzB,gBAAgB;GAClB;GACA,MAAM,KAAK,UAAU;IACnB,MAAMA,cAAAA,YAAY,QAAQ,IAAI;IAC9B,IAAI,QAAQ;IACZ,GAAI,QAAQ,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,UAAUA,cAAAA,YAAY,QAAQ,OAAO,EAAE;IAClF,SAAS,QAAQ;IACjB,MAAM,QAAQ;GAChB,CAAC;EACH,CACF;EACA,IAAI,OAAmF,CAAC;EACxF,IAAI;GACF,OAAQ,MAAM,SAAS,KAAK;EAC9B,QAAQ;GACN,OAAO,CAAC;EACV;EACA,IAAI,CAAC,SAAS,MAAM,KAAK,YAAY,MACnC,MAAM,IAAI,qBACR,SAAS,QACT,MAAM,QAAQ,KAAK,MAAM,IAAK,KAAK,SAAmD,CAAC,CACzF;EAIF,MAAM,KAAK,KAAK,QAAQ;EACxB,OAAO,OAAO,OAAO,WAAW,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC/D,EACF;AACF"}