@workflow/world-postgres 4.1.0-beta.4 → 4.1.0-beta.41

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 (53) hide show
  1. package/LICENSE.md +201 -21
  2. package/README.md +17 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +12 -12
  5. package/dist/cli.js.map +1 -1
  6. package/dist/drizzle/cbor.d.ts +38 -0
  7. package/dist/drizzle/cbor.d.ts.map +1 -0
  8. package/dist/drizzle/cbor.js +10 -0
  9. package/dist/drizzle/cbor.js.map +1 -0
  10. package/dist/drizzle/index.d.ts +3 -1
  11. package/dist/drizzle/index.d.ts.map +1 -1
  12. package/dist/drizzle/index.js.map +1 -1
  13. package/dist/drizzle/schema.d.ts +756 -59
  14. package/dist/drizzle/schema.d.ts.map +1 -1
  15. package/dist/drizzle/schema.js +68 -37
  16. package/dist/drizzle/schema.js.map +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +17 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/{boss.d.ts → message.d.ts} +6 -6
  21. package/dist/message.d.ts.map +1 -0
  22. package/dist/{boss.js → message.js} +5 -5
  23. package/dist/message.js.map +1 -0
  24. package/dist/queue.d.ts +8 -6
  25. package/dist/queue.d.ts.map +1 -1
  26. package/dist/queue.js +129 -50
  27. package/dist/queue.js.map +1 -1
  28. package/dist/storage.d.ts.map +1 -1
  29. package/dist/storage.js +854 -212
  30. package/dist/storage.js.map +1 -1
  31. package/dist/streamer.d.ts +5 -1
  32. package/dist/streamer.d.ts.map +1 -1
  33. package/dist/streamer.js +50 -9
  34. package/dist/streamer.js.map +1 -1
  35. package/package.json +21 -20
  36. package/src/drizzle/migrations/{0000_redundant_smasher.sql → 0000_cultured_the_anarchist.sql} +17 -13
  37. package/src/drizzle/migrations/0001_tricky_sersi.sql +11 -0
  38. package/src/drizzle/migrations/0002_add_expired_at.sql +2 -0
  39. package/src/drizzle/migrations/0003_add_stream_run_id.sql +3 -0
  40. package/src/drizzle/migrations/0004_remove_run_pause_status.sql +14 -0
  41. package/src/drizzle/migrations/0005_add_spec_version.sql +5 -0
  42. package/src/drizzle/migrations/0006_add_error_cbor.sql +5 -0
  43. package/src/drizzle/migrations/0007_add_waits_table.sql +13 -0
  44. package/src/drizzle/migrations/0008_migrate_pgboss_to_graphile.sql +20 -0
  45. package/src/drizzle/migrations/0009_add_is_webhook.sql +1 -0
  46. package/src/drizzle/migrations/meta/0000_snapshot.json +499 -0
  47. package/src/drizzle/migrations/meta/0001_snapshot.json +548 -0
  48. package/src/drizzle/migrations/meta/0002_snapshot.json +554 -0
  49. package/src/drizzle/migrations/meta/0003_snapshot.json +576 -0
  50. package/src/drizzle/migrations/meta/0005_snapshot.json +575 -0
  51. package/src/drizzle/migrations/meta/_journal.json +76 -0
  52. package/dist/boss.d.ts.map +0 -1
  53. package/dist/boss.js.map +0 -1
package/dist/storage.js CHANGED
@@ -1,10 +1,67 @@
1
- import { WorkflowAPIError } from '@workflow/errors';
2
- import { and, desc, eq, gt, lt, sql } from 'drizzle-orm';
1
+ import { HookNotFoundError, RunNotSupportedError, WorkflowAPIError, } from '@workflow/errors';
2
+ import { EventSchema, HookSchema, isLegacySpecVersion, requiresNewerWorld, SPEC_VERSION_CURRENT, StepSchema, validateUlidTimestamp, WorkflowRunSchema, } from '@workflow/world';
3
+ import { and, asc, desc, eq, gt, lt, notInArray, sql } from 'drizzle-orm';
3
4
  import { monotonicFactory } from 'ulid';
4
5
  import { Schema } from './drizzle/index.js';
5
6
  import { compact } from './util.js';
