@gencow/core 0.1.24 → 0.1.25

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 (73) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +46 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/src/db.ts +0 -18
@@ -1,203 +1,194 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { mutation, query } from "./reactive.js";
3
3
  import {
4
- createWorkflowRealtimeToken,
5
- deserializeWorkflowValue,
6
- getWorkflowResumeActionName,
7
- getWorkflowRealtimeKey,
8
- serializeWorkflowValue,
4
+ createWorkflowRealtimeToken,
5
+ deserializeWorkflowValue,
6
+ getWorkflowResumeActionName,
7
+ getWorkflowRealtimeKey,
8
+ serializeWorkflowValue,
9
9
  } from "./workflow.js";
10
10
  import { GencowValidationError, v } from "./v.js";
11
11
  import type {
12
- WorkflowDerivedStatus,
13
- WorkflowSnapshot,
14
- WorkflowSignalResult,
15
- WorkflowStatus,
16
- WorkflowStepSnapshot,
17
- WorkflowSummary,
12
+ WorkflowDerivedStatus,
13
+ WorkflowSnapshot,
14
+ WorkflowSignalResult,
15
+ WorkflowStatus,
16
+ WorkflowStepSnapshot,
17
+ WorkflowSummary,
18
18
  } from "./workflow-types.js";
19
19
  import { deriveWorkflowStatus } from "./workflow-types.js";
20
20
 
21
21
  declare global {
22
- // eslint-disable-next-line no-var
23
- var __gencow_workflowsApiRegistered: boolean | undefined;
22
+ // eslint-disable-next-line no-var
23
+ var __gencow_workflowsApiRegistered: boolean | undefined;
24
24
  }
25
25
 
26
26
  type WorkflowRow = {
27
- id: string;
28
- name: string;
29
- args: unknown;
30
- status: WorkflowStatus;
31
- current_step: string | null;
32
- result: unknown;
33
- error: string | null;
34
- retry_count: number;
35
- max_retries: number;
36
- max_duration_ms: number;
37
- started_at: string | Date;
38
- updated_at: string | Date;
39
- completed_at: string | Date | null;
40
- realtime_token: string | null;
41
- user_id: string | null;
27
+ id: string;
28
+ name: string;
29
+ args: unknown;
30
+ status: WorkflowStatus;
31
+ current_step: string | null;
32
+ result: unknown;
33
+ error: string | null;
34
+ retry_count: number;
35
+ max_retries: number;
36
+ max_duration_ms: number;
37
+ started_at: string | Date;
38
+ updated_at: string | Date;
39
+ completed_at: string | Date | null;
40
+ realtime_token: string | null;
41
+ user_id: string | null;
42
42
  };
43
43
 
44
44
  type WorkflowStepRow = {
45
- step_name: string;
46
- status: WorkflowStatus;
47
- output: unknown;
48
- error: string | null;
49
- started_at: string | Date | null;
50
- updated_at: string | Date;
51
- completed_at: string | Date | null;
45
+ step_name: string;
46
+ status: WorkflowStatus;
47
+ output: unknown;
48
+ error: string | null;
49
+ started_at: string | Date | null;
50
+ updated_at: string | Date;
51
+ completed_at: string | Date | null;
52
52
  };
53
53
 
54
54
  type WorkflowSignalTargetRow = {
55
- id: string;
56
- name: string;
57
- status: WorkflowStatus;
58
- current_step: string | null;
59
- user_id: string | null;
55
+ id: string;
56
+ name: string;
57
+ status: WorkflowStatus;
58
+ current_step: string | null;
59
+ user_id: string | null;
60
60
  };
61
61
 
62
- const WORKFLOW_STATUSES = new Set<WorkflowStatus>([
63
- "pending",
64
- "running",
65
- "completed",
66
- "failed",
67
- ]);
68
- const WORKFLOW_DERIVED_PENDING_STATUSES = new Set<WorkflowDerivedStatus>([
69
- "queued",
70
- "waiting",
71
- "sleeping",
72
- ]);
62
+ const WORKFLOW_STATUSES = new Set<WorkflowStatus>(["pending", "running", "completed", "failed"]);
63
+ const WORKFLOW_DERIVED_PENDING_STATUSES = new Set<WorkflowDerivedStatus>(["queued", "waiting", "sleeping"]);
73
64
 
