@gencow/core 0.1.24 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +46 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/src/db.ts +0 -18
package/src/auth.ts CHANGED
@@ -5,20 +5,20 @@ import { sign, verify } from "hono/utils/jwt/jwt";
5
5
  // ─── Types ──────────────────────────────────────────────
6
6
 
7
7
  interface User {
8
- id: string;
9
- email: string;
10
- name?: string;
8
+ id: string;
9
+ email: string;
10
+ name?: string;
11
11
  }
12
12
 
13
13
  interface AuthContext {
14
- /** Get current user or null — Convex의 ctx.auth.getUserIdentity() */
15
- getUserIdentity(): User | null;
16
- /** Get current user or throw 401 — 편의 메서드 */
17
- requireAuth(): User;
14
+ /** Get current user or null — Convex의 ctx.auth.getUserIdentity() */
15
+ getUserIdentity(): User | null;
16
+ /** Get current user or throw 401 — 편의 메서드 */
17
+ requireAuth(): User;
18
18
  }
19
19
 
20
20
  interface AuthConfig {
21
- jwtSecret: string;
21
+ jwtSecret: string;
22
22
  }
23
23
 
24
24
  // ─── In-memory user store (POC용, 프로덕션에서는 Drizzle 테이블 사용) ──
@@ -28,12 +28,12 @@ const users = new Map<string, User & { passwordHash: string; createdAt: string }
28
28
  // ─── Simple password hashing (POC용) ────────────────────
29
29
 
30
30
  async function hashPassword(password: string): Promise<string> {
31
- const encoder = new TextEncoder();
32
- const data = encoder.encode(password);
33
- const hash = await crypto.subtle.digest("SHA-256", data);
34
- return Array.from(new Uint8Array(hash))
35
- .map((b) => b.toString(16).padStart(2, "0"))
36
- .join("");
31
+ const encoder = new TextEncoder();
32
+ const data = encoder.encode(password);
33
+ const hash = await crypto.subtle.digest("SHA-256", data);
34
+ return Array.from(new Uint8Array(hash))
35
+ .map((b) => b.toString(16).padStart(2, "0"))
36
+ .join("");
37
37
  }
38
38
 
39
39
  // ─── Auth middleware — Convex ctx.auth 패턴 재현 ─────────