7
+ /**
8
+ * Parse legacy errorJson (text column with JSON-stringified StructuredError).
9
+ * Used for backwards compatibility when reading from deprecated error column.
10
+ */
11
+ function parseErrorJson(errorJson) {
12
+ if (!errorJson)
13
+ return null;
14
+ try {
15
+ const parsed = JSON.parse(errorJson);
16
+ if (typeof parsed === 'object' && parsed.message !== undefined) {
17
+ return {
18
+ message: parsed.message,
19
+ stack: parsed.stack,
20
+ code: parsed.code,
21
+ };
22
+ }
23
+ // Not a structured error object, treat as plain string
24
+ return { message: String(parsed) };
25
+ }
26
+ catch {
27
+ // Not JSON, treat as plain string error message
28
+ return { message: errorJson };
29
+ }
30
+ }
31
+ /**
32
+ * Deserialize run data, handling legacy error fields.
33
+ * The error field should already be deserialized from CBOR or fallback to errorJson.
34
+ * This function only handles very old legacy fields (errorStack, errorCode).
35
+ */
36
+ function deserializeRunError(run) {
37
+ const { errorStack, errorCode, ...rest } = run;
38
+ // If no legacy fields, return as-is (error is already a StructuredError or undefined)
39
+ if (!errorStack && !errorCode) {
40
+ return rest;
41
+ }
42
+ // Very old legacy: separate errorStack/errorCode fields
43
+ const existingError = rest.error;
44
+ return {
45
+ ...rest,
46
+ error: {
47
+ message: existingError?.message || '',
48
+ stack: existingError?.stack || errorStack,
49
+ code: existingError?.code || errorCode,
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Deserialize step data, mapping DB columns to interface fields.
55
+ * The error field should already be deserialized from CBOR or fallback to errorJson.
56
+ */
57
+ function deserializeStepError(step) {
58
+ const { startedAt, ...rest } = step;
59
+ return {
60
+ ...rest,
61
+ startedAt,
62
+ };
63
+ }
6
64
  export function createRunsStorage(drizzle) {
7
- const ulid = monotonicFactory();
8
65
  const { runs } = Schema;
9
66
  const get = drizzle
10
67
  .select()
@@ -13,51 +70,21 @@ export function createRunsStorage(drizzle) {
13
70
  .limit(1)
14
71
  .prepare('workflow_runs_get');
15
72
  return {
16
- async get(id) {
73
+ get: (async (id, params) => {
17
74
  const [value] = await get.execute({ id });
18
75
  if (!value) {
19
76
  throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 });
20
77
  }
21
- return compact(value);
22
- },
23
- async cancel(id) {
24
- // TODO: we might want to guard this for only specific statuses
25
- const [value] = await drizzle
26
- .update(Schema.runs)
27
- .set({ status: 'cancelled', completedAt: sql `now()` })
28
- .where(eq(runs.runId, id))
29
- .returning();
30
- if (!value) {
31
- throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 });
32
- }
33
- return compact(value);
34
- },
35
- async pause(id) {
36
- // TODO: we might want to guard this for only specific statuses
37
- const [value] = await drizzle
38
- .update(Schema.runs)
39
- .set({ status: 'paused' })
40
- .where(eq(runs.runId, id))
41
- .returning();
42
- if (!value) {
43
- throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 });
44
- }
45
- return compact(value);
46
- },
47
- async resume(id) {
48
- const [value] = await drizzle
49
- .update(Schema.runs)
50
- .set({ status: 'running' })
51
- .where(and(eq(runs.runId, id), eq(runs.status, 'paused')))
52
- .returning();
53
- if (!value) {
54
- throw new WorkflowAPIError(`Paused run not found: ${id}`, {
55
- status: 404,
56
- });
57
- }
58
- return compact(value);
59
- },
60
- async list(params) {
78
+ value.output ||= value.outputJson;
79
+ value.input ||= value.inputJson;
80
+ value.executionContext ||= value.executionContextJson;
81
+ value.error ||= parseErrorJson(value.errorJson);
82
+ const deserialized = deserializeRunError(compact(value));
83
+ const parsed = WorkflowRunSchema.parse(deserialized);
84
+ const resolveData = params?.resolveData ?? 'all';
85
+ return filterRunData(parsed, resolveData);
86
+ }),
87
+ list: (async (params) => {
61
88
  const limit = params?.pagination?.limit ?? 20;
62
89
  const fromCursor = params?.pagination?.cursor;
63
90
  const all = await drizzle
@@ -68,85 +95,711 @@ export function createRunsStorage(drizzle) {
68
95
  .limit(limit + 1);
69
96
  const values = all.slice(0, limit);
70
97
  const hasMore = all.length > limit;
98
+ const resolveData = params?.resolveData ?? 'all';
71
99
  return {
72
- data: values.map(compact),
100
+ data: values.map((v) => {
101
+ v.output ||= v.outputJson;
102
+ v.input ||= v.inputJson;
103
+ v.executionContext ||= v.executionContextJson;
104
+ v.error ||= parseErrorJson(v.errorJson);
105
+ const deserialized = deserializeRunError(compact(v));
106
+ const parsed = WorkflowRunSchema.parse(deserialized);
107
+ return filterRunData(parsed, resolveData);
108
+ }),
73
109
  hasMore,
74
110
  cursor: values.at(-1)?.runId ?? null,
75
111
  };
76
- },
77
- async create(data) {
78
- const runId = `wrun_${ulid()}`;
79
- const [value] = await drizzle
80
- .insert(runs)
81
- .values({
82
- runId,
83
- input: data.input,
84
- executionContext: data.executionContext,
85
- deploymentId: data.deploymentId,
86
- status: 'pending',
87
- workflowName: data.workflowName,
88
- })
89
- .onConflictDoNothing()
90
- .returning();
91
- if (!value) {
92
- throw new WorkflowAPIError(`Run ${runId} already exists`, {
93
- status: 409,
94
- });
95
- }
96
- return compact(value);
97
- },
98
- async update(id, data) {
99
- // Fetch current run to check if startedAt is already set
100
- const [currentRun] = await drizzle
101
- .select()
102
- .from(runs)
103
- .where(eq(runs.runId, id))
104
- .limit(1);
105
- if (!currentRun) {
106
- throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 });
107
- }
108
- const updates = {
109
- ...data,
110
- output: data.output,
111
- };
112
- // Only set startedAt the first time transitioning to 'running'
113
- if (data.status === 'running' && !currentRun.startedAt) {
114
- updates.startedAt = new Date();
115
- }
116
- if (data.status === 'completed' ||
117
- data.status === 'failed' ||
118
- data.status === 'cancelled') {
119
- updates.completedAt = new Date();
120
- }
121
- const [value] = await drizzle
122
- .update(runs)
123
- .set(updates)
124
- .where(eq(runs.runId, id))
125
- .returning();
126
- if (!value) {
127
- throw new WorkflowAPIError(`Run not found: ${id}`, { status: 404 });
128
- }
129
- return compact(value);
130
- },
112
+ }),
131
113
  };
132
114
  }
133
115
  function map(obj, fn) {
134
116
  return obj ? fn(obj) : undefined;
135
117
  }
