@gencow/core 0.1.24 → 0.1.26
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 +5 -5
- package/dist/index.js +2 -2
- 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.js +4 -11
- package/dist/workflows-api.js +5 -12
- package/package.json +45 -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 -120
- 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 +47 -41
- 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 +309 -286
- 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 +69 -5
- 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 +67 -70
- package/src/workflow.ts +99 -116
- package/src/workflows-api.ts +231 -241
- package/dist/db.d.ts +0 -13
- package/dist/db.js +0 -16
- package/src/db.ts +0 -18
package/dist/rls-db.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PgAsyncDatabase } from "drizzle-orm/pg-core";
|
|
2
2
|
/**
|
|
3
3
|
* RLS DB wrapper — execution paths for `withRlsConnection`:
|
|
4
4
|
* 1. **Reuse outer Drizzle transaction** (`reuseOuterConnection`): same connection, apply GUCs then run `fn`.
|
|
@@ -44,4 +44,4 @@ export declare function withRlsLeasedConnection<T>(leased: {
|
|
|
44
44
|
*
|
|
45
45
|
* `db.transaction()` still injects the same variables at the start of the callback transaction.
|
|
46
46
|
*/
|
|
47
|
-
export declare function createRlsDb(db:
|
|
47
|
+
export declare function createRlsDb(db: PgAsyncDatabase<any, any, any, any>, rls: RlsSessionContext): PgAsyncDatabase<any, any, any, any>;
|
package/dist/rls-db.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
const gucNameRe = /^app\.[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
|
|
4
|
-
const RESERVED_VARS_KEYS = new Set([
|
|
5
|
-
"app.current_user_id",
|
|
6
|
-
"app.current_user_role",
|
|
7
|
-
"app.tenant_id",
|
|
8
|
-
]);
|
|
4
|
+
const RESERVED_VARS_KEYS = new Set(["app.current_user_id", "app.current_user_role", "app.tenant_id"]);
|
|
9
5
|
function assertSafeGucName(key) {
|
|
10
6
|
if (!gucNameRe.test(key)) {
|
|
11
7
|
throw new Error(`createRlsDb: GUC name "${key}" is invalid — use lowercase app.* names (e.g. app.org_id)`);
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface Scheduler {
|
|
|
45
45
|
cron(name: string, pattern: string, handler: () => Promise<void>): void;
|
|
46
46
|
/** Register an action handler */
|
|
47
47
|
registerAction(name: string, handler: ActionHandler): void;
|
|
48
|
+
/** Execute a registered action and propagate errors to the caller */
|
|
49
|
+
executeActionStrict(name: string, args?: any): Promise<void>;
|
|
48
50
|
/** Execute a registered action by name — 선언적 crons.ts 문자열 액션 실행용 */
|
|
49
51
|
executeAction(name: string, args?: any): Promise<void>;
|
|
50
52
|
}
|
package/dist/scheduler.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as cron from "node-cron";
|
|
2
|
-
const _cronInfo = globalThis.__gencow_cronInfo ??= [];
|
|
3
|
-
const _pendingJobs = globalThis.__gencow_pendingJobs ??= [];
|
|
4
|
-
const _failedJobs = globalThis.__gencow_failedJobs ??= [];
|
|
2
|
+
const _cronInfo = (globalThis.__gencow_cronInfo ??= []);
|
|
3
|
+
const _pendingJobs = (globalThis.__gencow_pendingJobs ??= []);
|
|
4
|
+
const _failedJobs = (globalThis.__gencow_failedJobs ??= []);
|
|
5
5
|
/** 최대 보관할 실패 작업 수 (메모리 보호) */
|
|
6
6
|
const MAX_FAILED_JOBS = 100;
|
|
7
7
|
export function getSchedulerInfo() {
|
|
@@ -89,7 +89,12 @@ export function createScheduler(options) {
|
|
|
89
89
|
return {
|
|
90
90
|
runAfter(ms, action, args, scheduleOpts) {
|
|
91
91
|
const id = generateId();
|
|
92
|
-
const jobEntry = {
|
|
92
|
+
const jobEntry = {
|
|
93
|
+
id,
|
|
94
|
+
action,
|
|
95
|
+
scheduledAt: new Date().toISOString(),
|
|
96
|
+
status: "pending",
|
|
97
|
+
};
|
|
93
98
|
_pendingJobs.push(jobEntry);
|
|
94
99
|
// ── Durable mode: DB에 영속화, 실행은 외부 폴러에 위임 ──
|
|
95
100
|
if (isDurable) {
|
|
@@ -100,10 +105,12 @@ export function createScheduler(options) {
|
|
|
100
105
|
args: args ?? {},
|
|
101
106
|
runAt,
|
|
102
107
|
onErrorAction: scheduleOpts?.onError,
|
|
103
|
-
})
|
|
108
|
+
})
|
|
109
|
+
.then(() => {
|
|
104
110
|
console.log(`[scheduler] Persisted "${action}" to run at ${runAt.toISOString()} (id: ${id}, durable)` +
|
|
105
111
|
`${scheduleOpts?.onError ? ` [onError: ${scheduleOpts.onError}]` : ""}`);
|
|
106
|
-
})
|
|
112
|
+
})
|
|
113
|
+
.catch((err) => {
|
|
107
114
|
console.error(`[scheduler] Failed to persist job ${id}:`, err instanceof Error ? err.message : err);
|
|
108
115
|
// DB persist 실패 → 인메모리 fallback
|
|
109
116
|
scheduleInMemory(id, ms, action, args, scheduleOpts, jobEntry);
|
|
@@ -171,6 +178,9 @@ export function createScheduler(options) {
|
|
|
171
178
|
registerAction(name, handler) {
|
|
172
179
|
actions.set(name, handler);
|
|
173
180
|
},
|
|
181
|
+
async executeActionStrict(name, args) {
|
|
182
|
+
await executeAction(name, args);
|
|
183
|
+
},
|
|
174
184
|
async executeAction(name, args) {
|
|
175
185
|
try {
|
|
176
186
|
await executeAction(name, args);
|
package/dist/server.d.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* executing server. Excluded from client-side core (`index.ts`) so they aren't
|
|
6
6
|
* bundled into user functions which run in Firecracker.
|
|
7
7
|
*/
|
|
8
|
-
export { createDb } from "./db.js";
|
|
9
8
|
export { createStorage, storageRoutes } from "./storage.js";
|
|
10
9
|
export type { StorageImageTierConfig } from "./storage.js";
|
|
11
10
|
export { createScheduler, getSchedulerInfo } from "./scheduler.js";
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* executing server. Excluded from client-side core (`index.ts`) so they aren't
|
|
6
6
|
* bundled into user functions which run in Firecracker.
|
|
7
7
|
*/
|
|
8
|
-
export { createDb } from "./db.js";
|
|
9
8
|
export { createStorage, storageRoutes } from "./storage.js";
|
|
10
9
|
export { createScheduler, getSchedulerInfo } from "./scheduler.js";
|
|
11
10
|
export { authMiddleware, authRoutes, getUsers } from "./auth.js";
|
package/dist/storage.js
CHANGED
|
@@ -141,7 +141,9 @@ export function createStorage(dir = "./uploads", options) {
|
|
|
141
141
|
const filePath = path.join(dir, id);
|
|
142
142
|
await fs.writeFile(filePath, buffer);
|
|
143
143
|
// .meta JSON 기록 — Platform 레벨 직접 서빙용
|
|
144
|
-
await fs
|
|
144
|
+
await fs
|
|
145
|
+
.writeFile(`${filePath}.meta`, JSON.stringify({ name: filename, type, size: buffer.length }))
|
|
146
|
+
.catch(() => { });
|
|
145
147
|
metaStore.set(id, {
|
|
146
148
|
id,
|
|
147
149
|
name: filename,
|
|
@@ -187,25 +189,27 @@ export function createStorage(dir = "./uploads", options) {
|
|
|
187
189
|
const entries = await fs.readdir(cacheDir);
|
|
188
190
|
const prefix = `${storageId}_`;
|
|
189
191
|
await Promise.all(entries
|
|
190
|
-
.filter(e => e.startsWith(prefix))
|
|
191
|
-
.map(e => fs.unlink(path.join(cacheDir, e)).catch(() => { })));
|
|
192
|
+
.filter((e) => e.startsWith(prefix))
|
|
193
|
+
.map((e) => fs.unlink(path.join(cacheDir, e)).catch(() => { })));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* .cache 디렉토리 미존재 시 무시 */
|
|
192
197
|
}
|
|
193
|
-
catch { /* .cache 디렉토리 미존재 시 무시 */ }
|
|
194
198
|
// DB에서도 삭제 (rawSql 있을 때만)
|
|
195
199
|
if (rawSql) {
|
|
196
200
|
try {
|
|
197
201
|
await rawSql(`DELETE FROM _system_files WHERE storage_id = $1`, [storageId]);
|
|
198
202
|
}
|
|
199
|
-
catch {
|
|
203
|
+
catch {
|
|
204
|
+
/* 삭제 실패 무시 — 파일은 이미 제거됨 */
|
|
205
|
+
}
|
|
200
206
|
}
|
|
201
207
|
},
|
|
202
208
|
};
|
|
203
209
|
}
|
|
204
210
|
// ─── Image Optimization Constants ───────────────────────
|
|
205
211
|
/** 변환 가능한 원본 MIME 타입 */
|
|
206
|
-
const TRANSFORMABLE_TYPES = new Set([
|
|
207
|
-
"image/png", "image/jpeg", "image/jpg", "image/webp",
|
|
208
|
-
]);
|
|
212
|
+
const TRANSFORMABLE_TYPES = new Set(["image/png", "image/jpeg", "image/jpg", "image/webp"]);
|
|
209
213
|
/** 허용 출력 포맷 */
|
|
210
214
|
const ALLOWED_FORMATS = new Set(["webp", "avif", "jpeg", "png"]);
|
|
211
215
|
/** 허용 맞춤 모드 */
|
|
@@ -363,9 +367,10 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
363
367
|
const appConfig = JSON.parse(raw);
|
|
364
368
|
// Tier ceiling 적용: min(앱 설정, Tier 최대값)
|
|
365
369
|
if (appConfig.autoMaxWidth > 0) {
|
|
366
|
-
config.autoMaxWidth =
|
|
367
|
-
|
|
368
|
-
|
|
370
|
+
config.autoMaxWidth =
|
|
371
|
+
config.autoMaxWidth > 0
|
|
372
|
+
? Math.min(appConfig.autoMaxWidth, config.autoMaxWidth)
|
|
373
|
+
: appConfig.autoMaxWidth;
|
|
369
374
|
}
|
|
370
375
|
if (appConfig.autoQuality > 0 && appConfig.autoQuality <= 100) {
|
|
371
376
|
config.autoQuality = appConfig.autoQuality;
|
|
@@ -374,7 +379,9 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
374
379
|
config.autoWebp = false;
|
|
375
380
|
}
|
|
376
381
|
}
|
|
377
|
-
catch {
|
|
382
|
+
catch {
|
|
383
|
+
/* 파일 없음 or 파싱 실패 → Tier 기본값 유지 */
|
|
384
|
+
}
|
|
378
385
|
return config;
|
|
379
386
|
}
|
|
380
387
|
// sharp 모듈 캐시 (동적 import 1회만)
|
|
@@ -417,7 +424,9 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
417
424
|
};
|
|
418
425
|
}
|
|
419
426
|
}
|
|
420
|
-
catch {
|
|
427
|
+
catch {
|
|
428
|
+
/* fallthrough to 404 */
|
|
429
|
+
}
|
|
421
430
|
}
|
|
422
431
|
if (!meta) {
|
|
423
432
|
return c.json({ error: "Not found" }, 404);
|
|
@@ -432,11 +441,11 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
432
441
|
const acceptHeader = c.req.header("accept") || "";
|
|
433
442
|
const clientAcceptsWebp = acceptHeader.includes("image/webp");
|
|
434
443
|
// Auto WebP: 파라미터 없지만 브라우저가 webp 지원 + 원본이 PNG/JPEG
|
|
435
|
-
const isAutoWebp = !transformParams
|
|
436
|
-
&&
|
|
437
|
-
&&
|
|
438
|
-
|
|
439
|
-
|
|
444
|
+
const isAutoWebp = !transformParams &&
|
|
445
|
+
isTransformable &&
|
|
446
|
+
clientAcceptsWebp &&
|
|
447
|
+
config.autoWebp &&
|
|
448
|
+
(meta.type === "image/png" || meta.type === "image/jpeg" || meta.type === "image/jpg");
|
|
440
449
|
// 이미지 변환이 필요 없는 경우 → 원본 서빙 (기존 동작 100% 유지)
|
|
441
450
|
if (!transformParams && !isAutoWebp) {
|
|
442
451
|
return serveOriginal(c, meta);
|
|
@@ -595,16 +604,14 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
595
604
|
// 변환된 파일명: original.png → original.webp (또는 original_300x200.webp)
|
|
596
605
|
const baseName = originalMeta.name.replace(/\.[^.]+$/, "");
|
|
597
606
|
const ext = outputFormat || originalMeta.name.split(".").pop() || "bin";
|
|
598
|
-
const suffix = params?.w || params?.h
|
|
599
|
-
? `_${params.w || "auto"}x${params.h || "auto"}`
|
|
600
|
-
: "";
|
|
607
|
+
const suffix = params?.w || params?.h ? `_${params.w || "auto"}x${params.h || "auto"}` : "";
|
|
601
608
|
const fileName = `${baseName}${suffix}.${ext}`;
|
|
602
609
|
const headers = {
|
|
603
610
|
"Content-Type": contentType,
|
|
604
611
|
"Content-Disposition": `inline; filename="${encodeURIComponent(fileName)}"; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
|
605
612
|
"Cache-Control": "public, max-age=31536000, immutable",
|
|
606
613
|
// Vary: Accept — Auto WebP 시 CDN/브라우저 캐시 분리
|
|
607
|
-
...(isAutoWebp ? {
|
|
614
|
+
...(isAutoWebp ? { Vary: "Accept" } : {}),
|
|
608
615
|
};
|
|
609
616
|
if (typeof globalThis.Bun !== "undefined") {
|
|
610
617
|
const bunFile = Bun.file(cachePath);
|
package/dist/v.d.ts
CHANGED
|
@@ -32,9 +32,9 @@ export declare const v: {
|
|
|
32
32
|
* Correctly handles optional properties (Validator<T | undefined> → optional key).
|
|
33
33
|
*/
|
|
34
34
|
export type InferArgs<T> = T extends Validator<infer U> ? U : T extends Record<string, Validator> ? {
|
|
35
|
-
[K in keyof T as T[K] extends Validator<infer U> ?
|
|
35
|
+
[K in keyof T as T[K] extends Validator<infer U> ? undefined extends U ? never : K : K]: T[K] extends Validator<infer U> ? U : any;
|
|
36
36
|
} & {
|
|
37
|
-
[K in keyof T as T[K] extends Validator<infer U> ?
|
|
37
|
+
[K in keyof T as T[K] extends Validator<infer U> ? undefined extends U ? K : never : never]?: T[K] extends Validator<infer U> ? U : any;
|
|
38
38
|
} : T;
|
|
39
39
|
/**
|
|
40
40
|
* Validates and parses arguments against a schema at runtime.
|
package/dist/workflow.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
2
|
import { mutation } from "./reactive.js";
|
|
3
3
|
import { registerWorkflowsApi } from "./workflows-api.js";
|
|
4
|
-
const workflowRegistry = globalThis.__gencow_workflowRegistry ??= new Map();
|
|
4
|
+
const workflowRegistry = (globalThis.__gencow_workflowRegistry ??= new Map());
|
|
5
5
|
export const DEFAULT_WORKFLOW_MAX_DURATION_MS = 30 * 60 * 1000;
|
|
6
6
|
export const DEFAULT_WORKFLOW_MAX_RETRIES = 3;
|
|
7
7
|
export const WORKFLOW_RESUME_ACTION_PREFIX = "__gencow.workflow.resume";
|
|
8
8
|
export const WORKFLOW_REALTIME_KEY_PREFIX = "__gencow.workflow.state";
|
|
9
9
|
function isSerializedWorkflowValue(value) {
|
|
10
|
-
return !!value && typeof value === "object" && ("__gencowUndefined" in value ||
|
|
11
|
-
"value" in value);
|
|
10
|
+
return !!value && typeof value === "object" && ("__gencowUndefined" in value || "value" in value);
|
|
12
11
|
}
|
|
13
12
|
export function serializeWorkflowValue(value) {
|
|
14
|
-
const payload = value === undefined
|
|
15
|
-
? { __gencowUndefined: true }
|
|
16
|
-
: { value };
|
|
13
|
+
const payload = value === undefined ? { __gencowUndefined: true } : { value };
|
|
17
14
|
try {
|
|
18
15
|
return JSON.parse(JSON.stringify(payload));
|
|
19
16
|
}
|
|
@@ -45,11 +42,7 @@ function parseDurationString(raw, label) {
|
|
|
45
42
|
}
|
|
46
43
|
const value = Number(match[1]);
|
|
47
44
|
const unit = match[2];
|
|
48
|
-
const unitMs = unit === "ms" ? 1 :
|
|
49
|
-
unit === "s" ? 1_000 :
|
|
50
|
-
unit === "m" ? 60_000 :
|
|
51
|
-
unit === "h" ? 3_600_000 :
|
|
52
|
-
86_400_000;
|
|
45
|
+
const unitMs = unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
|
|
53
46
|
return value * unitMs;
|
|
54
47
|
}
|
|
55
48
|
export function parseWorkflowDurationMs(raw, label = "workflow duration") {
|
package/dist/workflows-api.js
CHANGED
|
@@ -3,17 +3,8 @@ import { mutation, query } from "./reactive.js";
|
|
|
3
3
|
import { createWorkflowRealtimeToken, deserializeWorkflowValue, getWorkflowResumeActionName, getWorkflowRealtimeKey, serializeWorkflowValue, } from "./workflow.js";
|
|
4
4
|
import { GencowValidationError, v } from "./v.js";
|
|
5
5
|
import { deriveWorkflowStatus } from "./workflow-types.js";
|
|
6
|
-
const WORKFLOW_STATUSES = new Set([
|
|
7
|
-
|
|
8
|
-
"running",
|
|
9
|
-
"completed",
|
|
10
|
-
"failed",
|
|
11
|
-
]);
|
|
12
|
-
const WORKFLOW_DERIVED_PENDING_STATUSES = new Set([
|
|
13
|
-
"queued",
|
|
14
|
-
"waiting",
|
|
15
|
-
"sleeping",
|
|
16
|
-
]);
|
|
6
|
+
const WORKFLOW_STATUSES = new Set(["pending", "running", "completed", "failed"]);
|
|
7
|
+
const WORKFLOW_DERIVED_PENDING_STATUSES = new Set(["queued", "waiting", "sleeping"]);
|
|
17
8
|
function rowsFromResult(result) {
|
|
18
9
|
if (Array.isArray(result))
|
|
19
10
|
return result;
|
|
@@ -252,7 +243,9 @@ export function registerWorkflowsApi() {
|
|
|
252
243
|
let scheduledJobId = null;
|
|
253
244
|
if (workflow.status === "pending" && workflow.current_step?.startsWith("wait:")) {
|
|
254
245
|
try {
|
|
255
|
-
scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflow.name), {
|
|
246
|
+
scheduledJobId = ctx.scheduler.runAfter(0, getWorkflowResumeActionName(workflow.name), {
|
|
247
|
+
workflowId: workflow.id,
|
|
248
|
+
});
|
|
256
249
|
}
|
|
257
250
|
catch {
|
|
258
251
|
scheduledJobId = null;
|
package/package.json
CHANGED
|
@@ -1,46 +1,49 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
},
|
|
14
|
-
"./server": {
|
|
15
|
-
"import": "./dist/server.js",
|
|
16
|
-
"require": "./dist/server.js",
|
|
17
|
-
"types": "./dist/server.d.ts"
|
|
18
|
-
}
|
|
2
|
+
"name": "@gencow/core",
|
|
3
|
+
"version": "0.1.26",
|
|
4
|
+
"description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
19
13
|
},
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"scripts": {
|
|
25
|
-
"db:generate:fixture-basic": "drizzle-kit generate --config ./src/__tests__/fixtures/basic/drizzle.config.ts",
|
|
26
|
-
"build": "tsc",
|
|
27
|
-
"typecheck": "tsc --noEmit",
|
|
28
|
-
"prepublishOnly": "npm run build",
|
|
29
|
-
"postinstall": "tsc"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@electric-sql/pglite": "^0.3.15",
|
|
33
|
-
"drizzle-orm": "^0.45.1",
|
|
34
|
-
"hono": "^4.12.0",
|
|
35
|
-
"node-cron": "^4.2.1"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"@types/bun": "^1.3.9",
|
|
39
|
-
"@types/node": "^25.3.0",
|
|
40
|
-
"@types/node-cron": "^3.0.11",
|
|
41
|
-
"drizzle-kit": "^0.31.10",
|
|
42
|
-
"drizzle-seed": "^0.3.1",
|
|
43
|
-
"typescript": "^5.9.3",
|
|
44
|
-
"uuid": "^13.0.0"
|
|
14
|
+
"./server": {
|
|
15
|
+
"import": "./dist/server.js",
|
|
16
|
+
"require": "./dist/server.js",
|
|
17
|
+
"types": "./dist/server.d.ts"
|
|
45
18
|
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/",
|
|
22
|
+
"src/"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"db:generate:fixture-basic": "drizzle-kit generate --config ./src/__tests__/fixtures/basic/drizzle.config.ts",
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"node-cron": "^4.2.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"drizzle-orm": "^1.0.0-beta || ^1.0.0",
|
|
35
|
+
"hono": "^4.12.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
39
|
+
"@types/bun": "^1.3.9",
|
|
40
|
+
"@types/node": "^25.3.0",
|
|
41
|
+
"@types/node-cron": "^3.0.11",
|
|
42
|
+
"drizzle-kit": "^1.0.0-beta || ^1.0.0",
|
|
43
|
+
"drizzle-orm": "^1.0.0-beta || ^1.0.0",
|
|
44
|
+
"drizzle-seed": "^1.0.0-beta",
|
|
45
|
+
"hono": "^4.12.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"uuid": "^13.0.0"
|
|
48
|
+
}
|
|
46
49
|
}
|
|
@@ -7,108 +7,112 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from "bun:test";
|
|
10
|
-
import { defineAuth } from "../auth-config";
|
|
11
|
-
import type { GencowAuthConfig } from "../auth-config";
|
|
10
|
+
import { defineAuth } from "../auth-config.js";
|
|
11
|
+
import type { GencowAuthConfig } from "../auth-config.js";
|
|
12
12
|
|
|
13
13
|
// ─── defineAuth() ───────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
describe("defineAuth()", () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
},
|
|
19
30
|
});
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
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
|
+
},
|
|
36
43
|
});
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sendVerificationEmail: async () => {},
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect(config.emailVerification?.sendOnSignUp).toBeUndefined();
|
|
46
|
-
expect(config.emailVerification?.sendVerificationEmail).toBeDefined();
|
|
47
|
-
});
|
|
45
|
+
expect(config.emailVerification?.sendOnSignUp).toBeUndefined();
|
|
46
|
+
expect(config.emailVerification?.sendVerificationEmail).toBeDefined();
|
|
47
|
+
});
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// ─── AuthCtx interface 패턴 ─────────────────────────────
|
|
51
51
|
|
|
52
52
|
describe("AuthCtx 패턴 (mock)", () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
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
|
+
});
|
|
95
99
|
});
|
|
96
100
|
|
|
97
101
|
// ─── Secure by Default 검증 ─────────────────────────────
|
|
98
102
|
|
|
99
103
|
describe("Secure by Default — 기본 인증 필수", () => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
});
|
|
114
118
|
});
|