74
65
  type WorkflowDbLike = {
75
- execute: (query: unknown) => Promise<unknown>;
66
+ execute: (query: unknown) => Promise<unknown>;
76
67
  };
77
68
 
78
69
  function rowsFromResult<T>(result: unknown): T[] {
79
- if (Array.isArray(result)) return result as T[];
80
- if (result && typeof result === "object" && Array.isArray((result as { rows?: unknown[] }).rows)) {
81
- return (result as { rows: T[] }).rows;
82
- }
83
- return [];
70
+ if (Array.isArray(result)) return result as T[];
71
+ if (result && typeof result === "object" && Array.isArray((result as { rows?: unknown[] }).rows)) {
72
+ return (result as { rows: T[] }).rows;
73
+ }
74
+ return [];
84
75
  }
85
76
 
86
77
  function parseJsonField(value: unknown): unknown {
87
- if (typeof value !== "string") return value;
88
- try {
89
- return JSON.parse(value);
90
- } catch {
91
- return value;
92
- }
78
+ if (typeof value !== "string") return value;
79
+ try {
80
+ return JSON.parse(value);
81
+ } catch {
82
+ return value;
83
+ }
93
84
  }
94
85
 
95
86
  function toIsoString(value: string | Date): string {
96
- if (value instanceof Date) return value.toISOString();
97
- const parsed = new Date(value);
98
- return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
87
+ if (value instanceof Date) return value.toISOString();
88
+ const parsed = new Date(value);
89
+ return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
99
90
  }
100
91
 
101
92
  function toOptionalIsoString(value: string | Date | null): string | null {
102
- return value ? toIsoString(value) : null;
93
+ return value ? toIsoString(value) : null;
103
94
  }
104
95
 
105
96
  function mapWorkflowSummary(row: WorkflowRow): WorkflowSummary {
106
- return {
107
- id: row.id,
108
- name: row.name,
109
- status: row.status,
110
- derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
111
- currentStep: row.current_step,
112
- error: row.error,
113
- retryCount: row.retry_count,
114
- maxRetries: row.max_retries,
115
- maxDurationMs: Number(row.max_duration_ms),
116
- startedAt: toIsoString(row.started_at),
117
- updatedAt: toIsoString(row.updated_at),
118
- completedAt: toOptionalIsoString(row.completed_at),
119
- };
97
+ return {
98
+ id: row.id,
99
+ name: row.name,
100
+ status: row.status,
101
+ derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
102
+ currentStep: row.current_step,
103
+ error: row.error,
104
+ retryCount: row.retry_count,
105
+ maxRetries: row.max_retries,
106
+ maxDurationMs: Number(row.max_duration_ms),
107
+ startedAt: toIsoString(row.started_at),
108
+ updatedAt: toIsoString(row.updated_at),
109
+ completedAt: toOptionalIsoString(row.completed_at),
110
+ };
120
111
  }
121
112
 
122
113
  function mapWorkflowStep(row: WorkflowStepRow): WorkflowStepSnapshot {
123
- return {
124
- name: row.step_name,
125
- status: row.status,
126
- output: deserializeWorkflowValue(parseJsonField(row.output)),
127
- error: row.error,
128
- startedAt: toOptionalIsoString(row.started_at),
129
- updatedAt: toIsoString(row.updated_at),
130
- completedAt: toOptionalIsoString(row.completed_at),
131
- };
114
+ return {
115
+ name: row.step_name,
116
+ status: row.status,
117
+ output: deserializeWorkflowValue(parseJsonField(row.output)),
118
+ error: row.error,
119
+ startedAt: toOptionalIsoString(row.started_at),
120
+ updatedAt: toIsoString(row.updated_at),
121
+ completedAt: toOptionalIsoString(row.completed_at),
122
+ };
132
123
  }
133
124
 