118
+ /**
119
+ * Handle events for legacy runs (pre-event-sourcing, specVersion < 2).
120
+ * Legacy runs use different behavior:
121
+ * - run_cancelled: Skip event storage, directly update run
122
+ * - wait_completed: Store event only (no entity mutation)
123
+ * - hook_received: Store event only (hooks exist via old system, no entity mutation)
124
+ * - Other events: Throw error (not supported for legacy runs)
125
+ */
126
+ async function handleLegacyEventPostgres(drizzle, runId, eventId, data, currentRun, params) {
127
+ const resolveData = params?.resolveData ?? 'all';
128
+ switch (data.eventType) {
129
+ case 'run_cancelled': {
130
+ // Legacy: Skip event storage, directly update run to cancelled
131
+ const now = new Date();
132
+ // Update run status to cancelled
133
+ await drizzle
134
+ .update(Schema.runs)
135
+ .set({
136
+ status: 'cancelled',
137
+ completedAt: now,
138
+ updatedAt: now,
139
+ })
140
+ .where(eq(Schema.runs.runId, runId));
141
+ // Delete all hooks and waits for this run
142
+ await Promise.all([
143
+ drizzle.delete(Schema.hooks).where(eq(Schema.hooks.runId, runId)),
144
+ drizzle.delete(Schema.waits).where(eq(Schema.waits.runId, runId)),
145
+ ]);
146
+ // Fetch updated run for return value
147
+ const [updatedRun] = await drizzle
148
+ .select()
149
+ .from(Schema.runs)
150
+ .where(eq(Schema.runs.runId, runId))
151
+ .limit(1);
152
+ // Return without event (legacy behavior skips event storage)
153
+ // Type assertion: EventResult expects WorkflowRun, filterRunData may return WorkflowRunWithoutData
154
+ return {
155
+ run: updatedRun
156
+ ? filterRunData(deserializeRunError(compact(updatedRun)), resolveData)
157
+ : undefined,
158
+ };
159
+ }
160
+ case 'wait_completed':
161
+ case 'hook_received': {
162
+ // Legacy: Store event only (no entity mutation)
163
+ // - wait_completed: for replay purposes
164
+ // - hook_received: hooks exist via old system, just record the event
165
+ const [insertedEvent] = await drizzle
166
+ .insert(Schema.events)
167
+ .values({
168
+ runId,
169
+ eventId,
170
+ correlationId: data.correlationId,
171
+ eventType: data.eventType,
172
+ eventData: 'eventData' in data ? data.eventData : undefined,
173
+ specVersion: SPEC_VERSION_CURRENT,
174
+ })
175
+ .returning({ createdAt: Schema.events.createdAt });
176
+ const event = EventSchema.parse({
177
+ ...data,
178
+ ...insertedEvent,
179
+ runId,
180
+ eventId,
181
+ });
182
+ return { event: filterEventData(event, resolveData) };
183
+ }
184
+ default:
185
+ throw new Error(`Event type '${data.eventType}' not supported for legacy runs ` +
186
+ `(specVersion: ${currentRun.specVersion || 'undefined'}). ` +
187
+ `Please upgrade @workflow packages.`);
188
+ }
189
+ }
136
190
  export function createEventsStorage(drizzle) {
137
191
  const ulid = monotonicFactory();
138
192
  const { events } = Schema;
193
+ // Prepared statements for validation queries (performance optimization)
194
+ const getRunForValidation = drizzle
195
+ .select({
196
+ status: Schema.runs.status,
197
+ specVersion: Schema.runs.specVersion,
198
+ })
199
+ .from(Schema.runs)
200
+ .where(eq(Schema.runs.runId, sql.placeholder('runId')))
201
+ .limit(1)
202
+ .prepare('events_get_run_for_validation');
203
+ const getStepForValidation = drizzle
204
+ .select({
205
+ status: Schema.steps.status,
206
+ startedAt: Schema.steps.startedAt,
207
+ retryAfter: Schema.steps.retryAfter,
208
+ })
209
+ .from(Schema.steps)
210
+ .where(and(eq(Schema.steps.runId, sql.placeholder('runId')), eq(Schema.steps.stepId, sql.placeholder('stepId'))))
211
+ .limit(1)
212
+ .prepare('events_get_step_for_validation');
213
+ const getHookByToken = drizzle
214
+ .select({ hookId: Schema.hooks.hookId })
215
+ .from(Schema.hooks)
216
+ .where(eq(Schema.hooks.token, sql.placeholder('token')))
217
+ .limit(1)
218
+ .prepare('events_get_hook_by_token');
219
+ const getWaitForValidation = drizzle
220
+ .select({
221
+ status: Schema.waits.status,
222
+ })
223
+ .from(Schema.waits)
224
+ .where(eq(Schema.waits.waitId, sql.placeholder('waitId')))
225
+ .limit(1)
226
+ .prepare('events_get_wait_for_validation');
139
227
  return {
140
- async create(runId, data) {
228
+ async create(runId, data, params) {
141
229
  const eventId = `wevt_${ulid()}`;
230
+ // For run_created events, use client-provided runId or generate one server-side
231
+ let effectiveRunId;
232
+ if (data.eventType === 'run_created' && (!runId || runId === '')) {
233
+ effectiveRunId = `wrun_${ulid()}`;
234
+ }
235
+ else if (!runId) {
236
+ throw new Error('runId is required for non-run_created events');
237
+ }
238
+ else {
239
+ effectiveRunId = runId;
240
+ }
241
+ // Validate client-provided runId timestamp is within acceptable threshold
242
+ if (data.eventType === 'run_created' && runId && runId !== '') {
243
+ const validationError = validateUlidTimestamp(effectiveRunId, 'wrun_');
244
+ if (validationError) {
245
+ throw new WorkflowAPIError(validationError, { status: 400 });
246
+ }
247
+ }
248
+ // specVersion is always sent by the runtime, but we provide a fallback for safety
249
+ const effectiveSpecVersion = data.specVersion ?? SPEC_VERSION_CURRENT;
250
+ // Track entity created/updated for EventResult
251
+ let run;
252
+ let step;
253
+ let hook;
254
+ let wait;
255
+ const now = new Date();
256
+ // Helper to check if run is in terminal state
257
+ const isRunTerminal = (status) => ['completed', 'failed', 'cancelled'].includes(status);
258
+ // Helper to check if step is in terminal state
259
+ const isStepTerminal = (status) => ['completed', 'failed'].includes(status);
260
+ // ============================================================
261
+ // VALIDATION: Terminal state and event ordering checks
262
+ // ============================================================
263
+ // Get current run state for validation (if not creating a new run)
264
+ // Skip run validation for step_completed and step_retrying - they only operate
265
+ // on running steps, and running steps are always allowed to modify regardless
266
+ // of run state. This optimization saves database queries per step event.
267
+ let currentRun = null;
268
+ const skipRunValidationEvents = ['step_completed', 'step_retrying'];
269
+ if (data.eventType !== 'run_created' &&
270
+ !skipRunValidationEvents.includes(data.eventType)) {
271
+ // Use prepared statement for better performance
272
+ const [runValue] = await getRunForValidation.execute({
273
+ runId: effectiveRunId,
274
+ });
275
+ currentRun = runValue ?? null;
276
+ }
277
+ // ============================================================
278
+ // VERSION COMPATIBILITY: Check run spec version
279
+ // ============================================================
280
+ // For events that have fetched the run, check version compatibility.
281
+ // Skip for run_created (no existing run) and runtime events (step_completed, step_retrying).
282
+ if (currentRun) {
283
+ // Check if run requires a newer world version
284
+ if (requiresNewerWorld(currentRun.specVersion)) {
285
+ throw new RunNotSupportedError(currentRun.specVersion, SPEC_VERSION_CURRENT);
286
+ }
287
+ // Route to legacy handler for pre-event-sourcing runs
288
+ if (isLegacySpecVersion(currentRun.specVersion)) {
289
+ return handleLegacyEventPostgres(drizzle, effectiveRunId, eventId, data, currentRun, params);
290
+ }
291
+ }
292
+ // Run terminal state validation
293
+ if (currentRun && isRunTerminal(currentRun.status)) {
294
+ const runTerminalEvents = [
295
+ 'run_started',
296
+ 'run_completed',
297
+ 'run_failed',
298
+ ];
299
+ // Idempotent operation: run_cancelled on already cancelled run is allowed
300
+ if (data.eventType === 'run_cancelled' &&
301
+ currentRun.status === 'cancelled') {
302
+ // Get full run for return value
303
+ const [fullRun] = await drizzle
304
+ .select()
305
+ .from(Schema.runs)
306
+ .where(eq(Schema.runs.runId, effectiveRunId))
307
+ .limit(1);
308
+ // Create the event (still record it)
309
+ const [value] = await drizzle
310
+ .insert(Schema.events)
311
+ .values({
312
+ runId: effectiveRunId,
313
+ eventId,
314
+ correlationId: data.correlationId,
315
+ eventType: data.eventType,
316
+ eventData: 'eventData' in data ? data.eventData : undefined,
317
+ specVersion: effectiveSpecVersion,
318
+ })
319
+ .returning({ createdAt: Schema.events.createdAt });
320
+ const result = { ...data, ...value, runId: effectiveRunId, eventId };
321
+ const parsed = EventSchema.parse(result);
322
+ const resolveData = params?.resolveData ?? 'all';
323
+ return {
324
+ event: filterEventData(parsed, resolveData),
325
+ run: fullRun ? deserializeRunError(compact(fullRun)) : undefined,
326
+ };
327
+ }
328
+ // Run state transitions are not allowed on terminal runs
329
+ if (runTerminalEvents.includes(data.eventType) ||
330
+ data.eventType === 'run_cancelled') {
331
+ throw new WorkflowAPIError(`Cannot transition run from terminal state "${currentRun.status}"`, { status: 409 });
332
+ }
333
+ // Creating new entities on terminal runs is not allowed
334
+ if (data.eventType === 'step_created' ||
335
+ data.eventType === 'hook_created' ||
336
+ data.eventType === 'wait_created') {
337
+ throw new WorkflowAPIError(`Cannot create new entities on run in terminal state "${currentRun.status}"`, { status: 409 });
338
+ }
339
+ }
340
+ // Step-related event validation (ordering and terminal state)
341
+ // Fetch status + startedAt so we can reuse for step_started (avoid double read)
342
+ // Skip validation for step_completed/step_failed - use conditional UPDATE instead
343
+ let validatedStep = null;
344
+ const stepEventsNeedingValidation = ['step_started', 'step_retrying'];
345
+ if (stepEventsNeedingValidation.includes(data.eventType) &&
346
+ data.correlationId) {
347
+ // Use prepared statement for better performance
348
+ const [existingStep] = await getStepForValidation.execute({
349
+ runId: effectiveRunId,
350
+ stepId: data.correlationId,
351
+ });
352
+ validatedStep = existingStep ?? null;
353
+ // Event ordering: step must exist before these events
354
+ if (!validatedStep) {
355
+ throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, {
356
+ status: 404,
357
+ });
358
+ }
359
+ // Step terminal state validation
360
+ if (isStepTerminal(validatedStep.status)) {
361
+ throw new WorkflowAPIError(`Cannot modify step in terminal state "${validatedStep.status}"`, { status: 409 });
362
+ }
363
+ // On terminal runs: only allow completing/failing in-progress steps
364
+ if (currentRun && isRunTerminal(currentRun.status)) {
365
+ if (validatedStep.status !== 'running') {
366
+ throw new WorkflowAPIError(`Cannot modify non-running step on run in terminal state "${currentRun.status}"`, { status: 410 });
367
+ }
368
+ }
369
+ }
370
+ // Hook-related event validation (ordering)
371
+ const hookEventsRequiringExistence = ['hook_disposed', 'hook_received'];
372
+ if (hookEventsRequiringExistence.includes(data.eventType) &&
373
+ data.correlationId) {
374
+ const [existingHook] = await drizzle
375
+ .select({ hookId: Schema.hooks.hookId })
376
+ .from(Schema.hooks)
377
+ .where(eq(Schema.hooks.hookId, data.correlationId))
378
+ .limit(1);
379
+ if (!existingHook) {
380
+ throw new WorkflowAPIError(`Hook "${data.correlationId}" not found`, {
381
+ status: 404,
382
+ });
383
+ }
384
+ }
385
+ // ============================================================
386
+ // Entity creation/updates based on event type
387
+ // ============================================================
388
+ // Handle run_created event: create the run entity atomically
389
+ if (data.eventType === 'run_created') {
390
+ const eventData = data.eventData;
391
+ const [runValue] = await drizzle
392
+ .insert(Schema.runs)
393
+ .values({
394
+ runId: effectiveRunId,
395
+ deploymentId: eventData.deploymentId,
396
+ workflowName: eventData.workflowName,
397
+ // Propagate specVersion from the event to the run entity
398
+ specVersion: effectiveSpecVersion,
399
+ input: eventData.input,
400
+ executionContext: eventData.executionContext,
401
+ status: 'pending',
402
+ })
403
+ .onConflictDoNothing()
404
+ .returning();
405
+ if (runValue) {
406
+ run = deserializeRunError(compact(runValue));
407
+ }
408
+ }
409
+ // Handle run_started event: update run status
410
+ if (data.eventType === 'run_started') {
411
+ const [runValue] = await drizzle
412
+ .update(Schema.runs)
413
+ .set({
414
+ status: 'running',
415
+ startedAt: now,
416
+ updatedAt: now,
417
+ })
418
+ .where(eq(Schema.runs.runId, effectiveRunId))
419
+ .returning();
420
+ if (runValue) {
421
+ run = deserializeRunError(compact(runValue));
422
+ }
423
+ }
424
+ // Handle run_completed event: update run status and cleanup hooks
425
+ if (data.eventType === 'run_completed') {
426
+ const eventData = data.eventData;
427
+ const [runValue] = await drizzle
428
+ .update(Schema.runs)
429
+ .set({
430
+ status: 'completed',
431
+ output: eventData.output,
432
+ completedAt: now,
433
+ updatedAt: now,
434
+ })
435
+ .where(eq(Schema.runs.runId, effectiveRunId))
436
+ .returning();
437
+ if (runValue) {
438
+ run = deserializeRunError(compact(runValue));
439
+ }
440
+ // Delete all hooks and waits for this run to allow token reuse
441
+ await Promise.all([
442
+ drizzle
443
+ .delete(Schema.hooks)
444
+ .where(eq(Schema.hooks.runId, effectiveRunId)),
445
+ drizzle
446
+ .delete(Schema.waits)
447
+ .where(eq(Schema.waits.runId, effectiveRunId)),
448
+ ]);
449
+ }
450
+ // Handle run_failed event: update run status and cleanup hooks
451
+ if (data.eventType === 'run_failed') {
452
+ const eventData = data.eventData;
453
+ const errorMessage = typeof eventData.error === 'string'
454
+ ? eventData.error
455
+ : (eventData.error?.message ?? 'Unknown error');
456
+ const [runValue] = await drizzle
457
+ .update(Schema.runs)
458
+ .set({
459
+ status: 'failed',
460
+ error: {
461
+ message: errorMessage,
462
+ stack: eventData.error?.stack,
463
+ code: eventData.errorCode,
464
+ },
465
+ completedAt: now,
466
+ updatedAt: now,
467
+ })
468
+ .where(eq(Schema.runs.runId, effectiveRunId))
469
+ .returning();
470
+ if (runValue) {
471
+ run = deserializeRunError(compact(runValue));
472
+ }
473
+ // Delete all hooks and waits for this run to allow token reuse
474
+ await Promise.all([
475
+ drizzle
476
+ .delete(Schema.hooks)
477
+ .where(eq(Schema.hooks.runId, effectiveRunId)),
478
+ drizzle
479
+ .delete(Schema.waits)
480
+ .where(eq(Schema.waits.runId, effectiveRunId)),
481
+ ]);
482
+ }
483
+ // Handle run_cancelled event: update run status and cleanup hooks
484
+ if (data.eventType === 'run_cancelled') {
485
+ const [runValue] = await drizzle
486
+ .update(Schema.runs)
487
+ .set({
488
+ status: 'cancelled',
489
+ completedAt: now,
490
+ updatedAt: now,
491
+ })
492
+ .where(eq(Schema.runs.runId, effectiveRunId))
493
+ .returning();
494
+ if (runValue) {
495
+ run = deserializeRunError(compact(runValue));
496
+ }
497
+ // Delete all hooks and waits for this run to allow token reuse
498
+ await Promise.all([
499
+ drizzle
500
+ .delete(Schema.hooks)
501
+ .where(eq(Schema.hooks.runId, effectiveRunId)),
502
+ drizzle
503
+ .delete(Schema.waits)
504
+ .where(eq(Schema.waits.runId, effectiveRunId)),
505
+ ]);
506
+ }
507
+ // Handle step_created event: create step entity
508
+ if (data.eventType === 'step_created') {
509
+ const eventData = data.eventData;
510
+ const [stepValue] = await drizzle
511
+ .insert(Schema.steps)
512
+ .values({
513
+ runId: effectiveRunId,
514
+ stepId: data.correlationId,
515
+ stepName: eventData.stepName,
516
+ input: eventData.input,
517
+ status: 'pending',
518
+ attempt: 0,
519
+ // Propagate specVersion from the event to the step entity
520
+ specVersion: effectiveSpecVersion,
521
+ })
522
+ .onConflictDoNothing()
523
+ .returning();
524
+ if (stepValue) {
525
+ step = deserializeStepError(compact(stepValue));
526
+ }
527
+ }
528
+ // Handle step_started event: increment attempt, set status to 'running'
529
+ // Sets startedAt (maps to startedAt) only on first start
530
+ // Reuse validatedStep from validation (already read above)
531
+ if (data.eventType === 'step_started') {
532
+ // Check if retryAfter timestamp hasn't been reached yet
533
+ if (validatedStep?.retryAfter &&
534
+ validatedStep.retryAfter.getTime() > Date.now()) {
535
+ const err = new WorkflowAPIError(`Cannot start step "${data.correlationId}": retryAfter timestamp has not been reached yet`, { status: 425 });
536
+ // Add meta for step-handler to extract retryAfter timestamp
537
+ err.meta = {
538
+ stepId: data.correlationId,
539
+ retryAfter: validatedStep.retryAfter.toISOString(),
540
+ };
541
+ throw err;
542
+ }
543
+ const isFirstStart = !validatedStep?.startedAt;
544
+ const hadRetryAfter = !!validatedStep?.retryAfter;
545
+ const [stepValue] = await drizzle
546
+ .update(Schema.steps)
547
+ .set({
548
+ status: 'running',
549
+ // Increment attempt counter using SQL
550
+ attempt: sql `${Schema.steps.attempt} + 1`,
551
+ // Only set startedAt on first start (not updated on retries)
552
+ ...(isFirstStart ? { startedAt: now } : {}),
553
+ // Clear retryAfter now that the step has started
554
+ ...(hadRetryAfter ? { retryAfter: null } : {}),
555
+ })
556
+ .where(and(eq(Schema.steps.runId, effectiveRunId), eq(Schema.steps.stepId, data.correlationId)))
557
+ .returning();
558
+ if (stepValue) {
559
+ step = deserializeStepError(compact(stepValue));
560
+ }
561
+ }
562
+ // Handle step_completed event: update step status
563
+ // Uses conditional UPDATE to skip validation query (performance optimization)
564
+ if (data.eventType === 'step_completed') {
565
+ const eventData = data.eventData;
566
+ const [stepValue] = await drizzle
567
+ .update(Schema.steps)
568
+ .set({
569
+ status: 'completed',
570
+ output: eventData.result,
571
+ completedAt: now,
572
+ })
573
+ .where(and(eq(Schema.steps.runId, effectiveRunId), eq(Schema.steps.stepId, data.correlationId),
574
+ // Only update if not already in terminal state (validation in WHERE clause)
575
+ notInArray(Schema.steps.status, ['completed', 'failed'])))
576
+ .returning();
577
+ if (stepValue) {
578
+ step = deserializeStepError(compact(stepValue));
579
+ }
580
+ else {
581
+ // Step not updated - check if it exists and why
582
+ const [existing] = await getStepForValidation.execute({
583
+ runId: effectiveRunId,
584
+ stepId: data.correlationId,
585
+ });
586
+ if (!existing) {
587
+ throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { status: 404 });
588
+ }
589
+ if (['completed', 'failed'].includes(existing.status)) {
590
+ throw new WorkflowAPIError(`Cannot modify step in terminal state "${existing.status}"`, { status: 409 });
591
+ }
592
+ }
593
+ }
594
+ // Handle step_failed event: terminal state with error
595
+ // Uses conditional UPDATE to skip validation query (performance optimization)
596
+ if (data.eventType === 'step_failed') {
597
+ const eventData = data.eventData;
598
+ const errorMessage = typeof eventData.error === 'string'
599
+ ? eventData.error
600
+ : (eventData.error?.message ?? 'Unknown error');
601
+ const [stepValue] = await drizzle
602
+ .update(Schema.steps)
603
+ .set({
604
+ status: 'failed',
605
+ error: {
606
+ message: errorMessage,
607
+ stack: eventData.stack,
608
+ },
609
+ completedAt: now,
610
+ })
611
+ .where(and(eq(Schema.steps.runId, effectiveRunId), eq(Schema.steps.stepId, data.correlationId),
612
+ // Only update if not already in terminal state (validation in WHERE clause)
613
+ notInArray(Schema.steps.status, ['completed', 'failed'])))
614
+ .returning();
615
+ if (stepValue) {
616
+ step = deserializeStepError(compact(stepValue));
617
+ }
618
+ else {
619
+ // Step not updated - check if it exists and why
620
+ const [existing] = await getStepForValidation.execute({
621
+ runId: effectiveRunId,
622
+ stepId: data.correlationId,
623
+ });
624
+ if (!existing) {
625
+ throw new WorkflowAPIError(`Step "${data.correlationId}" not found`, { status: 404 });
626
+ }
627
+ if (['completed', 'failed'].includes(existing.status)) {
628
+ throw new WorkflowAPIError(`Cannot modify step in terminal state "${existing.status}"`, { status: 409 });
629
+ }
630
+ }
631
+ }
632
+ // Handle step_retrying event: sets status back to 'pending', records error
633
+ if (data.eventType === 'step_retrying') {
634
+ const eventData = data.eventData;
635
+ const errorMessage = typeof eventData.error === 'string'
636
+ ? eventData.error
637
+ : (eventData.error?.message ?? 'Unknown error');
638
+ const [stepValue] = await drizzle
639
+ .update(Schema.steps)
640
+ .set({
641
+ status: 'pending',
642
+ error: {
643
+ message: errorMessage,
644
+ stack: eventData.stack,
645
+ },
646
+ retryAfter: eventData.retryAfter,
647
+ })
648
+ .where(and(eq(Schema.steps.runId, effectiveRunId), eq(Schema.steps.stepId, data.correlationId)))
649
+ .returning();
650
+ if (stepValue) {
651
+ step = deserializeStepError(compact(stepValue));
652
+ }
653
+ }
654
+ // Handle hook_created event: create hook entity
655
+ // Uses prepared statement for token uniqueness check (performance optimization)
656
+ if (data.eventType === 'hook_created') {
657
+ const eventData = data.eventData;
658
+ // Check for duplicate token using prepared statement
659
+ const [existingHook] = await getHookByToken.execute({
660
+ token: eventData.token,
661
+ });
662
+ if (existingHook) {
663
+ // Create hook_conflict event instead of throwing 409
664
+ // This allows the workflow to continue and fail gracefully when the hook is awaited
665
+ const conflictEventData = {
666
+ token: eventData.token,
667
+ };
668
+ const [conflictValue] = await drizzle
669
+ .insert(events)
670
+ .values({
671
+ runId: effectiveRunId,
672
+ eventId,
673
+ correlationId: data.correlationId,
674
+ eventType: 'hook_conflict',
675
+ eventData: conflictEventData,
676
+ specVersion: effectiveSpecVersion,
677
+ })
678
+ .returning({ createdAt: events.createdAt });
679
+ if (!conflictValue) {
680
+ throw new WorkflowAPIError(`Event ${eventId} could not be created`, { status: 409 });
681
+ }
682
+ const conflictResult = {
683
+ eventType: 'hook_conflict',
684
+ correlationId: data.correlationId,
685
+ eventData: conflictEventData,
686
+ ...conflictValue,
687
+ runId: effectiveRunId,
688
+ eventId,
689
+ };
690
+ const parsedConflict = EventSchema.parse(conflictResult);
691
+ const resolveData = params?.resolveData ?? 'all';
692
+ return {
693
+ event: filterEventData(parsedConflict, resolveData),
694
+ run,
695
+ step,
696
+ hook: undefined,
697
+ };
698
+ }
699
+ const [hookValue] = await drizzle
700
+ .insert(Schema.hooks)
701
+ .values({
702
+ runId: effectiveRunId,
703
+ hookId: data.correlationId,
704
+ token: eventData.token,
705
+ metadata: eventData.metadata,
706
+ ownerId: '', // TODO: get from context
707
+ projectId: '', // TODO: get from context
708
+ environment: '', // TODO: get from context
709
+ // Propagate specVersion from the event to the hook entity
710
+ specVersion: effectiveSpecVersion,
711
+ isWebhook: eventData.isWebhook,
712
+ })
713
+ .onConflictDoNothing()
714
+ .returning();
715
+ if (hookValue) {
716
+ hookValue.metadata ||= hookValue.metadataJson;
717
+ hook = HookSchema.parse(compact(hookValue));
718
+ }
719
+ }
720
+ // Handle hook_disposed event: delete hook entity
721
+ if (data.eventType === 'hook_disposed' && data.correlationId) {
722
+ await drizzle
723
+ .delete(Schema.hooks)
724
+ .where(eq(Schema.hooks.hookId, data.correlationId));
725
+ }
726
+ // Handle wait_created event: create wait entity
727
+ if (data.eventType === 'wait_created') {
728
+ const eventData = data.eventData;
729
+ const waitId = `${effectiveRunId}-${data.correlationId}`;
730
+ const [waitValue] = await drizzle
731
+ .insert(Schema.waits)
732
+ .values({
733
+ waitId,
734
+ runId: effectiveRunId,
735
+ status: 'waiting',
736
+ resumeAt: eventData.resumeAt,
737
+ specVersion: effectiveSpecVersion,
738
+ })
739
+ .onConflictDoNothing()
740
+ .returning();
741
+ if (waitValue) {
742
+ wait = {
743
+ waitId: waitValue.waitId,
744
+ runId: waitValue.runId,
745
+ status: waitValue.status,
746
+ resumeAt: waitValue.resumeAt ?? undefined,
747
+ completedAt: waitValue.completedAt ?? undefined,
748
+ createdAt: waitValue.createdAt,
749
+ updatedAt: waitValue.updatedAt,
750
+ specVersion: waitValue.specVersion ?? undefined,
751
+ };
752
+ }
753
+ else {
754
+ throw new WorkflowAPIError(`Wait "${data.correlationId}" already exists`, { status: 409 });
755
+ }
756
+ }
757
+ // Handle wait_completed event: transition wait to 'completed'
758
+ // Uses conditional UPDATE to reject duplicate completions (same pattern as step_completed)
759
+ if (data.eventType === 'wait_completed') {
760
+ const waitId = `${effectiveRunId}-${data.correlationId}`;
761
+ const [waitValue] = await drizzle
762
+ .update(Schema.waits)
763
+ .set({
764
+ status: 'completed',
765
+ completedAt: now,
766
+ })
767
+ .where(and(eq(Schema.waits.waitId, waitId), eq(Schema.waits.status, 'waiting')))
768
+ .returning();
769
+ if (waitValue) {
770
+ wait = {
771
+ waitId: waitValue.waitId,
772
+ runId: waitValue.runId,
773
+ status: waitValue.status,
774
+ resumeAt: waitValue.resumeAt ?? undefined,
775
+ completedAt: waitValue.completedAt ?? undefined,
776
+ createdAt: waitValue.createdAt,
777
+ updatedAt: waitValue.updatedAt,
778
+ specVersion: waitValue.specVersion ?? undefined,
779
+ };
780
+ }
781
+ else {
782
+ // Wait not updated - check if it exists and why
783
+ const [existing] = await getWaitForValidation.execute({
784
+ waitId,
785
+ });
786
+ if (!existing) {
787
+ throw new WorkflowAPIError(`Wait "${data.correlationId}" not found`, { status: 404 });
788
+ }
789
+ if (existing.status === 'completed') {
790
+ throw new WorkflowAPIError(`Wait "${data.correlationId}" already completed`, { status: 409 });
791
+ }
792
+ }
793
+ }
142
794
  const [value] = await drizzle
143
795
  .insert(events)
144
796
  .values({
145
- runId,
797
+ runId: effectiveRunId,
146
798
  eventId,
147
799
  correlationId: data.correlationId,
148
800
  eventType: data.eventType,
149
801
  eventData: 'eventData' in data ? data.eventData : undefined,
802
+ specVersion: effectiveSpecVersion,
150
803
  })
151
804
  .returning({ createdAt: events.createdAt });
152
805
  if (!value) {
@@ -154,7 +807,16 @@ export function createEventsStorage(drizzle) {
154
807
  status: 409,
155
808
  });
156
809
  }
157
- return { ...data, ...value, runId, eventId };
810
+ const result = { ...data, ...value, runId: effectiveRunId, eventId };
811
+ const parsed = EventSchema.parse(result);
812
+ const resolveData = params?.resolveData ?? 'all';
813
+ return {
814
+ event: filterEventData(parsed, resolveData),
815
+ run,
816
+ step,
817
+ hook,
818
+ wait,
819
+ };
158
820
  },
