@gencow/core 0.1.23 → 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.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow-types.d.ts +81 -0
- package/dist/workflow-types.js +12 -0
- package/dist/workflow.d.ts +30 -0
- package/dist/workflow.js +150 -0
- package/dist/workflows-api.d.ts +13 -0
- package/dist/workflows-api.js +321 -0
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -114
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +606 -0
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +71 -6
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +108 -0
- package/src/workflow.ts +188 -0
- package/src/workflows-api.ts +415 -0
- package/src/db.ts +0 -18
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { mutation, query } from "./reactive.js";
|
|
3
|
+
import {
|
|
4
|
+
createWorkflowRealtimeToken,
|
|
5
|
+
deserializeWorkflowValue,
|
|
6
|
+
getWorkflowResumeActionName,
|
|
7
|
+
getWorkflowRealtimeKey,
|
|
8
|
+
serializeWorkflowValue,
|
|
9
|
+
} from "./workflow.js";
|
|
10
|
+
import { GencowValidationError, v } from "./v.js";
|
|
11
|
+
import type {
|
|
12
|
+
WorkflowDerivedStatus,
|
|
13
|
+
WorkflowSnapshot,
|
|
14
|
+
WorkflowSignalResult,
|
|
15
|
+
WorkflowStatus,
|
|
16
|
+
WorkflowStepSnapshot,
|
|
17
|
+
WorkflowSummary,
|
|
18
|
+
} from "./workflow-types.js";
|
|
19
|
+
import { deriveWorkflowStatus } from "./workflow-types.js";
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
// eslint-disable-next-line no-var
|
|
23
|
+
var __gencow_workflowsApiRegistered: boolean | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
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;
|
|
42
|
+
};
|
|
43
|
+
|
|
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;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type WorkflowSignalTargetRow = {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
status: WorkflowStatus;
|
|
58
|
+
current_step: string | null;
|
|
59
|
+
user_id: string | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const WORKFLOW_STATUSES = new Set<WorkflowStatus>(["pending", "running", "completed", "failed"]);
|
|
63
|
+
const WORKFLOW_DERIVED_PENDING_STATUSES = new Set<WorkflowDerivedStatus>(["queued", "waiting", "sleeping"]);
|
|
64
|
+
|
|
65
|
+
type WorkflowDbLike = {
|
|
66
|
+
execute: (query: unknown) => Promise<unknown>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function rowsFromResult<T>(result: unknown): T[] {
|
|
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 [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseJsonField(value: unknown): unknown {
|
|
78
|
+
if (typeof value !== "string") return value;
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(value);
|
|
81
|
+
} catch {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toIsoString(value: string | Date): string {
|
|
87
|
+
if (value instanceof Date) return value.toISOString();
|
|
88
|
+
const parsed = new Date(value);
|
|
89
|
+
return Number.isFinite(parsed.getTime()) ? parsed.toISOString() : String(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toOptionalIsoString(value: string | Date | null): string | null {
|
|
93
|
+
return value ? toIsoString(value) : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function mapWorkflowSummary(row: WorkflowRow): WorkflowSummary {
|
|
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
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mapWorkflowStep(row: WorkflowStepRow): WorkflowStepSnapshot {
|
|
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
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeListLimit(limit: number | undefined): number {
|
|
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)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeStatus(status: string | undefined): WorkflowStatus | undefined {
|
|
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;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeDerivedStatus(status: string | undefined): WorkflowDerivedStatus | undefined {
|
|
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;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function toWorkflowStatusFilter(status: WorkflowDerivedStatus | undefined): WorkflowStatus | undefined {
|
|
150
|
+
if (status == null) return undefined;
|
|
151
|
+
if (WORKFLOW_DERIVED_PENDING_STATUSES.has(status)) {
|
|
152
|
+
return "pending";
|
|
153
|
+
}
|
|
154
|
+
return status as WorkflowStatus;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function ensureWorkflowRealtimeToken(
|
|
158
|
+
db: WorkflowDbLike,
|
|
159
|
+
workflowId: string,
|
|
160
|
+
currentToken: string | null,
|
|
161
|
+
): Promise<string | null> {
|
|
162
|
+
if (currentToken && currentToken.trim() !== "") return currentToken;
|
|
163
|
+
|
|
164
|
+
const nextToken = createWorkflowRealtimeToken();
|
|
165
|
+
const updateResult = await db.execute(sql`
|
|
166
|
+
UPDATE _gencow_workflows
|
|
167
|
+
SET realtime_token = ${nextToken}
|
|
168
|
+
WHERE id = ${workflowId}
|
|
169
|
+
AND (realtime_token IS NULL OR realtime_token = '')
|
|
170
|
+
RETURNING realtime_token
|
|
171
|
+
`);
|
|
172
|
+
const updatedToken =
|
|
173
|
+
rowsFromResult<{ realtime_token: string | null }>(updateResult)[0]?.realtime_token ?? null;
|
|
174
|
+
if (updatedToken && updatedToken.trim() !== "") return updatedToken;
|
|
175
|
+
|
|
176
|
+
const rereadResult = await db.execute(sql`
|
|
177
|
+
SELECT realtime_token
|
|
178
|
+
FROM _gencow_workflows
|
|
179
|
+
WHERE id = ${workflowId}
|
|
180
|
+
LIMIT 1
|
|
181
|
+
`);
|
|
182
|
+
const rereadToken =
|
|
183
|
+
rowsFromResult<{ realtime_token: string | null }>(rereadResult)[0]?.realtime_token ?? null;
|
|
184
|
+
return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function loadWorkflowSignalTarget(
|
|
188
|
+
db: WorkflowDbLike,
|
|
189
|
+
workflowId: string,
|
|
190
|
+
): Promise<WorkflowSignalTargetRow | null> {
|
|
191
|
+
const result = await db.execute(sql`
|
|
192
|
+
SELECT
|
|
193
|
+
id,
|
|
194
|
+
name,
|
|
195
|
+
status,
|
|
196
|
+
current_step,
|
|
197
|
+
user_id
|
|
198
|
+
FROM _gencow_workflows
|
|
199
|
+
WHERE id = ${workflowId}
|
|
200
|
+
LIMIT 1
|
|
201
|
+
`);
|
|
202
|
+
return rowsFromResult<WorkflowSignalTargetRow>(result)[0] ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function loadWorkflowSnapshot(
|
|
206
|
+
db: WorkflowDbLike,
|
|
207
|
+
workflowId: string,
|
|
208
|
+
options?: {
|
|
209
|
+
viewerUserId?: string | null;
|
|
210
|
+
requireViewerMatch?: boolean;
|
|
211
|
+
},
|
|
212
|
+
): Promise<WorkflowSnapshot | null> {
|
|
213
|
+
const workflowResult = await db.execute(sql`
|
|
214
|
+
SELECT
|
|
215
|
+
id,
|
|
216
|
+
name,
|
|
217
|
+
args,
|
|
218
|
+
status,
|
|
219
|
+
current_step,
|
|
220
|
+
result,
|
|
221
|
+
error,
|
|
222
|
+
retry_count,
|
|
223
|
+
max_retries,
|
|
224
|
+
max_duration_ms,
|
|
225
|
+
started_at,
|
|
226
|
+
updated_at,
|
|
227
|
+
completed_at,
|
|
228
|
+
realtime_token,
|
|
229
|
+
user_id
|
|
230
|
+
FROM _gencow_workflows
|
|
231
|
+
WHERE id = ${workflowId}
|
|
232
|
+
LIMIT 1
|
|
233
|
+
`);
|
|
234
|
+
const row = rowsFromResult<WorkflowRow>(workflowResult)[0] ?? null;
|
|
235
|
+
if (!row) return null;
|
|
236
|
+
|
|
237
|
+
const viewerUserId = options?.viewerUserId ?? null;
|
|
238
|
+
if (options?.requireViewerMatch && row.user_id && row.user_id !== viewerUserId) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const realtimeToken = await ensureWorkflowRealtimeToken(db, workflowId, row.realtime_token);
|
|
243
|
+
if (!realtimeToken) return null;
|
|
244
|
+
|
|
245
|
+
const stepsResult = await db.execute(sql`
|
|
246
|
+
SELECT
|
|
247
|
+
step_name,
|
|
248
|
+
status,
|
|
249
|
+
output,
|
|
250
|
+
error,
|
|
251
|
+
started_at,
|
|
252
|
+
updated_at,
|
|
253
|
+
completed_at
|
|
254
|
+
FROM _gencow_workflow_steps
|
|
255
|
+
WHERE workflow_id = ${workflowId}
|
|
256
|
+
ORDER BY COALESCE(started_at, updated_at) ASC, step_name ASC
|
|
257
|
+
`);
|
|
258
|
+
|
|
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
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function registerWorkflowsApi(): void {
|
|
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`
|
|
319
|
+
INSERT INTO _gencow_workflow_events (
|
|
320
|
+
id,
|
|
321
|
+
workflow_id,
|
|
322
|
+
event_name,
|
|
323
|
+
payload
|
|
324
|
+
)
|
|
325
|
+
VALUES (
|
|
326
|
+
${crypto.randomUUID()},
|
|
327
|
+
${workflow.id},
|
|
328
|
+
${normalizedEvent},
|
|
329
|
+
${JSON.stringify(persistedPayload)}::jsonb
|
|
330
|
+
)
|
|
331
|
+
`);
|
|
332
|
+
|
|
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`
|
|
367
|
+
SELECT
|
|
368
|
+
id,
|
|
369
|
+
name,
|
|
370
|
+
args,
|
|
371
|
+
status,
|
|
372
|
+
current_step,
|
|
373
|
+
result,
|
|
374
|
+
error,
|
|
375
|
+
retry_count,
|
|
376
|
+
max_retries,
|
|
377
|
+
max_duration_ms,
|
|
378
|
+
started_at,
|
|
379
|
+
updated_at,
|
|
380
|
+
completed_at,
|
|
381
|
+
user_id
|
|
382
|
+
FROM _gencow_workflows
|
|
383
|
+
WHERE user_id = ${userId}
|
|
384
|
+
ORDER BY started_at DESC
|
|
385
|
+
LIMIT ${limit}
|
|
386
|
+
`)
|
|
387
|
+
: await ctx.unsafeDb.execute(sql`
|
|
388
|
+
SELECT
|
|
389
|
+
id,
|
|
390
|
+
name,
|
|
391
|
+
args,
|
|
392
|
+
status,
|
|
393
|
+
current_step,
|
|
394
|
+
result,
|
|
395
|
+
error,
|
|
396
|
+
retry_count,
|
|
397
|
+
max_retries,
|
|
398
|
+
max_duration_ms,
|
|
399
|
+
started_at,
|
|
400
|
+
updated_at,
|
|
401
|
+
completed_at,
|
|
402
|
+
user_id
|
|
403
|
+
FROM _gencow_workflows
|
|
404
|
+
WHERE user_id = ${userId}
|
|
405
|
+
AND status = ${status}
|
|
406
|
+
ORDER BY started_at DESC
|
|
407
|
+
LIMIT ${limit}
|
|
408
|
+
`);
|
|
409
|
+
|
|
410
|
+
return rowsFromResult<WorkflowRow>(result)
|
|
411
|
+
.map(mapWorkflowSummary)
|
|
412
|
+
.filter((row) => requestedStatus == null || row.derivedStatus === requestedStatus);
|
|
413
|
+
},
|
|
414
|
+
});
|
|
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
|
-
}
|