@tanstack/workflow-store-drizzle-postgres 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import { LogConflictError } from '@tanstack/workflow-core'
2
2
  import { sql } from 'drizzle-orm'
3
+ import {
4
+ getDrizzlePostgresWorkflowStoreSchemaStatements,
5
+ qualifiedTableName,
6
+ resolveDrizzlePostgresWorkflowStoreTables,
7
+ } from './schema-contract'
3
8
  import type { RunState, WorkflowEvent } from '@tanstack/workflow-core'
4
9
  import type { SQL } from 'drizzle-orm'
10
+ import type { DrizzlePostgresWorkflowStoreTables } from './schema-contract'
5
11
  import type {
6
12
  AppendEventsArgs,
7
13
  AppendEventsResult,
@@ -50,17 +56,6 @@ export interface DrizzlePostgresDatabase {
50
56
  ) => Promise<TResult>
51
57
  }
52
58
 
53
- export interface DrizzlePostgresWorkflowStoreTables {
54
- runs: string
55
- runStates: string
56
- eventLocks: string
57
- events: string
58
- timers: string
59
- signalDeliveries: string
60
- schedules: string
61
- scheduleBuckets: string
62
- }
63
-
64
59
  export interface DrizzlePostgresWorkflowStoreOptions {
65
60
  db: DrizzlePostgresDatabase
66
61
  schema?: string
@@ -72,31 +67,19 @@ export type DrizzlePostgresWorkflowStore = WorkflowExecutionStore &
72
67
  ensureSchema: () => Promise<void>
73
68
  }
74
69
 
