@gencow/core 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.js +4 -1
  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-types.d.ts +81 -0
  16. package/dist/workflow-types.js +12 -0
  17. package/dist/workflow.d.ts +30 -0
  18. package/dist/workflow.js +150 -0
  19. package/dist/workflows-api.d.ts +13 -0
  20. package/dist/workflows-api.js +321 -0
  21. package/package.json +46 -42
  22. package/src/__tests__/auth.test.ts +90 -86
  23. package/src/__tests__/crons.test.ts +69 -67
  24. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  25. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  26. package/src/__tests__/crud.test.ts +694 -711
  27. package/src/__tests__/dist-exports.test.ts +120 -114
  28. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  29. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  30. package/src/__tests__/fixtures/basic/index.ts +1 -1
  31. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  32. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  33. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  34. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  35. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  36. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  37. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  38. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  39. package/src/__tests__/httpaction.test.ts +91 -91
  40. package/src/__tests__/image-optimization.test.ts +570 -574
  41. package/src/__tests__/load.test.ts +321 -308
  42. package/src/__tests__/network-sim.test.ts +238 -215
  43. package/src/__tests__/reactive.test.ts +380 -358
  44. package/src/__tests__/retry.test.ts +99 -84
  45. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  46. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  47. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  48. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  49. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  50. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  51. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  52. package/src/__tests__/scheduler-durable.test.ts +117 -117
  53. package/src/__tests__/scheduler-exec.test.ts +258 -246
  54. package/src/__tests__/scheduler.test.ts +129 -111
  55. package/src/__tests__/storage.test.ts +282 -269
  56. package/src/__tests__/tsconfig.json +6 -6
  57. package/src/__tests__/validator.test.ts +236 -232
  58. package/src/__tests__/workflow.test.ts +606 -0
  59. package/src/__tests__/ws-integration.test.ts +223 -218
  60. package/src/__tests__/ws-scale.test.ts +168 -159
  61. package/src/auth-config.ts +18 -18
  62. package/src/auth.ts +106 -106
  63. package/src/crons.ts +77 -77
  64. package/src/crud.ts +523 -479
  65. package/src/index.ts +71 -6
  66. package/src/reactive.ts +357 -331
  67. package/src/retry.ts +51 -54
  68. package/src/rls-db.ts +195 -205
  69. package/src/rls.ts +33 -36
  70. package/src/scheduler.ts +237 -211
  71. package/src/server.ts +0 -1
  72. package/src/storage.ts +632 -593
  73. package/src/v.ts +119 -114
  74. package/src/workflow-types.ts +108 -0
  75. package/src/workflow.ts +188 -0
  76. package/src/workflows-api.ts +415 -0
  77. 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
  }