134
125
  function normalizeListLimit(limit: number | undefined): number {
135
- if (limit == null) return 20;
136
- if (!Number.isFinite(limit)) {
137
- throw new GencowValidationError(`Argument "limit": expected a finite number, got ${limit}`);
138
- }
139
- return Math.max(1, Math.min(100, Math.floor(limit)));
126
+ if (limit == null) return 20;
127
+ if (!Number.isFinite(limit)) {
128
+ throw new GencowValidationError(`Argument "limit": expected a finite number, got ${limit}`);
129
+ }
130
+ return Math.max(1, Math.min(100, Math.floor(limit)));
140
131
  }
141
132
 
142
133
  function normalizeStatus(status: string | undefined): WorkflowStatus | undefined {
143
- if (status == null) return undefined;
144
- if (!WORKFLOW_STATUSES.has(status as WorkflowStatus)) {
145
- throw new GencowValidationError(
146
- `Argument "status": expected one of pending, running, completed, failed`
147
- );
148
- }
149
- return status as WorkflowStatus;
134
+ if (status == null) return undefined;
135
+ if (!WORKFLOW_STATUSES.has(status as WorkflowStatus)) {
136
+ throw new GencowValidationError(`Argument "status": expected one of pending, running, completed, failed`);
137
+ }
138
+ return status as WorkflowStatus;
150
139
  }
151
140
 
152
141
  function normalizeDerivedStatus(status: string | undefined): WorkflowDerivedStatus | undefined {
153
- if (status == null) return undefined;
154
- if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status as WorkflowDerivedStatus)) {
155
- return status as WorkflowDerivedStatus;
156
- }
157
- return normalizeStatus(status) as WorkflowDerivedStatus;
142
+ if (status == null) return undefined;
143
+ if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status as WorkflowDerivedStatus)) {
144
+ return status as WorkflowDerivedStatus;
145
+ }
146
+ return normalizeStatus(status) as WorkflowDerivedStatus;
158
147
  }
159
148
 
160
149
  function toWorkflowStatusFilter(status: WorkflowDerivedStatus | undefined): WorkflowStatus | undefined {
161
- if (status == null) return undefined;
162
- if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
163
- return "pending";
164
- }
165
- return status as WorkflowStatus;
150
+ if (status == null) return undefined;
151
+ if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
152
+ return "pending";
153
+ }
154
+ return status as WorkflowStatus;
166
155
  }
167
156
 
