@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.
Files changed (75) 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 +45 -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 +47 -41
  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/dist/db.d.ts +0 -13
  74. package/dist/db.js +0 -16
  75. package/src/db.ts +0 -18
package/dist/rls-db.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PgDatabase } from "drizzle-orm/pg-core";
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: PgDatabase<any, any, any>, rls: RlsSessionContext): PgDatabase<any, any, any>;
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)`);
@@ -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 = { id, action, scheduledAt: new Date().toISOString(), status: "pending" };
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
- }).then(() => {
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
- }).catch((err) => {
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.writeFile(`${filePath}.meta`, JSON.stringify({ name: filename, type, size: buffer.length })).catch(() => { });
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 = config.autoMaxWidth > 0
367
- ? Math.min(appConfig.autoMaxWidth, config.autoMaxWidth)
368
- : appConfig.autoMaxWidth;
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 { /* 파일 없음 or 파싱 실패 → Tier 기본값 유지 */ }
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 { /* fallthrough to 404 */ }
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
- && isTransformable
437
- && clientAcceptsWebp
438
- && config.autoWebp
439
- && (meta.type === "image/png" || meta.type === "image/jpeg" || meta.type === "image/jpg");
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 ? { "Vary": "Accept" } : {}),
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> ? (undefined extends U ? never : K) : K]: T[K] extends Validator<infer U> ? U : any;
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> ? (undefined extends U ? K : never) : never]?: T[K] extends Validator<infer U> ? U : any;
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") {
@@ -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
- "pending",
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), { workflowId: workflow.id });
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
- "name": "@gencow/core",
3
- "version": "0.1.24",
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"
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
- "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
- "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
- it("빈 설정 객체 반환", () => {
17
- const config = defineAuth({});
18
- expect(config).toEqual({});
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
- 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);
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
- 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
- });
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
- // AuthCtx는 런타임에서 생성되므로, 인터페이스 수준에서 패턴 검증
54
-
55
- it("getUserIdentity — 비로그인 시 null", () => {
56
- const authCtx = {
57
- getUserIdentity: () => null,
58
- requireAuth: () => { throw new Error("Authentication required"); },
59
- };
60
-
61
- expect(authCtx.getUserIdentity()).toBeNull();
62
- });
63
-
64
- it("getUserIdentity — 로그인 시 유저 반환", () => {
65
- const user = { id: "u1", email: "test@test.com", name: "Test" };
66
- const authCtx = {
67
- getUserIdentity: () => user,
68
- requireAuth: () => user,
69
- };
70
-
71
- expect(authCtx.getUserIdentity()).toEqual(user);
72
- expect(authCtx.getUserIdentity()!.id).toBe("u1");
73
- expect(authCtx.getUserIdentity()!.email).toBe("test@test.com");
74
- });
75
-
76
- it("requireAuth — 비로그인 시 에러 throw", () => {
77
- const authCtx = {
78
- getUserIdentity: () => null,
79
- requireAuth: () => { throw new Error("Authentication required"); },
80
- };
81
-
82
- expect(() => authCtx.requireAuth()).toThrow("Authentication required");
83
- });
84
-
85
- it("requireAuth — 로그인 시 유저 반환 (throw 안 함)", () => {
86
- const user = { id: "u2", email: "auth@test.com" };
87
- const authCtx = {
88
- getUserIdentity: () => user,
89
- requireAuth: () => user,
90
- };
91
-
92
- expect(() => authCtx.requireAuth()).not.toThrow();
93
- expect(authCtx.requireAuth()).toEqual(user);
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
- // query/mutation의 isPublic 기본값은 reactive.test.ts에서 검증됨.
101
- // 여기서는 auth 엔드포인트 레벨 패턴만 확인.
102
-
103
- it("공개(public) 쿼리는 auth 없이 실행 가능해야 함", () => {
104
- // 이 테스트는 query({ public: true })의 isPublic 플래그 확인
105
- // reactive.test.ts의 "Secure by Default" 섹션과 연계
106
- const mockQueryDef = { isPublic: true, handler: async () => [] };
107
- expect(mockQueryDef.isPublic).toBe(true);
108
- });
109
-
110
- it("비공개 쿼리는 기본적으로 auth 필수", () => {
111
- const mockQueryDef = { isPublic: false, handler: async () => [] };
112
- expect(mockQueryDef.isPublic).toBe(false);
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
  });