@gencow/core 0.1.27 → 0.1.29
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/auth-config.d.ts +92 -5
- package/dist/config.d.ts +107 -0
- package/dist/config.js +12 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.js +3 -0
- package/dist/crud.d.ts +5 -5
- package/dist/crud.js +19 -35
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/http-action.d.ts +77 -0
- package/dist/http-action.js +41 -0
- package/dist/index.d.ts +30 -5
- package/dist/index.js +15 -2
- package/dist/platform-capacity-profile.d.ts +19 -0
- package/dist/platform-capacity-profile.js +94 -0
- package/dist/procedure.d.ts +58 -0
- package/dist/procedure.js +115 -0
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1466 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive-mutation-types.d.ts +11 -0
- package/dist/reactive-mutation-types.js +1 -0
- package/dist/reactive-mutation.d.ts +51 -0
- package/dist/reactive-mutation.js +75 -0
- package/dist/reactive-query-types.d.ts +12 -0
- package/dist/reactive-query-types.js +1 -0
- package/dist/reactive-query.d.ts +14 -0
- package/dist/reactive-query.js +28 -0
- package/dist/reactive-realtime.d.ts +48 -0
- package/dist/reactive-realtime.js +236 -0
- package/dist/reactive.d.ts +29 -5
- package/dist/reactive.js +65 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-metering.d.ts +13 -0
- package/dist/storage-metering.js +18 -0
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +5 -27
- package/dist/storage.js +30 -22
- package/dist/wake-app-result.d.ts +22 -0
- package/dist/wake-app-result.js +11 -0
- package/dist/workflow-types.d.ts +16 -2
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.js +136 -11
- package/dist/workflows-api.js +71 -3
- package/package.json +11 -7
- package/src/auth-config.ts +104 -3
- package/src/config.ts +119 -0
- package/src/context.ts +152 -0
- package/src/crud.ts +18 -35
- package/src/document-types.ts +102 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/http-action.ts +101 -0
- package/src/index.ts +142 -19
- package/src/platform-capacity-profile.ts +114 -0
- package/src/procedure.ts +283 -0
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive-mutation-types.ts +13 -0
- package/src/reactive-mutation.ts +115 -0
- package/src/reactive-query-types.ts +14 -0
- package/src/reactive-query.ts +48 -0
- package/src/reactive-realtime.ts +267 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +6 -2
- package/src/storage-metering.ts +35 -0
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +44 -53
- package/src/wake-app-result.ts +37 -0
- package/src/workflow-types.ts +16 -2
- package/src/workflow.ts +166 -12
- package/src/workflows-api.ts +82 -3
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
- package/src/reactive.ts +0 -580
package/src/workflow.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
|
-
import { mutation } from "./reactive.js";
|
|
3
|
-
import type { MutationDef } from "./reactive.js";
|
|
2
|
+
import { mutation } from "./reactive-mutation.js";
|
|
3
|
+
import type { MutationDef } from "./reactive-mutation-types.js";
|
|
4
4
|
import type {
|
|
5
5
|
WorkflowDef,
|
|
6
6
|
WorkflowDuration,
|
|
@@ -82,9 +82,137 @@ export function parseWorkflowDurationMs(raw: WorkflowDuration, label = "workflow
|
|
|
82
82
|
return parseDurationString(raw, label);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function normalizeMaxDurationMs(maxDuration: WorkflowDuration | undefined): number {
|
|
85
|
+
function normalizeMaxDurationMs(maxDuration: WorkflowDuration | undefined, label = "workflow() maxDuration"): number {
|
|
86
86
|
if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
|
|
87
|
-
return parseWorkflowDurationMs(maxDuration,
|
|
87
|
+
return parseWorkflowDurationMs(maxDuration, label);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeOptionalDurationMs(
|
|
91
|
+
duration: WorkflowDuration | undefined,
|
|
92
|
+
label: string,
|
|
93
|
+
): number | null {
|
|
94
|
+
return duration == null ? null : parseWorkflowDurationMs(duration, label);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeConcurrency(concurrency: number | undefined): number | null {
|
|
98
|
+
if (concurrency == null) return null;
|
|
99
|
+
if (!Number.isFinite(concurrency) || concurrency <= 0) {
|
|
100
|
+
throw new Error(`workflow() concurrency must be a positive finite number, got "${concurrency}"`);
|
|
101
|
+
}
|
|
102
|
+
return Math.floor(concurrency);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isMissingWorkflowV2SchemaError(error: unknown): boolean {
|
|
106
|
+
const code =
|
|
107
|
+
error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
108
|
+
const cause =
|
|
109
|
+
error && typeof error === "object" && "cause" in error ? (error as { cause?: unknown }).cause : null;
|
|
110
|
+
const causeCode =
|
|
111
|
+
cause && typeof cause === "object" && "code" in cause ? String((cause as { code?: unknown }).code) : "";
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
return (
|
|
114
|
+
code === "42P01" ||
|
|
115
|
+
code === "42703" ||
|
|
116
|
+
causeCode === "42P01" ||
|
|
117
|
+
causeCode === "42703" ||
|
|
118
|
+
message.includes('relation "_gencow_workflow_runs_v2" does not exist') ||
|
|
119
|
+
message.includes("relation _gencow_workflow_runs_v2 does not exist") ||
|
|
120
|
+
message.includes('relation "_gencow_workflow_outbox_v2" does not exist') ||
|
|
121
|
+
message.includes("relation _gencow_workflow_outbox_v2 does not exist") ||
|
|
122
|
+
message.includes('column "max_active_duration_ms"') ||
|
|
123
|
+
message.includes('column "retry_count"') ||
|
|
124
|
+
message.includes('column "user_id"')
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function tryInsertWorkflowV2WakeOutbox(
|
|
129
|
+
db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> },
|
|
130
|
+
workflowId: string,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
await db.execute(sql`
|
|
134
|
+
INSERT INTO _gencow_workflow_outbox_v2 (
|
|
135
|
+
id,
|
|
136
|
+
run_id,
|
|
137
|
+
kind,
|
|
138
|
+
available_at,
|
|
139
|
+
status
|
|
140
|
+
)
|
|
141
|
+
VALUES (
|
|
142
|
+
${`start:${workflowId}`},
|
|
143
|
+
${workflowId},
|
|
144
|
+
'wake_run',
|
|
145
|
+
NOW(),
|
|
146
|
+
'pending'
|
|
147
|
+
)
|
|
148
|
+
ON CONFLICT (id) DO NOTHING
|
|
149
|
+
`);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!isMissingWorkflowV2SchemaError(error)) throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function tryInsertWorkflowV2Run(options: {
|
|
156
|
+
db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> };
|
|
157
|
+
workflowId: string;
|
|
158
|
+
workflowName: string;
|
|
159
|
+
workflowVersion?: string | null;
|
|
160
|
+
args: unknown;
|
|
161
|
+
userId: string | null;
|
|
162
|
+
maxActiveDurationMs: number;
|
|
163
|
+
lifecycleTimeoutMs: number | null;
|
|
164
|
+
maxRetries: number;
|
|
165
|
+
}): Promise<boolean> {
|
|
166
|
+
try {
|
|
167
|
+
await options.db.execute(sql`
|
|
168
|
+
INSERT INTO _gencow_workflow_runs_v2 (
|
|
169
|
+
id,
|
|
170
|
+
workflow_name,
|
|
171
|
+
workflow_version,
|
|
172
|
+
args_json,
|
|
173
|
+
user_id,
|
|
174
|
+
max_active_duration_ms,
|
|
175
|
+
lifecycle_deadline_at,
|
|
176
|
+
retry_count,
|
|
177
|
+
max_retries,
|
|
178
|
+
max_attempts
|
|
179
|
+
)
|
|
180
|
+
VALUES (
|
|
181
|
+
${options.workflowId},
|
|
182
|
+
${options.workflowName},
|
|
183
|
+
${options.workflowVersion ?? null},
|
|
184
|
+
${JSON.stringify(options.args)}::jsonb,
|
|
185
|
+
${options.userId},
|
|
186
|
+
${options.maxActiveDurationMs},
|
|
187
|
+
CASE
|
|
188
|
+
WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
|
|
189
|
+
ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
|
|
190
|
+
END,
|
|
191
|
+
0,
|
|
192
|
+
${options.maxRetries},
|
|
193
|
+
${options.maxRetries + 1}
|
|
194
|
+
)
|
|
195
|
+
`);
|
|
196
|
+
await tryInsertWorkflowV2WakeOutbox(options.db, options.workflowId);
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (isMissingWorkflowV2SchemaError(error)) return false;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function tryDeleteWorkflowV2Run(
|
|
205
|
+
db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> },
|
|
206
|
+
workflowId: string,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
await db.execute(sql`
|
|
210
|
+
DELETE FROM _gencow_workflow_runs_v2
|
|
211
|
+
WHERE id = ${workflowId}
|
|
212
|
+
`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (!isMissingWorkflowV2SchemaError(error)) throw error;
|
|
215
|
+
}
|
|
88
216
|
}
|
|
89
217
|
|
|
90
218
|
export function getWorkflowResumeActionName(name: string): string {
|
|
@@ -119,14 +247,25 @@ export function workflow<TSchema = any, TReturn = any>(
|
|
|
119
247
|
): MutationDef<TSchema, WorkflowStartResult> {
|
|
120
248
|
registerWorkflowsApi();
|
|
121
249
|
|
|
122
|
-
const
|
|
250
|
+
const maxActiveDurationMs = normalizeMaxDurationMs(
|
|
251
|
+
options.maxActiveDuration ?? options.maxDuration,
|
|
252
|
+
options.maxActiveDuration == null ? "workflow() maxDuration" : "workflow() maxActiveDuration",
|
|
253
|
+
);
|
|
254
|
+
const lifecycleTimeoutMs = normalizeOptionalDurationMs(
|
|
255
|
+
options.lifecycleTimeout,
|
|
256
|
+
"workflow() lifecycleTimeout",
|
|
257
|
+
);
|
|
123
258
|
const maxRetries = clampRetries(options.retries);
|
|
124
259
|
|
|
125
260
|
const def: WorkflowDef<TSchema, TReturn> = {
|
|
126
261
|
name,
|
|
127
262
|
argsSchema: options.args,
|
|
128
263
|
isPublic: options.public === true,
|
|
129
|
-
|
|
264
|
+
version: options.version,
|
|
265
|
+
maxDurationMs: maxActiveDurationMs,
|
|
266
|
+
maxActiveDurationMs,
|
|
267
|
+
lifecycleTimeoutMs,
|
|
268
|
+
concurrency: normalizeConcurrency(options.concurrency),
|
|
130
269
|
maxRetries,
|
|
131
270
|
handler: options.handler,
|
|
132
271
|
};
|
|
@@ -159,16 +298,28 @@ export function workflow<TSchema = any, TReturn = any>(
|
|
|
159
298
|
${workflowId},
|
|
160
299
|
${name},
|
|
161
300
|
${JSON.stringify(persistedArgs)}::jsonb,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
301
|
+
${realtimeToken},
|
|
302
|
+
'pending',
|
|
303
|
+
0,
|
|
304
|
+
${maxRetries},
|
|
305
|
+
${maxActiveDurationMs},
|
|
306
|
+
${ownerId}
|
|
168
307
|
)
|
|
169
308
|
`);
|
|
309
|
+
let insertedWorkflowV2 = false;
|
|
170
310
|
|
|
171
311
|
try {
|
|
312
|
+
insertedWorkflowV2 = await tryInsertWorkflowV2Run({
|
|
313
|
+
db: ctx.unsafeDb,
|
|
314
|
+
workflowId,
|
|
315
|
+
workflowName: name,
|
|
316
|
+
workflowVersion: options.version ?? null,
|
|
317
|
+
args: persistedArgs,
|
|
318
|
+
userId: ownerId,
|
|
319
|
+
maxActiveDurationMs,
|
|
320
|
+
lifecycleTimeoutMs,
|
|
321
|
+
maxRetries,
|
|
322
|
+
});
|
|
172
323
|
const scheduledJobId = ctx.scheduler.runAfter(0, resumeAction, { workflowId });
|
|
173
324
|
return {
|
|
174
325
|
id: workflowId,
|
|
@@ -181,6 +332,9 @@ export function workflow<TSchema = any, TReturn = any>(
|
|
|
181
332
|
DELETE FROM _gencow_workflows
|
|
182
333
|
WHERE id = ${workflowId}
|
|
183
334
|
`);
|
|
335
|
+
if (insertedWorkflowV2) {
|
|
336
|
+
await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
|
|
337
|
+
}
|
|
184
338
|
throw error;
|
|
185
339
|
}
|
|
186
340
|
},
|
package/src/workflows-api.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import { query } from "./reactive-query.js";
|
|
3
|
+
import { mutation } from "./reactive-mutation.js";
|
|
3
4
|
import {
|
|
4
5
|
createWorkflowRealtimeToken,
|
|
5
6
|
deserializeWorkflowValue,
|
|
@@ -31,6 +32,7 @@ type WorkflowRow = {
|
|
|
31
32
|
current_step: string | null;
|
|
32
33
|
result: unknown;
|
|
33
34
|
error: string | null;
|
|
35
|
+
error_code: string | null;
|
|
34
36
|
retry_count: number;
|
|
35
37
|
max_retries: number;
|
|
36
38
|
max_duration_ms: number;
|
|
@@ -101,6 +103,7 @@ function mapWorkflowSummary(row: WorkflowRow): WorkflowSummary {
|
|
|
101
103
|
derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
|
|
102
104
|
currentStep: row.current_step,
|
|
103
105
|
error: row.error,
|
|
106
|
+
errorCode: row.error_code,
|
|
104
107
|
retryCount: row.retry_count,
|
|
105
108
|
maxRetries: row.max_retries,
|
|
106
109
|
maxDurationMs: Number(row.max_duration_ms),
|
|
@@ -198,10 +201,77 @@ async function loadWorkflowSignalTarget(
|
|
|
198
201
|
FROM _gencow_workflows
|
|
199
202
|
WHERE id = ${workflowId}
|
|
200
203
|
LIMIT 1
|
|
201
|
-
|
|
204
|
+
`);
|
|
202
205
|
return rowsFromResult<WorkflowSignalTargetRow>(result)[0] ?? null;
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
function isMissingWorkflowV2SignalSchemaError(error: unknown): boolean {
|
|
209
|
+
const code =
|
|
210
|
+
error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
211
|
+
const cause =
|
|
212
|
+
error && typeof error === "object" && "cause" in error ? (error as { cause?: unknown }).cause : null;
|
|
213
|
+
const causeCode =
|
|
214
|
+
cause && typeof cause === "object" && "code" in cause ? String((cause as { code?: unknown }).code) : "";
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return (
|
|
217
|
+
code === "42P01" ||
|
|
218
|
+
code === "42703" ||
|
|
219
|
+
causeCode === "42P01" ||
|
|
220
|
+
causeCode === "42703" ||
|
|
221
|
+
((code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2")) ||
|
|
222
|
+
message.includes('relation "_gencow_workflow_signals_v2" does not exist') ||
|
|
223
|
+
message.includes("relation _gencow_workflow_signals_v2 does not exist") ||
|
|
224
|
+
message.includes('relation "_gencow_workflow_runs_v2" does not exist') ||
|
|
225
|
+
message.includes("relation _gencow_workflow_runs_v2 does not exist")
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function tryRecordWorkflowV2Signal(options: {
|
|
230
|
+
db: WorkflowDbLike;
|
|
231
|
+
workflowId: string;
|
|
232
|
+
event: string;
|
|
233
|
+
payload: unknown;
|
|
234
|
+
}): Promise<boolean> {
|
|
235
|
+
try {
|
|
236
|
+
await options.db.execute(sql`
|
|
237
|
+
WITH inserted AS (
|
|
238
|
+
INSERT INTO _gencow_workflow_signals_v2 (
|
|
239
|
+
id,
|
|
240
|
+
run_id,
|
|
241
|
+
event_name,
|
|
242
|
+
payload_json,
|
|
243
|
+
idempotency_key
|
|
244
|
+
)
|
|
245
|
+
VALUES (
|
|
246
|
+
${crypto.randomUUID()},
|
|
247
|
+
${options.workflowId},
|
|
248
|
+
${options.event},
|
|
249
|
+
${JSON.stringify(options.payload)}::jsonb,
|
|
250
|
+
${crypto.randomUUID()}
|
|
251
|
+
)
|
|
252
|
+
RETURNING run_id
|
|
253
|
+
)
|
|
254
|
+
UPDATE _gencow_workflow_runs_v2 run
|
|
255
|
+
SET
|
|
256
|
+
status = 'queued',
|
|
257
|
+
runnable_at = NOW(),
|
|
258
|
+
lease_owner = NULL,
|
|
259
|
+
lease_expires_at = NULL,
|
|
260
|
+
heartbeat_at = NULL,
|
|
261
|
+
updated_at = NOW()
|
|
262
|
+
FROM inserted
|
|
263
|
+
WHERE run.id = inserted.run_id
|
|
264
|
+
AND run.status = 'waiting'
|
|
265
|
+
AND run.completed_at IS NULL
|
|
266
|
+
AND run.cancel_requested_at IS NULL
|
|
267
|
+
`);
|
|
268
|
+
return true;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (isMissingWorkflowV2SignalSchemaError(error)) return false;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
205
275
|
export async function loadWorkflowSnapshot(
|
|
206
276
|
db: WorkflowDbLike,
|
|
207
277
|
workflowId: string,
|
|
@@ -219,6 +289,7 @@ export async function loadWorkflowSnapshot(
|
|
|
219
289
|
current_step,
|
|
220
290
|
result,
|
|
221
291
|
error,
|
|
292
|
+
error_code,
|
|
222
293
|
retry_count,
|
|
223
294
|
max_retries,
|
|
224
295
|
max_duration_ms,
|
|
@@ -326,9 +397,15 @@ export function registerWorkflowsApi(): void {
|
|
|
326
397
|
${crypto.randomUUID()},
|
|
327
398
|
${workflow.id},
|
|
328
399
|
${normalizedEvent},
|
|
329
|
-
|
|
400
|
+
${JSON.stringify(persistedPayload)}::jsonb
|
|
330
401
|
)
|
|
331
402
|
`);
|
|
403
|
+
await tryRecordWorkflowV2Signal({
|
|
404
|
+
db: ctx.unsafeDb,
|
|
405
|
+
workflowId: workflow.id,
|
|
406
|
+
event: normalizedEvent,
|
|
407
|
+
payload: persistedPayload,
|
|
408
|
+
});
|
|
332
409
|
|
|
333
410
|
let scheduledJobId: string | null = null;
|
|
334
411
|
if (workflow.status === "pending" && workflow.current_step?.startsWith("wait:")) {
|
|
@@ -372,6 +449,7 @@ export function registerWorkflowsApi(): void {
|
|
|
372
449
|
current_step,
|
|
373
450
|
result,
|
|
374
451
|
error,
|
|
452
|
+
error_code,
|
|
375
453
|
retry_count,
|
|
376
454
|
max_retries,
|
|
377
455
|
max_duration_ms,
|
|
@@ -393,6 +471,7 @@ export function registerWorkflowsApi(): void {
|
|
|
393
471
|
current_step,
|
|
394
472
|
result,
|
|
395
473
|
error,
|
|
474
|
+
error_code,
|
|
396
475
|
retry_count,
|
|
397
476
|
max_retries,
|
|
398
477
|
max_duration_ms,
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* packages/core/src/__tests__/auth.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests for auth module — AuthCtx, defineAuth, auth-config.
|
|
5
|
-
*
|
|
6
|
-
* Run: bun test packages/core/src/__tests__/auth.test.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from "bun:test";
|
|
10
|
-
import { defineAuth } from "../auth-config.js";
|
|
11
|
-
import type { GencowAuthConfig } from "../auth-config.js";
|
|
12
|
-
|
|
13
|
-
// ─── defineAuth() ───────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
describe("defineAuth()", () => {
|
|
16
|
-
it("빈 설정 객체 반환", () => {
|
|
17
|
-
const config = defineAuth({});
|
|
18
|
-
expect(config).toEqual({});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("emailVerification 설정이 그대로 반환된다", () => {
|
|
22
|
-
const sendFn = async () => {};
|
|
23
|
-
const config = defineAuth({
|
|
24
|
-
emailVerification: {
|
|
25
|
-
sendOnSignUp: true,
|
|
26
|
-
requireEmailVerification: true,
|
|
27
|
-
autoSignInAfterVerification: true,
|
|
28
|
-
sendVerificationEmail: sendFn,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
expect(config.emailVerification?.sendOnSignUp).toBe(true);
|
|
33
|
-
expect(config.emailVerification?.requireEmailVerification).toBe(true);
|
|
34
|
-
expect(config.emailVerification?.autoSignInAfterVerification).toBe(true);
|
|
35
|
-
expect(config.emailVerification?.sendVerificationEmail).toBe(sendFn);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("부분 설정도 허용된다", () => {
|
|
39
|
-
const config = defineAuth({
|
|
40
|
-
emailVerification: {
|
|
41
|
-
sendVerificationEmail: async () => {},
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(config.emailVerification?.sendOnSignUp).toBeUndefined();
|
|
46
|
-
expect(config.emailVerification?.sendVerificationEmail).toBeDefined();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ─── AuthCtx interface 패턴 ─────────────────────────────
|
|
51
|
-
|
|
52
|
-
describe("AuthCtx 패턴 (mock)", () => {
|
|
53
|
-
// AuthCtx는 런타임에서 생성되므로, 인터페이스 수준에서 패턴 검증
|
|
54
|
-
|
|
55
|
-
it("getUserIdentity — 비로그인 시 null", () => {
|
|
56
|
-
const authCtx = {
|
|
57
|
-
getUserIdentity: () => null,
|
|
58
|
-
requireAuth: () => {
|
|
59
|
-
throw new Error("Authentication required");
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
expect(authCtx.getUserIdentity()).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("getUserIdentity — 로그인 시 유저 반환", () => {
|
|
67
|
-
const user = { id: "u1", email: "test@test.com", name: "Test" };
|
|
68
|
-
const authCtx = {
|
|
69
|
-
getUserIdentity: () => user,
|
|
70
|
-
requireAuth: () => user,
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
expect(authCtx.getUserIdentity()).toEqual(user);
|
|
74
|
-
expect(authCtx.getUserIdentity()!.id).toBe("u1");
|
|
75
|
-
expect(authCtx.getUserIdentity()!.email).toBe("test@test.com");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("requireAuth — 비로그인 시 에러 throw", () => {
|
|
79
|
-
const authCtx = {
|
|
80
|
-
getUserIdentity: () => null,
|
|
81
|
-
requireAuth: () => {
|
|
82
|
-
throw new Error("Authentication required");
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
expect(() => authCtx.requireAuth()).toThrow("Authentication required");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("requireAuth — 로그인 시 유저 반환 (throw 안 함)", () => {
|
|
90
|
-
const user = { id: "u2", email: "auth@test.com" };
|
|
91
|
-
const authCtx = {
|
|
92
|
-
getUserIdentity: () => user,
|
|
93
|
-
requireAuth: () => user,
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
expect(() => authCtx.requireAuth()).not.toThrow();
|
|
97
|
-
expect(authCtx.requireAuth()).toEqual(user);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ─── Secure by Default 검증 ─────────────────────────────
|
|
102
|
-
|
|
103
|
-
describe("Secure by Default — 기본 인증 필수", () => {
|
|
104
|
-
// query/mutation의 isPublic 기본값은 reactive.test.ts에서 검증됨.
|
|
105
|
-
// 여기서는 auth 엔드포인트 레벨 패턴만 확인.
|
|
106
|
-
|
|
107
|
-
it("공개(public) 쿼리는 auth 없이 실행 가능해야 함", () => {
|
|
108
|
-
// 이 테스트는 query({ public: true })의 isPublic 플래그 확인
|
|
109
|
-
// reactive.test.ts의 "Secure by Default" 섹션과 연계
|
|
110
|
-
const mockQueryDef = { isPublic: true, handler: async () => [] };
|
|
111
|
-
expect(mockQueryDef.isPublic).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("비공개 쿼리는 기본적으로 auth 필수", () => {
|
|
115
|
-
const mockQueryDef = { isPublic: false, handler: async () => [] };
|
|
116
|
-
expect(mockQueryDef.isPublic).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { cronJobs } from "../crons.js";
|
|
3
|
-
|
|
4
|
-
describe("cronJobs 빌더", () => {
|
|
5
|
-
test("interval — minutes → cron 패턴 변환", () => {
|
|
6
|
-
const crons = cronJobs();
|
|
7
|
-
crons.interval("test", { minutes: 15 }, "test.action");
|
|
8
|
-
const jobs = crons.getJobs();
|
|
9
|
-
expect(jobs).toHaveLength(1);
|
|
10
|
-
expect(jobs[0].name).toBe("test");
|
|
11
|
-
expect(jobs[0].pattern).toBe("*/15 * * * *");
|
|
12
|
-
expect(jobs[0].action).toBe("test.action");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("interval — hours → cron 패턴 변환", () => {
|
|
16
|
-
const crons = cronJobs();
|
|
17
|
-
crons.interval("hourly", { hours: 6 }, "cleanup");
|
|
18
|
-
expect(crons.getJobs()[0].pattern).toBe("0 */6 * * *");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("interval — seconds → 6자리 cron 패턴", () => {
|
|
22
|
-
const crons = cronJobs();
|
|
23
|
-
crons.interval("fast", { seconds: 30 }, "tick");
|
|
24
|
-
expect(crons.getJobs()[0].pattern).toBe("*/30 * * * * *");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("daily — 특정 시각", () => {
|
|
28
|
-
const crons = cronJobs();
|
|
29
|
-
crons.daily("report", { hour: 9, minute: 30 }, "reports.gen");
|
|
30
|
-
const job = crons.getJobs()[0];
|
|
31
|
-
expect(job.pattern).toBe("30 9 * * *");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("daily — minute 기본값 0", () => {
|
|
35
|
-
const crons = cronJobs();
|
|
36
|
-
crons.daily("cleanup", { hour: 2 }, "admin.cleanup");
|
|
37
|
-
expect(crons.getJobs()[0].pattern).toBe("0 2 * * *");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("weekly — 요일 + 시각", () => {
|
|
41
|
-
const crons = cronJobs();
|
|
42
|
-
crons.weekly("monday-report", { dayOfWeek: 1, hour: 9 }, "reports.weekly");
|
|
43
|
-
expect(crons.getJobs()[0].pattern).toBe("0 9 * * 1");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("cron — 직접 패턴 지정", () => {
|
|
47
|
-
const crons = cronJobs();
|
|
48
|
-
crons.cron("custom", "0 */2 * * *", "custom.handler");
|
|
49
|
-
expect(crons.getJobs()[0].pattern).toBe("0 */2 * * *");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("체이닝 지원", () => {
|
|
53
|
-
const crons = cronJobs();
|
|
54
|
-
crons
|
|
55
|
-
.interval("a", { minutes: 5 }, "a.action")
|
|
56
|
-
.daily("b", { hour: 3 }, "b.action")
|
|
57
|
-
.cron("c", "0 * * * *", "c.action");
|
|
58
|
-
expect(crons.getJobs()).toHaveLength(3);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("인라인 핸들러 지원", () => {
|
|
62
|
-
const crons = cronJobs();
|
|
63
|
-
const fn = async () => {
|
|
64
|
-
/* noop */
|
|
65
|
-
};
|
|
66
|
-
crons.interval("inline", { minutes: 1 }, fn);
|
|
67
|
-
expect(typeof crons.getJobs()[0].action).toBe("function");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("getJobs는 복사본 반환 (원본 보호)", () => {
|
|
71
|
-
const crons = cronJobs();
|
|
72
|
-
crons.interval("test", { minutes: 1 }, "test");
|
|
73
|
-
const jobs1 = crons.getJobs();
|
|
74
|
-
const jobs2 = crons.getJobs();
|
|
75
|
-
expect(jobs1).not.toBe(jobs2); // 서로 다른 참조
|
|
76
|
-
expect(jobs1).toEqual(jobs2); // 내용은 동일
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("interval — 옵션 없으면 에러", () => {
|
|
80
|
-
const crons = cronJobs();
|
|
81
|
-
expect(() => crons.interval("bad", {}, "bad")).toThrow("minutes, hours, 또는 seconds");
|
|
82
|
-
});
|
|
83
|
-
});
|