168
157
  async function ensureWorkflowRealtimeToken(
169
- db: WorkflowDbLike,
170
- workflowId: string,
171
- currentToken: string | null
158
+ db: WorkflowDbLike,
159
+ workflowId: string,
160
+ currentToken: string | null,
172
161
  ): Promise<string | null> {
173
- if (currentToken && currentToken.trim() !== "") return currentToken;
162
+ if (currentToken && currentToken.trim() !== "") return currentToken;
174
163
 
175
- const nextToken = createWorkflowRealtimeToken();
176
- const updateResult = await db.execute(sql`
164
+ const nextToken = createWorkflowRealtimeToken();
165
+ const updateResult = await db.execute(sql`
177
166
  UPDATE _gencow_workflows
178
167
  SET realtime_token = ${nextToken}
179
168
  WHERE id = ${workflowId}
180
169
  AND (realtime_token IS NULL OR realtime_token = '')
181
170
  RETURNING realtime_token
182
171
  `);
183
- const updatedToken = rowsFromResult<{ realtime_token: string | null }>(updateResult)[0]?.realtime_token ?? null;
184
- if (updatedToken && updatedToken.trim() !== "") return updatedToken;
172
+ const updatedToken =
173
+ rowsFromResult<{ realtime_token: string | null }>(updateResult)[0]?.realtime_token ?? null;
174
+ if (updatedToken && updatedToken.trim() !== "") return updatedToken;
185
175
 
186
- const rereadResult = await db.execute(sql`
176
+ const rereadResult = await db.execute(sql`
187
177
  SELECT realtime_token
188
178
  FROM _gencow_workflows
189
179
  WHERE id = ${workflowId}
190
180
  LIMIT 1
191
181
  `);
192
- const rereadToken = rowsFromResult<{ realtime_token: string | null }>(rereadResult)[0]?.realtime_token ?? null;
193
- return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
182
+ const rereadToken =
183
+ rowsFromResult<{ realtime_token: string | null }>(rereadResult)[0]?.realtime_token ?? null;
184
+ return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
194
185
  }
195
186
 
196
187
  async function loadWorkflowSignalTarget(
197
- db: WorkflowDbLike,
198
- workflowId: string
188
+ db: WorkflowDbLike,
189
+ workflowId: string,
199
190
  ): Promise<WorkflowSignalTargetRow | null> {
200
- const result = await db.execute(sql`
191
+ const result = await db.execute(sql`
201
192
  SELECT
202
193
  id,
203
194
  name,
@@ -208,18 +199,18 @@ async function loadWorkflowSignalTarget(
208
199
  WHERE id = ${workflowId}
209
200
  LIMIT 1
210
201
  `);
211
- return rowsFromResult<WorkflowSignalTargetRow>(result)[0] ?? null;
202
+ return rowsFromResult<WorkflowSignalTargetRow>(result)[0] ?? null;
212
203
  }
213
204
 
214
205
  export async function loadWorkflowSnapshot(
215
- db: WorkflowDbLike,
216
- workflowId: string,
217
- options?: {
218
- viewerUserId?: string | null;
219
- requireViewerMatch?: boolean;
220
- }
206
+ db: WorkflowDbLike,
207
+ workflowId: string,
208
+ options?: {
209
+ viewerUserId?: string | null;
210
+ requireViewerMatch?: boolean;
211
+ },
221
212
  ): Promise<WorkflowSnapshot | null> {
222
- const workflowResult = await db.execute(sql`
213
+ const workflowResult = await db.execute(sql`
223
214
  SELECT
224
215
  id,
225
216
  name,
@@ -240,18 +231,18 @@ export async function loadWorkflowSnapshot(
240
231
  WHERE id = ${workflowId}
241
232
  LIMIT 1
242
233
  `);
243
- const row = rowsFromResult<WorkflowRow>(workflowResult)[0] ?? null;
244
- if (!row) return null;
234
+ const row = rowsFromResult<WorkflowRow>(workflowResult)[0] ?? null;
235
+ if (!row) return null;
245
236
 
246
- const viewerUserId = options?.viewerUserId ?? null;
247
- if (options?.requireViewerMatch && row.user_id && row.user_id !== viewerUserId) {
248
- return null;
249
- }
237
+ const viewerUserId = options?.viewerUserId ?? null;
238
+ if (options?.requireViewerMatch && row.user_id && row.user_id !== viewerUserId) {
239
+ return null;
240
+ }
250
241
 
251
- const realtimeToken = await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token);
252
- if (!realtimeToken) return null;
242
+ const realtimeToken = await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token);
243
+ if (!realtimeToken) return null;
253
244
 
254
- const stepsResult = await db.execute(sql`
245
+ const stepsResult = await db.execute(sql`
255
246
  SELECT
256
247
  step_name,
257
248
  status,
@@ -265,66 +256,66 @@ export async function loadWorkflowSnapshot(
265
256
  ORDER BY COALESCE(started_at, updated_at) ASC, step_name ASC
266
257
  `);
267
258
 
268
- return {
269
- ...mapWorkflowSummary(row),
270
- args: deserializeWorkflowValue(parseJsonField(row.args)),
271
- result: deserializeWorkflowValue(parseJsonField(row.result)),
272
- steps: rowsFromResult<WorkflowStepRow>(stepsResult).map(mapWorkflowStep),
273
- realtimeKey: getWorkflowRealtimeKey(row.id, realtimeToken),
274
- };
259
+ return {
260
+ ...mapWorkflowSummary(row),
261
+ args: deserializeWorkflowValue(parseJsonField(row.args)),
262
+ result: deserializeWorkflowValue(parseJsonField(row.result)),
263
+ steps: rowsFromResult<WorkflowStepRow>(stepsResult).map(mapWorkflowStep),
264
+ realtimeKey: getWorkflowRealtimeKey(row.id, realtimeToken),
265
+ };
275
266
  }
276
267
 
277
268
  export function registerWorkflowsApi(): void {
278
- if (globalThis.__gencow_workflowsApiRegistered) return;
279
- globalThis.__gencow_workflowsApiRegistered = true;
280
-
281
- query("workflows.get", {
282
- args: { id: v.string() },
283
- public: true,
284
- handler: async (ctx, args): Promise<WorkflowSnapshot | null> => {
285
- return loadWorkflowSnapshot(ctx.unsafeDb, args.id, {
286
- viewerUserId: ctx.auth.getUserIdentity()?.id ?? null,
287
- requireViewerMatch: true,
288
- });
289
- },
290
- });
291
-
292
- mutation("workflows.signal", {
293
- args: {
294
- id: v.string(),
295
- event: v.string(),
296
- payload: v.optional(v.any()),
297
- },
298
- public: true,
299
- handler: async (ctx, args): Promise<WorkflowSignalResult> => {
300
- const normalizedEvent = args.event.trim();
301
- if (!normalizedEvent) {
302
- throw new GencowValidationError(`Argument "event": expected a non-empty string`);
303
- }
304
-
305
- const workflow = await loadWorkflowSignalTarget(ctx.unsafeDb, args.id);
306
- const viewerUserId = ctx.auth.getUserIdentity()?.id ?? null;
307
-
308
- if (!workflow || (workflow.user_id && workflow.user_id !== viewerUserId)) {
309
- return {
310
- ok: false,
311
- workflowId: args.id,
312
- event: normalizedEvent,
313
- scheduledJobId: null,
314
- };
315
- }
316
-
317
- if (workflow.status === "completed" || workflow.status === "failed") {
318
- return {
319
- ok: false,
320
- workflowId: workflow.id,
321
- event: normalizedEvent,
322
- scheduledJobId: null,
323
- };
324
- }
325
-
326
- const persistedPayload = serializeWorkflowValue(args.payload);
327
- await ctx.unsafeDb.execute(sql`
269
+ if (globalThis.__gencow_workflowsApiRegistered) return;
270
+ globalThis.__gencow_workflowsApiRegistered = true;
271
+
272
+ query("workflows.get", {
273
+ args: { id: v.string() },
274
+ public: true,
275
+ handler: async (ctx, args): Promise<WorkflowSnapshot | null> => {
276
+ return loadWorkflowSnapshot(ctx.unsafeDb, args.id, {
277
+ viewerUserId: ctx.auth.getUserIdentity()?.id ?? null,
278
+ requireViewerMatch: true,
279
+ });
280
+ },
281
+ });
282
+
283
+ mutation("workflows.signal", {
284
+ args: {
285
+ id: v.string(),
286
+ event: v.string(),
287
+ payload: v.optional(v.any()),
288
+ },
289
+ public: true,
290
+ handler: async (ctx, args): Promise<WorkflowSignalResult> => {
291
+ const normalizedEvent = args.event.trim();
292
+ if (!normalizedEvent) {
293
+ throw new GencowValidationError(`Argument "event": expected a non-empty string`);
294
+ }
295
+
296
+ const workflow = await loadWorkflowSignalTarget(ctx.unsafeDb, args.id);
297
+ const viewerUserId = ctx.auth.getUserIdentity()?.id ?? null;
298
+
299
+ if (!workflow || (workflow.user_id && workflow.user_id !== viewerUserId)) {
300
+ return {
301
+ ok: false,
302
+ workflowId: args.id,
303
+ event: normalizedEvent,
304
+ scheduledJobId: null,
305
+ };
306
+ }
307
+
308
+ if (workflow.status === "completed" || workflow.status === "failed") {
309
+ return {
310
+ ok: false,
311
+ workflowId: workflow.id,
312
+ event: normalizedEvent,
313
+ scheduledJobId: null,
314
+ };
315
+ }
316
+
317
+ const persistedPayload = serializeWorkflowValue(args.payload);
318
+ await ctx.unsafeDb.execute(sql`
328
319
  INSERT INTO _gencow_workflow_events (
329
320
  id,
330
321
  workflow_id,
@@ -339,41 +330,40 @@ export function registerWorkflowsApi(): void {
339
330
  )
340
331
  `);
341
332
 
342
- let scheduledJobId: string | null = null;
343
- if (workflow.status === "pending" && workflow.current_step?.startsWith("wait:")) {
344
- try {
345
- scheduledJobId = ctx.scheduler.runAfter(
346
- 0,
347
- getWorkflowResumeActionName(workflow.name),
348
- { workflowId: workflow.id }
349
- );
350
- } catch {
351
- scheduledJobId = null;
352
- }
353
- }
354
-
355
- return {
356
- ok: true,
357
- workflowId: workflow.id,
358
- event: normalizedEvent,
359
- scheduledJobId,
360
- };
361
- },
362
- });
363
-
364
- query("workflows.list", {
365
- args: {
366
- limit: v.optional(v.number()),
367
- status: v.optional(v.string()),
368
- },
369
- handler: async (ctx, args): Promise<WorkflowSummary[]> => {
370
- const userId = ctx.auth.requireAuth().id;
371
- const limit = normalizeListLimit(args.limit);
372
- const requestedStatus = normalizeDerivedStatus(args.status);
373
- const status = toWorkflowStatusFilter(requestedStatus);
374
-
375
- const result = status == null
376
- ? await ctx.unsafeDb.execute(sql`
333
+ let scheduledJobId: string | null = null;
334
+ if (workflow.status === "pending" && workflow.current_step?.startsWith("wait:")) {
335
+ try {
336
+ scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflow.name), {
337
+ workflowId: workflow.id,
338
+ });
339
+ } catch {
340
+ scheduledJobId = null;
341
+ }
342
+ }
343
+
344
+ return {
345
+ ok: true,
346
+ workflowId: workflow.id,
347
+ event: normalizedEvent,
348
+ scheduledJobId,
349
+ };
350
+ },
351
+ });
352
+
353
+ query("workflows.list", {
354
+ args: {
355
+ limit: v.optional(v.number()),
356
+ status: v.optional(v.string()),
357
+ },
358
+ handler: async (ctx, args): Promise<WorkflowSummary[]> => {
359
+ const userId = ctx.auth.requireAuth().id;
360
+ const limit = normalizeListLimit(args.limit);
361
+ const requestedStatus = normalizeDerivedStatus(args.status);
362
+ const status = toWorkflowStatusFilter(requestedStatus);
363
+
364
+ const result =
365
+ status == null
366
+ ? await ctx.unsafeDb.execute(sql`
377
367
  SELECT
378
368
  id,
379
369
  name,
@@ -394,7 +384,7 @@ export function registerWorkflowsApi(): void {
394
384
  ORDER BY started_at DESC
395
385
  LIMIT ${limit}
396
386
  `)
397
- : await ctx.unsafeDb.execute(sql`
387
+ : await ctx.unsafeDb.execute(sql`
398
388
  SELECT
399
389
  id,
400
390
  name,
@@ -417,9 +407,9 @@ export function registerWorkflowsApi(): void {
417
407
  LIMIT ${limit}
418
408
  `);
419
409
 
420
- return rowsFromResult<WorkflowRow>(result)
421
- .map(mapWorkflowSummary)
422
- .filter((row) => requestedStatus == null || row.derivedStatus === requestedStatus);
423
- },
424
- });
410
+ return rowsFromResult<WorkflowRow>(result)
411
+ .map(mapWorkflowSummary)
412
+ .filter((row) => requestedStatus == null || row.derivedStatus === requestedStatus);
413
+ },
414
+ });
425
415
  }
package/src/db.ts DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * @deprecated — 레거시 싱글톤 DB 인스턴스.
3
- * 새 코드에서는 ctx.db를 사용하세요.
4
- * 서버의 createDatabase() (database.ts)가 실제 DB 연결을 관리합니다.
5
- */
6
- import { PGlite } from "@electric-sql/pglite";
7
- import { drizzle } from "drizzle-orm/pglite";
8
-
9
- let pgliteInstance: PGlite | null = null;
10
-
11
- /** @deprecated Use ctx.db instead */
12
- export async function createDb(dataDir: string = "./data") {
13
- if (!pgliteInstance) {
14
- pgliteInstance = new PGlite(dataDir);
15
- }
16
- const db = drizzle(pgliteInstance);
17
- return { db, client: pgliteInstance };
18
- }