@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.
- 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 +46 -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 +50 -44
- 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/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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
interface AuthContext {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
/** 분 단위 */
|
|
32
|
+
minutes?: number;
|
|
33
|
+
/** 시간 단위 */
|
|
34
|
+
hours?: number;
|
|
35
|
+
/** 초 단위 */
|
|
36
|
+
seconds?: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface DailyOptions {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
/** 실행 시각 (0-23, 로컬 시간) */
|
|
41
|
+
hour: number;
|
|
42
|
+
/** 분 (0-59, 기본 0) */
|
|
43
|
+
minute?: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface WeeklyOptions {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
}
|