@@ -48,108 +48,108 @@ async function hashPassword(password: string): Promise<string> {
48
48
  * const user = c.get('auth').requireAuth();
49
49
  */
50
50
  export function authMiddleware(config: AuthConfig) {
51
- return async (c: Context, next: Next) => {
52
- let currentUser: User | null = null;
53
-
54
- // Extract JWT from Authorization header
55
- const authHeader = c.req.header("Authorization");
56
- if (authHeader?.startsWith("Bearer ")) {
57
- const token = authHeader.slice(7);
58
- try {
59
- const payload = (await verify(token, config.jwtSecret, "HS256")) as any;
60
- currentUser = {
61
- id: payload.sub as string,
62
- email: payload.email as string,
63
- name: payload.name as string | undefined,
64
- };
65
- } catch {
66
- // Invalid token — continue as unauthenticated
67
- }
68
- }
69
-
70
- const authContext: AuthContext = {
71
- getUserIdentity: () => currentUser,
72
- requireAuth: () => {
73
- if (!currentUser) {
74
- throw new HTTPException(401, { message: "Authentication required" });
75
- }
76
- return currentUser;
77
- },
51
+ return async (c: Context, next: Next) => {
52
+ let currentUser: User | null = null;
53
+
54
+ // Extract JWT from Authorization header
55
+ const authHeader = c.req.header("Authorization");
56
+ if (authHeader?.startsWith("Bearer ")) {
57
+ const token = authHeader.slice(7);
58
+ try {
59
+ const payload = (await verify(token, config.jwtSecret, "HS256")) as any;
60
+ currentUser = {
61
+ id: payload.sub as string,
62
+ email: payload.email as string,
63
+ name: payload.name as string | undefined,
78
64
  };
79
-
80
- c.set("auth", authContext);
81
- await next();
65
+ } catch {
66
+ // Invalid token — continue as unauthenticated
67
+ }
68
+ }
69
+
70
+ const authContext: AuthContext = {
71
+ getUserIdentity: () => currentUser,
72
+ requireAuth: () => {
73
+ if (!currentUser) {
74
+ throw new HTTPException(401, { message: "Authentication required" });
75
+ }
76
+ return currentUser;
77
+ },
82
78
  };
79
+
80
+ c.set("auth", authContext);
81
+ await next();
82
+ };
83
83
  }
84
84
 
85
85
  // ─── Auth routes — 회원가입/로그인/프로필 ────────────────
86
86
 
87
87
  export function authRoutes(config: AuthConfig) {
88
- return {
89
- /** POST /auth/signup — 회원가입 */
90
- async signup(c: Context) {
91
- const { email, password, name } = await c.req.json();
92
-
93
- if (!email || !password) {
94
- return c.json({ error: "Email and password required" }, 400);
95
- }
96
-
97
- if (users.has(email)) {
98
- return c.json({ error: "User already exists" }, 409);
99
- }
100
-
101
- const id = crypto.randomUUID();
102
- const passwordHash = await hashPassword(password);
103
- users.set(email, { id, email, name, passwordHash, createdAt: new Date().toISOString() });
104
-
105
- const token = await sign(
106
- { sub: id, email, name, exp: Math.floor(Date.now() / 1000) + 86400 },
107
- config.jwtSecret
108
- );
109
-
110
- return c.json({ token, user: { id, email, name } });
111
- },
112
-
113
- /** POST /auth/login — 로그인 */
114
- async login(c: Context) {
115
- const { email, password } = await c.req.json();
116
-
117
- const user = users.get(email);
118
- if (!user) {
119
- return c.json({ error: "Invalid credentials" }, 401);
120
- }
121
-
122
- const hash = await hashPassword(password);
123
- if (hash !== user.passwordHash) {
124
- return c.json({ error: "Invalid credentials" }, 401);
125
- }
126
-
127
- const token = await sign(
128
- {
129
- sub: user.id,
130
- email: user.email,
131
- name: user.name,
132
- exp: Math.floor(Date.now() / 1000) + 86400,
133
- },
134
- config.jwtSecret
135
- );
136
-
137
- return c.json({
138
- token,
139
- user: { id: user.id, email: user.email, name: user.name },
140
- });
88
+ return {
89
+ /** POST /auth/signup — 회원가입 */
90
+ async signup(c: Context) {
91
+ const { email, password, name } = await c.req.json();
92
+
93
+ if (!email || !password) {
94
+ return c.json({ error: "Email and password required" }, 400);
95
+ }
96
+
97
+ if (users.has(email)) {
98
+ return c.json({ error: "User already exists" }, 409);
99
+ }
100
+
101
+ const id = crypto.randomUUID();
102
+ const passwordHash = await hashPassword(password);
103
+ users.set(email, { id, email, name, passwordHash, createdAt: new Date().toISOString() });
104
+
105
+ const token = await sign(
106
+ { sub: id, email, name, exp: Math.floor(Date.now() / 1000) + 86400 },
107
+ config.jwtSecret,
108
+ );
109
+
110
+ return c.json({ token, user: { id, email, name } });
111
+ },
112
+
113
+ /** POST /auth/login — 로그인 */
114
+ async login(c: Context) {
115
+ const { email, password } = await c.req.json();
116
+
117
+ const user = users.get(email);
118
+ if (!user) {
119
+ return c.json({ error: "Invalid credentials" }, 401);
120
+ }
121
+
122
+ const hash = await hashPassword(password);
123
+ if (hash !== user.passwordHash) {
124
+ return c.json({ error: "Invalid credentials" }, 401);
125
+ }
126
+
127
+ const token = await sign(
128
+ {
129
+ sub: user.id,
130
+ email: user.email,
131
+ name: user.name,
132
+ exp: Math.floor(Date.now() / 1000) + 86400,
141
133
  },
142
-
143
- /** GET /auth/me — 현재 유저 정보 */
144
- async me(c: Context) {
145
- const auth: AuthContext = c.get("auth");
146
- const user = auth.requireAuth();
147
- return c.json(user);
148
- },
149
- };
134
+ config.jwtSecret,
135
+ );
136
+
137
+ return c.json({
138
+ token,
139
+ user: { id: user.id, email: user.email, name: user.name },
140
+ });
141
+ },
142
+
143
+ /** GET /auth/me — 현재 유저 정보 */
144
+ async me(c: Context) {
145
+ const auth: AuthContext = c.get("auth");
146
+ const user = auth.requireAuth();
147
+ return c.json(user);
148
+ },
149
+ };
150
150
  }
151
151
 
152
152
  /** Get all registered users (for admin dashboard) */
153
153
  export function getUsers(): (User & { createdAt: string })[] {
154
- return Array.from(users.values()).map(({ passwordHash, ...user }) => user);
154
+ return Array.from(users.values()).map(({ passwordHash, ...user }) => user);
155
155
  }
package/src/crons.ts CHANGED
@@ -19,50 +19,50 @@
19
19
  // ─── 타입 ───────────────────────────────────────────────
20
20
 
21
21
  export interface CronJobDef {
22
- /** 크론 잡 이름 */
23
- name: string;
24
- /** cron 표현식 (예: "0 2 * * *") */
25
- pattern: string;
26
- /** 실행할 액션 또는 핸들러 */
27
- action: string | (() => Promise<void>);
22
+ /** 크론 잡 이름 */
23
+ name: string;
24
+ /** cron 표현식 (예: "0 2 * * *") */
25
+ pattern: string;
26
+ /** 실행할 액션 또는 핸들러 */
27
+ action: string | (() => Promise<void>);
28
28
  }
29
29
 
30
30
  export interface IntervalOptions {
31
- /** 분 단위 */
32
- minutes?: number;
33
- /** 시간 단위 */
34
- hours?: number;
35
- /** 초 단위 */
36
- seconds?: number;
31
+ /** 분 단위 */
32
+ minutes?: number;
33
+ /** 시간 단위 */
34
+ hours?: number;
35
+ /** 초 단위 */
36
+ seconds?: number;
37
37
  }
38
38
 
39
39
  export interface DailyOptions {
40
- /** 실행 시각 (0-23, 로컬 시간) */
41
- hour: number;
42
- /** 분 (0-59, 기본 0) */
43
- minute?: number;
40
+ /** 실행 시각 (0-23, 로컬 시간) */
41
+ hour: number;
42
+ /** 분 (0-59, 기본 0) */
43
+ minute?: number;
44
44
  }
45
45
 
46
46
  export interface WeeklyOptions {
47
- /** 요일 (0=일, 1=월, ..., 6=토) */
48
- dayOfWeek: number;
49
- /** 시각 (0-23) */
50
- hour: number;
51
- /** 분 (0-59, 기본 0) */
52
- minute?: number;
47
+ /** 요일 (0=일, 1=월, ..., 6=토) */
48
+ dayOfWeek: number;
49
+ /** 시각 (0-23) */
50
+ hour: number;
51
+ /** 분 (0-59, 기본 0) */
52
+ minute?: number;
53
53
  }
54
54
 
55
55
  export interface CronJobsBuilder {
56
- /** 일정 간격으로 실행 */
57
- interval(name: string, options: IntervalOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
58
- /** 매일 특정 시각에 실행 */
59
- daily(name: string, options: DailyOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
60
- /** 매주 특정 요일/시각에 실행 */
61
- weekly(name: string, options: WeeklyOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
62
- /** cron 표현식으로 직접 지정 */
63
- cron(name: string, pattern: string, action: string | (() => Promise<void>)): CronJobsBuilder;
64
- /** 등록된 크론 잡 목록 (서버 내부에서 사용) */
65
- getJobs(): CronJobDef[];
56
+ /** 일정 간격으로 실행 */
57
+ interval(name: string, options: IntervalOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
58
+ /** 매일 특정 시각에 실행 */
59
+ daily(name: string, options: DailyOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
60
+ /** 매주 특정 요일/시각에 실행 */
61
+ weekly(name: string, options: WeeklyOptions, action: string | (() => Promise<void>)): CronJobsBuilder;
62
+ /** cron 표현식으로 직접 지정 */
63
+ cron(name: string, pattern: string, action: string | (() => Promise<void>)): CronJobsBuilder;
64
+ /** 등록된 크론 잡 목록 (서버 내부에서 사용) */
65
+ getJobs(): CronJobDef[];
66
66
  }
67
67
 
68
68
  // ─── 빌더 구현 ──────────────────────────────────────────
@@ -78,54 +78,54 @@ export interface CronJobsBuilder {
78
78
  * export default crons;
79
79
  */
80
80
  export function cronJobs(): CronJobsBuilder {
81
- const jobs: CronJobDef[] = [];
82
-
83
- const builder: CronJobsBuilder = {
84
- interval(name, options, action) {
85
- const pattern = intervalToPattern(options);
86
- jobs.push({ name, pattern, action });
87
- return builder;
88
- },
89
-
90
- daily(name, options, action) {
91
- const minute = options.minute ?? 0;
92
- const pattern = `${minute} ${options.hour} * * *`;
93
- jobs.push({ name, pattern, action });
94
- return builder;
95
- },
96
-
97
- weekly(name, options, action) {
98
- const minute = options.minute ?? 0;
99
- const pattern = `${minute} ${options.hour} * * ${options.dayOfWeek}`;
100
- jobs.push({ name, pattern, action });
101
- return builder;
102
- },
103
-
104
- cron(name, pattern, action) {
105
- jobs.push({ name, pattern, action });
106
- return builder;
107
- },
108
-
109
- getJobs() {
110
- return [...jobs];
111
- },
112
- };
113
-
114
- return builder;
81
+ const jobs: CronJobDef[] = [];
82
+
83
+ const builder: CronJobsBuilder = {
84
+ interval(name, options, action) {
85
+ const pattern = intervalToPattern(options);
86
+ jobs.push({ name, pattern, action });
87
+ return builder;
88
+ },
89
+
90
+ daily(name, options, action) {
91
+ const minute = options.minute ?? 0;
92
+ const pattern = `${minute} ${options.hour} * * *`;
93
+ jobs.push({ name, pattern, action });
94
+ return builder;
95
+ },
96
+
97
+ weekly(name, options, action) {
98
+ const minute = options.minute ?? 0;
99
+ const pattern = `${minute} ${options.hour} * * ${options.dayOfWeek}`;
100
+ jobs.push({ name, pattern, action });
101
+ return builder;
102
+ },
103
+
104
+ cron(name, pattern, action) {
105
+ jobs.push({ name, pattern, action });
106
+ return builder;
107
+ },
108
+
109
+ getJobs() {
110
+ return [...jobs];
111
+ },
112
+ };
113
+
114
+ return builder;
115
115
  }
116
116
 
117
117
  // ─── 유틸리티 ───────────────────────────────────────────
118
118
 
119
119
  function intervalToPattern(options: IntervalOptions): string {
120
- if (options.seconds) {
121
- // node-cron은 초 단위 지원: "*/N * * * * *"
122
- return `*/${options.seconds} * * * * *`;
123
- }
124
- if (options.minutes) {
125
- return `*/${options.minutes} * * * *`;
126
- }
127
- if (options.hours) {
128
- return `0 */${options.hours} * * *`;
129
- }
130
- throw new Error("interval: minutes, hours, 또는 seconds 중 하나를 지정하세요");
120
+ if (options.seconds) {
121
+ // node-cron은 초 단위 지원: "*/N * * * * *"
122
+ return `*/${options.seconds} * * * * *`;
123
+ }
124
+ if (options.minutes) {
125
+ return `*/${options.minutes} * * * *`;
126
+ }
127
+ if (options.hours) {
128
+ return `0 */${options.hours} * * *`;
129
+ }
130
+ throw new Error("interval: minutes, hours, 또는 seconds 중 하나를 지정하세요");
131
131
  }