159
821
  async list(params) {
160
822
  const limit = params?.pagination?.limit ?? 100;
@@ -169,8 +831,13 @@ export function createEventsStorage(drizzle) {
169
831
  .orderBy(order.by)
170
832
  .limit(limit + 1);
171
833
  const values = all.slice(0, limit);
834
+ const resolveData = params?.resolveData ?? 'all';
172
835
  return {
173
- data: values.map(compact),
836
+ data: values.map((v) => {
837
+ v.eventData ||= v.eventDataJson;
838
+ const parsed = EventSchema.parse(compact(v));
839
+ return filterEventData(parsed, resolveData);
840
+ }),
174
841
  cursor: values.at(-1)?.eventId ?? null,
175
842
  hasMore: all.length > limit,
176
843
  };
@@ -188,8 +855,13 @@ export function createEventsStorage(drizzle) {
188
855
  .orderBy(order.by)
189
856
  .limit(limit + 1);
190
857
  const values = all.slice(0, limit);
858
+ const resolveData = params?.resolveData ?? 'all';
191
859
  return {
192
- data: values.map(compact),
860
+ data: values.map((v) => {
861
+ v.eventData ||= v.eventDataJson;
862
+ const parsed = EventSchema.parse(compact(v));
863
+ return filterEventData(parsed, resolveData);
864
+ }),
193
865
  cursor: values.at(-1)?.eventId ?? null,
194
866
  hasMore: all.length > limit,
195
867
  };
@@ -205,149 +877,83 @@ export function createHooksStorage(drizzle) {
205
877
  .limit(1)
206
878
  .prepare('workflow_hooks_get_by_token');
207
879
  return {
208
- async get(hookId) {
880
+ async get(hookId, params) {
209
881
  const [value] = await drizzle
210
882
  .select()
211
883
  .from(hooks)
212
884
  .where(eq(hooks.hookId, hookId))
213
885
  .limit(1);
214
- return compact(value);
215
- },
216
- async create(runId, data) {
217
- const [value] = await drizzle
218
- .insert(hooks)
219
- .values({
220
- runId,
221
- hookId: data.hookId,
222
- token: data.token,
223
- ownerId: '', // TODO: get from context
224
- projectId: '', // TODO: get from context
225
- environment: '', // TODO: get from context
226
- })
227
- .onConflictDoNothing()
228
- .returning();
229
- if (!value) {
230
- throw new WorkflowAPIError(`Hook ${data.hookId} already exists`, {
231
- status: 409,
232
- });
233
- }
234
- return compact(value);
886
+ value.metadata ||= value.metadataJson;
887
+ const parsed = HookSchema.parse(compact(value));
888
+ parsed.isWebhook ??= true;
889
+ const resolveData = params?.resolveData ?? 'all';
890
+ return filterHookData(parsed, resolveData);
235
891
  },
236
- async getByToken(token) {
892
+ async getByToken(token, params) {
237
893
  const [value] = await getByToken.execute({ token });
238
894
  if (!value) {
239
- throw new WorkflowAPIError(`Hook not found for token: ${token}`, {
240
- status: 404,
241
- });
895
+ throw new HookNotFoundError(token);
242
896
  }
243
- return compact(value);
897
+ value.metadata ||= value.metadataJson;
898
+ const parsed = HookSchema.parse(compact(value));
899
+ parsed.isWebhook ??= true;
900
+ const resolveData = params?.resolveData ?? 'all';
901
+ return filterHookData(parsed, resolveData);
244
902
  },
245
903
  async list(params) {
246
904
  const limit = params?.pagination?.limit ?? 100;
247
905
  const fromCursor = params?.pagination?.cursor;
906
+ const sortOrder = params?.pagination?.sortOrder ?? 'asc';
907
+ const orderFn = sortOrder === 'asc' ? asc : desc;
908
+ const cursorFn = sortOrder === 'asc' ? gt : lt;
248
909
  const all = await drizzle
249
910
  .select()
250
911
  .from(hooks)
251
- .where(and(map(params.runId, (id) => eq(hooks.runId, id)), map(fromCursor, (c) => lt(hooks.hookId, c))))
252
- .orderBy(desc(hooks.hookId))
912
+ .where(and(map(params.runId, (id) => eq(hooks.runId, id)), map(fromCursor, (c) => cursorFn(hooks.hookId, c))))
913
+ .orderBy(orderFn(hooks.hookId))
253
914
  .limit(limit + 1);
254
915
  const values = all.slice(0, limit);
255
916
  const hasMore = all.length > limit;
917
+ const resolveData = params?.resolveData ?? 'all';
256
918
  return {
257
- data: values.map(compact),
919
+ data: values.map((v) => {
920
+ v.metadata ||= v.metadataJson;
921
+ const parsed = HookSchema.parse(compact(v));
922
+ return filterHookData(parsed, resolveData);
923
+ }),
258
924
  cursor: values.at(-1)?.hookId ?? null,
259
925
  hasMore,
260
926
  };
261
927
  },
262
- async dispose(hookId) {
263
- const [value] = await drizzle
264
- .delete(hooks)
265
- .where(eq(hooks.hookId, hookId))
266
- .returning();
267
- if (!value) {
268
- throw new WorkflowAPIError(`Hook not found: ${hookId}`, {
269
- status: 404,
270
- });
271
- }
272
- return compact(value);
273
- },
274
928
  };
275
929
  }
276
930
  export function createStepsStorage(drizzle) {
277
931
  const { steps } = Schema;
278
- const get = drizzle
279
- .select()
280
- .from(steps)
281
- .where(and(eq(steps.stepId, sql.placeholder('stepId')), eq(steps.runId, sql.placeholder('runId'))))
282
- .limit(1)
283
- .prepare('workflow_steps_get');
284
932
  return {
285
- async create(runId, data) {
933
+ get: (async (runId, stepId, params) => {
934
+ // If runId is not provided, query only by stepId
935
+ const whereClause = runId
936
+ ? and(eq(steps.stepId, stepId), eq(steps.runId, runId))
937
+ : eq(steps.stepId, stepId);
286
938
  const [value] = await drizzle
287
- .insert(steps)
288
- .values({
289
- runId,
290
- stepId: data.stepId,
291
- stepName: data.stepName,
292
- input: data.input,
293
- status: 'pending',
294
- attempt: 1,
295
- })
296
- .onConflictDoNothing()
297
- .returning();
298
- if (!value) {
299
- throw new WorkflowAPIError(`Step ${data.stepId} already exists`, {
300
- status: 409,
301
- });
302
- }
303
- return compact(value);
304
- },
305
- async get(runId, stepId) {
306
- const [value] = await get.execute({ stepId, runId });
307
- if (!value) {
308
- throw new WorkflowAPIError(`Step not found: ${stepId}`, {
309
- status: 404,
310
- });
311
- }
312
- return compact(value);
313
- },
314
- async update(runId, stepId, data) {
315
- // Fetch current step to check if startedAt is already set
316
- const [currentStep] = await drizzle
317
939
  .select()
318
940
  .from(steps)
319
- .where(and(eq(steps.stepId, stepId), eq(steps.runId, runId)))
941
+ .where(whereClause)
320
942
  .limit(1);
321
- if (!currentStep) {
322
- throw new WorkflowAPIError(`Step not found: ${stepId}`, {
323
- status: 404,
324
- });
325
- }
326
- const updates = {
327
- ...data,
328
- output: data.output,
329
- };
330
- const now = new Date();
331
- // Only set startedAt the first time the step transitions to 'running'
332
- if (data.status === 'running' && !currentStep.startedAt) {
333
- updates.startedAt = now;
334
- }
335
- if (data.status === 'completed' || data.status === 'failed') {
336
- updates.completedAt = now;
337
- }
338
- const [value] = await drizzle
339
- .update(steps)
340
- .set(updates)
341
- .where(and(eq(steps.stepId, stepId), eq(steps.runId, runId)))
342
- .returning();
343
943
  if (!value) {
344
944
  throw new WorkflowAPIError(`Step not found: ${stepId}`, {
345
945
  status: 404,
346
946
  });
347
947
  }
348
- return compact(value);
349
- },
350
- async list(params) {
948
+ value.output ||= value.outputJson;
949
+ value.input ||= value.inputJson;
950
+ value.error ||= parseErrorJson(value.errorJson);
951
+ const deserialized = deserializeStepError(compact(value));
952
+ const parsed = StepSchema.parse(deserialized);
953
+ const resolveData = params?.resolveData ?? 'all';
954
+ return filterStepData(parsed, resolveData);
955
+ }),
956
+ list: (async (params) => {
351
957
  const limit = params?.pagination?.limit ?? 20;
352
958
  const fromCursor = params?.pagination?.cursor;
353
959
  const all = await drizzle
@@ -358,12 +964,48 @@ export function createStepsStorage(drizzle) {
358
964
  .limit(limit + 1);
359
965
  const values = all.slice(0, limit);
360
966
  const hasMore = all.length > limit;
967
+ const resolveData = params?.resolveData ?? 'all';
361
968
  return {
362
- data: values.map(compact),
969
+ data: values.map((v) => {
970
+ v.output ||= v.outputJson;
971
+ v.input ||= v.inputJson;
972
+ v.error ||= parseErrorJson(v.errorJson);
973
+ const deserialized = deserializeStepError(compact(v));
974
+ const parsed = StepSchema.parse(deserialized);
975
+ return filterStepData(parsed, resolveData);
976
+ }),
363
977
  hasMore,
364
978
  cursor: values.at(-1)?.stepId ?? null,
365
979
  };
366
- },
980
+ }),
367
981
  };
368
982
  }
983
+ function filterStepData(step, resolveData) {
984
+ if (resolveData === 'none') {
985
+ const { input: _, output: __, ...rest } = step;
986
+ return { input: undefined, output: undefined, ...rest };
987
+ }
988
+ return step;
989
+ }
990
+ function filterRunData(run, resolveData) {
991
+ if (resolveData === 'none') {
992
+ const { input: _, output: __, ...rest } = run;
993
+ return { input: undefined, output: undefined, ...rest };
994
+ }
995
+ return run;
996
+ }
997
+ function filterHookData(hook, resolveData) {
998
+ if (resolveData === 'none' && 'metadata' in hook) {
999
+ const { metadata: _, ...rest } = hook;
1000
+ return { metadata: undefined, ...rest };
1001
+ }
1002
+ return hook;
1003
+ }
1004
+ function filterEventData(event, resolveData) {
1005
+ if (resolveData === 'none' && 'eventData' in event) {
1006
+ const { eventData: _, ...rest } = event;
1007
+ return rest;
1008
+ }
1009
+ return event;
1010
+ }
369
1011
  //# sourceMappingURL=storage.js.map