75
- export const defaultDrizzlePostgresWorkflowStoreTables: DrizzlePostgresWorkflowStoreTables =
76
- {
77
- runs: 'workflow_runs',
78
- runStates: 'workflow_run_states',
79
- eventLocks: 'workflow_event_locks',
80
- events: 'workflow_events',
81
- timers: 'workflow_timers',
82
- signalDeliveries: 'workflow_signal_deliveries',
83
- schedules: 'workflow_schedules',
84
- scheduleBuckets: 'workflow_schedule_buckets',
85
- }
86
-
87
70
  export function createDrizzlePostgresWorkflowStore(
88
71
  options: DrizzlePostgresWorkflowStoreOptions,
89
72
  ): DrizzlePostgresWorkflowStore {
90
- const tableNames = {
91
- ...defaultDrizzlePostgresWorkflowStoreTables,
92
- ...options.tables,
93
- }
73
+ const tableNames = resolveDrizzlePostgresWorkflowStoreTables(options.tables)
94
74
  const tableSql = tableSqls(options.schema, tableNames)
95
75
  const db = options.db
96
76
 
97
77
  return {
98
78
  async ensureSchema() {
99
- for (const statement of schemaStatements(options.schema, tableNames)) {
79
+ for (const statement of getDrizzlePostgresWorkflowStoreSchemaStatements({
80
+ schema: options.schema,
81
+ tables: tableNames,
82
+ })) {
100
83
  await db.execute(sql.raw(statement))
101
84
  }
102
85
  },
@@ -165,6 +148,7 @@ export function createDrizzlePostgresWorkflowStore(
165
148
  input,
166
149
  output,
167
150
  error,
151
+ awaiting,
168
152
  waiting_for,
169
153
  pending_approval,
170
154
  created_at,
@@ -178,6 +162,7 @@ export function createDrizzlePostgresWorkflowStore(
178
162
  ${encodeJson(state.input)}::jsonb,
179
163
  ${encodeJsonOrNull(state.output)}::jsonb,
180
164
  ${encodeJsonOrNull(state.error)}::jsonb,
165
+ ${encodeJsonOrNull(state.awaiting)}::jsonb,
181
166
  ${encodeJsonOrNull(state.waitingFor)}::jsonb,
182
167
  ${encodeJsonOrNull(state.pendingApproval)}::jsonb,
183
168
  ${state.createdAt},
@@ -190,6 +175,7 @@ export function createDrizzlePostgresWorkflowStore(
190
175
  input = excluded.input,
191
176
  output = excluded.output,
192
177
  error = excluded.error,
178
+ awaiting = excluded.awaiting,
193
179
  waiting_for = excluded.waiting_for,
194
180
  pending_approval = excluded.pending_approval,
195
181
  created_at = excluded.created_at,
@@ -204,6 +190,7 @@ export function createDrizzlePostgresWorkflowStore(
204
190
  input,
205
191
  output,
206
192
  error,
193
+ awaiting,
207
194
  waiting_for,
208
195
  pending_approval,
209
196
  wake_at,
@@ -218,6 +205,7 @@ export function createDrizzlePostgresWorkflowStore(
218
205
  ${encodeJson(state.input)}::jsonb,
219
206
  ${encodeJsonOrNull(state.output)}::jsonb,
220
207
  ${encodeJsonOrNull(state.error)}::jsonb,
208
+ ${encodeJsonOrNull(state.awaiting)}::jsonb,
221
209
  ${encodeJsonOrNull(state.waitingFor)}::jsonb,
222
210
  ${encodeJsonOrNull(state.pendingApproval)}::jsonb,
223
211
  ${
@@ -235,6 +223,7 @@ export function createDrizzlePostgresWorkflowStore(
235
223
  input = excluded.input,
236
224
  output = excluded.output,
237
225
  error = excluded.error,
226
+ awaiting = excluded.awaiting,
238
227
  waiting_for = excluded.waiting_for,
239
228
  pending_approval = excluded.pending_approval,
240
229
  wake_at = excluded.wake_at,
@@ -402,6 +391,7 @@ export function createDrizzlePostgresWorkflowStore(
402
391
  update ${tableSql.runs}
403
392
  set
404
393
  status = 'paused',
394
+ awaiting = ${encodeJsonOrNull(args.awaiting)}::jsonb,
405
395
  waiting_for = ${encodeJsonOrNull(args.waitingFor)}::jsonb,
406
396
  pending_approval = ${encodeJsonOrNull(args.pendingApproval)}::jsonb,
407
397
  wake_at = ${args.wakeAt ?? null},
@@ -418,6 +408,7 @@ export function createDrizzlePostgresWorkflowStore(
418
408
  set
419
409
  status = 'finished',
420
410
  output = ${encodeJson(args.output)}::jsonb,
411
+ awaiting = null,
421
412
  waiting_for = null,
422
413
  pending_approval = null,
423
414
  wake_at = null,
@@ -435,6 +426,7 @@ export function createDrizzlePostgresWorkflowStore(
435
426
  set
436
427
  status = 'errored',
437
428
  error = ${encodeJson(args.error)}::jsonb,
429
+ awaiting = null,
438
430
  waiting_for = null,
439
431
  pending_approval = null,
440
432
  wake_at = null,
@@ -524,7 +516,7 @@ export function createDrizzlePostgresWorkflowStore(
524
516
  )
525
517
  if (existingDelivery) return { kind: 'duplicate', run }
526
518
 
527
- if (run.waitingFor?.signalName !== args.delivery.name) {
519
+ if (!isRunWaitingForSignal(run, args.delivery)) {
528
520
  return { kind: 'not-waiting', run }
529
521
  }
530
522
 
@@ -549,6 +541,7 @@ export function createDrizzlePostgresWorkflowStore(
549
541
  update ${tableSql.runs}
550
542
  set
551
543
  status = 'queued',
544
+ awaiting = null,
552
545
  waiting_for = null,
553
546
  pending_approval = null,
554
547
  wake_at = null,
@@ -578,7 +571,7 @@ export function createDrizzlePostgresWorkflowStore(
578
571
  )
579
572
  if (existingDelivery) return { kind: 'duplicate', run }
580
573
 
581
- if (run.pendingApproval?.approvalId !== args.approval.approvalId) {
574
+ if (!isRunWaitingForApproval(run, args.approval)) {
582
575
  return { kind: 'not-waiting', run }
583
576
  }
584
577
 
@@ -597,6 +590,7 @@ export function createDrizzlePostgresWorkflowStore(
597
590
  update ${tableSql.runs}
598
591
  set
599
592
  status = 'queued',
593
+ awaiting = null,
600
594
  waiting_for = null,
601
595
  pending_approval = null,
602
596
  wake_at = null,
@@ -824,6 +818,7 @@ interface RunRow {
824
818
  input: unknown
825
819
  output: unknown
826
820
  error: unknown
821
+ awaiting: unknown
827
822
  waiting_for: unknown
828
823
  pending_approval: unknown
829
824
  wake_at: number | string | null
@@ -841,6 +836,7 @@ interface RunStateRow {
841
836
  input: unknown
842
837
  output: unknown
843
838
  error: unknown
839
+ awaiting: unknown
844
840
  waiting_for: unknown
845
841
  pending_approval: unknown
846
842
  created_at: number | string
@@ -904,122 +900,6 @@ function tableSqls(
904
900
  }
905
901
  }
906
902
 
907
- function schemaStatements(
908
- schema: string | undefined,
909
- tables: DrizzlePostgresWorkflowStoreTables,
910
- ): Array<string> {
911
- const runs = qualifiedTableName(schema, tables.runs)
912
- const runStates = qualifiedTableName(schema, tables.runStates)
913
- const eventLocks = qualifiedTableName(schema, tables.eventLocks)
914
- const events = qualifiedTableName(schema, tables.events)
915
- const timers = qualifiedTableName(schema, tables.timers)
916
- const signalDeliveries = qualifiedTableName(schema, tables.signalDeliveries)
917
- const schedules = qualifiedTableName(schema, tables.schedules)
918
- const scheduleBuckets = qualifiedTableName(schema, tables.scheduleBuckets)
919
-
920
- return [
921
- ...(schema ? [`create schema if not exists ${quoteIdent(schema)}`] : []),
922
- `create table if not exists ${runs} (
923
- run_id text primary key,
924
- workflow_id text not null,
925
- workflow_version text,
926
- status text not null,
927
- input jsonb not null,
928
- output jsonb,
929
- error jsonb,
930
- waiting_for jsonb,
931
- pending_approval jsonb,
932
- wake_at bigint,
933
- lease_owner text,
934
- lease_expires_at bigint,
935
- created_at bigint not null,
936
- updated_at bigint not null
937
- )`,
938
- `create index if not exists ${quoteIdent(`${tables.runs}_status_idx`)}
939
- on ${runs} (status, updated_at)`,
940
- `create index if not exists ${quoteIdent(`${tables.runs}_lease_idx`)}
941
- on ${runs} (status, lease_expires_at)`,
942
- `create table if not exists ${runStates} (
943
- run_id text primary key,
944
- workflow_id text not null,
945
- workflow_version text,
946
- status text not null,
947
- input jsonb not null,
948
- output jsonb,
949
- error jsonb,
950
- waiting_for jsonb,
951
- pending_approval jsonb,
952
- created_at bigint not null,
953
- updated_at bigint not null
954
- )`,
955
- `create table if not exists ${eventLocks} (
956
- run_id text primary key,
957
- created_at bigint not null
958
- )`,
959
- `create table if not exists ${events} (
960
- run_id text not null,
961
- event_index integer not null,
962
- event_type text not null,
963
- step_id text,
964
- event jsonb not null,
965
- created_at bigint not null,
966
- primary key (run_id, event_index)
967
- )`,
968
- `create index if not exists ${quoteIdent(`${tables.events}_type_idx`)}
969
- on ${events} (run_id, event_type)`,
970
- `create table if not exists ${timers} (
971
- run_id text not null,
972
- signal_id text not null,
973
- workflow_id text not null,
974
- workflow_version text,
975
- wake_at bigint not null,
976
- lease_owner text,
977
- lease_expires_at bigint,
978
- primary key (run_id, signal_id)
979
- )`,
980
- `create index if not exists ${quoteIdent(`${tables.timers}_due_idx`)}
981
- on ${timers} (wake_at, lease_expires_at)`,
982
- `create table if not exists ${signalDeliveries} (
983
- run_id text not null,
984
- signal_id text not null,
985
- created_at bigint not null,
986
- primary key (run_id, signal_id)
987
- )`,
988
- `create table if not exists ${schedules} (
989
- schedule_id text primary key,
990
- workflow_id text not null,
991
- workflow_version text,
992
- schedule jsonb not null,
993
- overlap_policy text not null,
994
- input jsonb,
995
- next_fire_at bigint,
996
- enabled boolean not null,
997
- updated_at bigint not null
998
- )`,
999
- `create index if not exists ${quoteIdent(`${tables.schedules}_due_idx`)}
1000
- on ${schedules} (enabled, next_fire_at)`,
1001
- `create table if not exists ${scheduleBuckets} (
1002
- schedule_id text not null,
1003
- bucket_id text not null,
1004
- workflow_id text not null,
1005
- workflow_version text,
1006
- run_id text not null,
1007
- fire_at bigint not null,
1008
- input jsonb,
1009
- overlap_policy text not null,
1010
- status text not null,
1011
- lease_owner text,
1012
- lease_expires_at bigint,
1013
- started_at bigint,
1014
- primary key (schedule_id, bucket_id)
1015
- )`,
1016
- `create index if not exists ${quoteIdent(
1017
- `${tables.scheduleBuckets}_lease_idx`,
1018
- )}
1019
- on ${scheduleBuckets} (status, fire_at, lease_expires_at)`,
1020
- ]
1021
- }
1022
-
1023
903
  async function loadRunById(
1024
904
  db: DrizzlePostgresDatabase,
1025
905
  tables: TableSqls,
@@ -1158,6 +1038,7 @@ function runFromRow(row: RunRow): WorkflowExecution {
1158
1038
  input: decodeJson(row.input),
1159
1039
  output: decodeJsonOrUndefined(row.output),
1160
1040
  error: decodeJsonOrUndefined(row.error),
1041
+ awaiting: decodeJsonOrUndefined(row.awaiting),
1161
1042
  waitingFor: decodeJsonOrUndefined(row.waiting_for),
1162
1043
  pendingApproval: decodeJsonOrUndefined(row.pending_approval),
1163
1044
  wakeAt: numberOrUndefined(row.wake_at),
@@ -1182,6 +1063,7 @@ function runStateFromRow(row: RunStateRow): RunState {
1182
1063
  input: decodeJson(row.input),
1183
1064
  output: decodeJsonOrUndefined(row.output),
1184
1065
  error: decodeJsonOrUndefined(row.error),
1066
+ awaiting: decodeJsonOrUndefined(row.awaiting),
1185
1067
  waitingFor: decodeJsonOrUndefined(row.waiting_for),
1186
1068
  pendingApproval: decodeJsonOrUndefined(row.pending_approval),
1187
1069
  createdAt: Number(row.created_at),
@@ -1210,6 +1092,54 @@ function timerFromRow(row: TimerRow): TimerWakeup {
1210
1092
  }
1211
1093
  }
1212
1094
 
1095
+ function isRunWaitingForSignal(
1096
+ run: WorkflowExecution,
1097
+ delivery: DeliverSignalArgs['delivery'],
1098
+ ) {
1099
+ return (
1100
+ signalAwaitableMatches(run.waitingFor, delivery) ||
1101
+ run.awaiting?.some(
1102
+ (awaitable) =>
1103
+ awaitable.type === 'signal' &&
1104
+ signalAwaitableMatches(awaitable, delivery),
1105
+ ) === true
1106
+ )
1107
+ }
1108
+
1109
+ function signalAwaitableMatches(
1110
+ awaitable:
1111
+ | NonNullable<WorkflowExecution['waitingFor']>
1112
+ | Extract<
1113
+ NonNullable<WorkflowExecution['awaiting']>[number],
1114
+ {
1115
+ type: 'signal'
1116
+ }
1117
+ >
1118
+ | undefined,
1119
+ delivery: DeliverSignalArgs['delivery'],
1120
+ ) {
1121
+ return (
1122
+ awaitable?.signalName === delivery.name &&
1123
+ (delivery.stepId === undefined ||
1124
+ awaitable.stepId === undefined ||
1125
+ awaitable.stepId === delivery.stepId)
1126
+ )
1127
+ }
1128
+
1129
+ function isRunWaitingForApproval(
1130
+ run: WorkflowExecution,
1131
+ approval: DeliverApprovalArgs['approval'],
1132
+ ) {
1133
+ return (
1134
+ run.pendingApproval?.approvalId === approval.approvalId ||
1135
+ run.awaiting?.some(
1136
+ (awaitable) =>
1137
+ awaitable.type === 'approval' &&
1138
+ awaitable.approvalId === approval.approvalId,
1139
+ ) === true
1140
+ )
1141
+ }
1142
+
1213
1143
  function scheduleFromRow(row: ScheduleRow) {
1214
1144
  return {
1215
1145
  scheduleId: row.schedule_id satisfies ScheduleId,
@@ -1241,6 +1171,7 @@ function toRunSummary(row: RunRow): RunSummary {
1241
1171
  workflowId: run.workflowId,
1242
1172
  workflowVersion: run.workflowVersion,
1243
1173
  status: run.status,
1174
+ awaiting: run.awaiting,
1244
1175
  waitingFor: run.waitingFor,
1245
1176
  pendingApproval: run.pendingApproval,
1246
1177
  wakeAt: run.wakeAt,
@@ -1280,13 +1211,3 @@ function numberOrUndefined(value: number | string | null) {
1280
1211
  function getStepId(event: WorkflowEvent) {
1281
1212
  return 'stepId' in event ? event.stepId : undefined
1282
1213
  }
1283
-
1284
- function qualifiedTableName(schema: string | undefined, table: string) {
1285
- return schema
1286
- ? `${quoteIdent(schema)}.${quoteIdent(table)}`
1287
- : quoteIdent(table)
1288
- }
1289
-
1290
- function quoteIdent(identifier: string) {
1291
- return `"${identifier.replaceAll('"', '""')}"`
1292
- }
package/src/tables.ts ADDED
@@ -0,0 +1,149 @@
1
+ import {
2
+ bigint,
3
+ boolean,
4
+ index,
5
+ integer,
6
+ jsonb,
7
+ pgTable,
8
+ primaryKey,
9
+ text,
10
+ } from 'drizzle-orm/pg-core'
11
+
12
+ export const workflowSchemaMigrations = pgTable('workflow_schema_migrations', {
13
+ migrationId: text('migration_id').primaryKey(),
14
+ packageName: text('package_name').notNull(),
15
+ packageVersion: text('package_version'),
16
+ appliedAt: bigint('applied_at', { mode: 'number' }).notNull(),
17
+ })
18
+
19
+ export const workflowRuns = pgTable(
20
+ 'workflow_runs',
21
+ {
22
+ runId: text('run_id').primaryKey(),
23
+ workflowId: text('workflow_id').notNull(),
24
+ workflowVersion: text('workflow_version'),
25
+ status: text('status').notNull(),
26
+ input: jsonb('input').notNull(),
27
+ output: jsonb('output'),
28
+ error: jsonb('error'),
29
+ awaiting: jsonb('awaiting'),
30
+ waitingFor: jsonb('waiting_for'),
31
+ pendingApproval: jsonb('pending_approval'),
32
+ wakeAt: bigint('wake_at', { mode: 'number' }),
33
+ leaseOwner: text('lease_owner'),
34
+ leaseExpiresAt: bigint('lease_expires_at', { mode: 'number' }),
35
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
36
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
37
+ },
38
+ (table) => [
39
+ index('workflow_runs_status_idx').on(table.status, table.updatedAt),
40
+ index('workflow_runs_lease_idx').on(table.status, table.leaseExpiresAt),
41
+ ],
42
+ )
43
+
44
+ export const workflowRunStates = pgTable('workflow_run_states', {
45
+ runId: text('run_id').primaryKey(),
46
+ workflowId: text('workflow_id').notNull(),
47
+ workflowVersion: text('workflow_version'),
48
+ status: text('status').notNull(),
49
+ input: jsonb('input').notNull(),
50
+ output: jsonb('output'),
51
+ error: jsonb('error'),
52
+ awaiting: jsonb('awaiting'),
53
+ waitingFor: jsonb('waiting_for'),
54
+ pendingApproval: jsonb('pending_approval'),
55
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
56
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
57
+ })
58
+
59
+ export const workflowEventLocks = pgTable('workflow_event_locks', {
60
+ runId: text('run_id').primaryKey(),
61
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
62
+ })
63
+
64
+ export const workflowEvents = pgTable(
65
+ 'workflow_events',
66
+ {
67
+ runId: text('run_id').notNull(),
68
+ eventIndex: integer('event_index').notNull(),
69
+ eventType: text('event_type').notNull(),
70
+ stepId: text('step_id'),
71
+ event: jsonb('event').notNull(),
72
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
73
+ },
74
+ (table) => [
75
+ primaryKey({ columns: [table.runId, table.eventIndex] }),
76
+ index('workflow_events_type_idx').on(table.runId, table.eventType),
77
+ ],
78
+ )
79
+
80
+ export const workflowTimers = pgTable(
81
+ 'workflow_timers',
82
+ {
83
+ runId: text('run_id').notNull(),
84
+ signalId: text('signal_id').notNull(),
85
+ workflowId: text('workflow_id').notNull(),
86
+ workflowVersion: text('workflow_version'),
87
+ wakeAt: bigint('wake_at', { mode: 'number' }).notNull(),
88
+ leaseOwner: text('lease_owner'),
89
+ leaseExpiresAt: bigint('lease_expires_at', { mode: 'number' }),
90
+ },
91
+ (table) => [
92
+ primaryKey({ columns: [table.runId, table.signalId] }),
93
+ index('workflow_timers_due_idx').on(table.wakeAt, table.leaseExpiresAt),
94
+ ],
95
+ )
96
+
97
+ export const workflowSignalDeliveries = pgTable(
98
+ 'workflow_signal_deliveries',
99
+ {
100
+ runId: text('run_id').notNull(),
101
+ signalId: text('signal_id').notNull(),
102
+ createdAt: bigint('created_at', { mode: 'number' }).notNull(),
103
+ },
104
+ (table) => [primaryKey({ columns: [table.runId, table.signalId] })],
105
+ )
106
+
107
+ export const workflowSchedules = pgTable(
108
+ 'workflow_schedules',
109
+ {
110
+ scheduleId: text('schedule_id').primaryKey(),
111
+ workflowId: text('workflow_id').notNull(),
112
+ workflowVersion: text('workflow_version'),
113
+ schedule: jsonb('schedule').notNull(),
114
+ overlapPolicy: text('overlap_policy').notNull(),
115
+ input: jsonb('input'),
116
+ nextFireAt: bigint('next_fire_at', { mode: 'number' }),
117
+ enabled: boolean('enabled').notNull(),
118
+ updatedAt: bigint('updated_at', { mode: 'number' }).notNull(),
119
+ },
120
+ (table) => [
121
+ index('workflow_schedules_due_idx').on(table.enabled, table.nextFireAt),
122
+ ],
123
+ )
124
+
125
+ export const workflowScheduleBuckets = pgTable(
126
+ 'workflow_schedule_buckets',
127
+ {
128
+ scheduleId: text('schedule_id').notNull(),
129
+ bucketId: text('bucket_id').notNull(),
130
+ workflowId: text('workflow_id').notNull(),
131
+ workflowVersion: text('workflow_version'),
132
+ runId: text('run_id').notNull(),
133
+ fireAt: bigint('fire_at', { mode: 'number' }).notNull(),
134
+ input: jsonb('input'),
135
+ overlapPolicy: text('overlap_policy').notNull(),
136
+ status: text('status').notNull(),
137
+ leaseOwner: text('lease_owner'),
138
+ leaseExpiresAt: bigint('lease_expires_at', { mode: 'number' }),
139
+ startedAt: bigint('started_at', { mode: 'number' }),
140
+ },
141
+ (table) => [
142
+ primaryKey({ columns: [table.scheduleId, table.bucketId] }),
143
+ index('workflow_schedule_buckets_lease_idx').on(
144
+ table.status,
145
+ table.fireAt,
146
+ table.leaseExpiresAt,
147
+ ),
148
+ ],
149
+ )