@gencow/core 0.1.6 → 0.1.8
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/auth-config.d.ts +47 -0
- package/dist/auth-config.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/reactive.d.ts +36 -2
- package/dist/reactive.js +49 -10
- package/dist/storage.js +15 -7
- package/package.json +37 -35
- package/src/__tests__/reactive.test.ts +77 -0
- package/src/auth-config.ts +59 -0
- package/src/index.ts +2 -0
- package/src/reactive.ts +57 -11
- package/src/storage.ts +17 -7
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/auth-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Gencow Auth 설정 타입 및 defineAuth() 함수.
|
|
5
|
+
* 사용자 앱의 gencow/auth.ts 에서 사용.
|
|
6
|
+
*
|
|
7
|
+
* shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
|
|
8
|
+
* defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthEmailVerification {
|
|
11
|
+
/** 가입 시 인증 메일 자동 발송 (default: true) */
|
|
12
|
+
sendOnSignUp?: boolean;
|
|
13
|
+
/** 이메일 미인증 시 로그인 차단 (default: true) */
|
|
14
|
+
requireEmailVerification?: boolean;
|
|
15
|
+
/** 인증 완료 후 자동 로그인 (default: true) */
|
|
16
|
+
autoSignInAfterVerification?: boolean;
|
|
17
|
+
/** 인증 메일 발송 함수 — 사용자가 직접 구현 */
|
|
18
|
+
sendVerificationEmail: (data: {
|
|
19
|
+
user: {
|
|
20
|
+
email: string;
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
url: string;
|
|
24
|
+
token: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface GencowAuthConfig {
|
|
28
|
+
emailVerification?: AuthEmailVerification;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Auth 설정 정의 헬퍼.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // gencow/auth.ts
|
|
36
|
+
* import { defineAuth } from "gencow";
|
|
37
|
+
*
|
|
38
|
+
* export default defineAuth({
|
|
39
|
+
* emailVerification: {
|
|
40
|
+
* sendVerificationEmail: async ({ user, url }) => {
|
|
41
|
+
* // 이메일 발송 로직
|
|
42
|
+
* },
|
|
43
|
+
* },
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function defineAuth(config: GencowAuthConfig): GencowAuthConfig;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/auth-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Gencow Auth 설정 타입 및 defineAuth() 함수.
|
|
5
|
+
* 사용자 앱의 gencow/auth.ts 에서 사용.
|
|
6
|
+
*
|
|
7
|
+
* shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
|
|
8
|
+
* defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
|
|
9
|
+
*/
|
|
10
|
+
// ─── defineAuth() ────────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Auth 설정 정의 헬퍼.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // gencow/auth.ts
|
|
17
|
+
* import { defineAuth } from "gencow";
|
|
18
|
+
*
|
|
19
|
+
* export default defineAuth({
|
|
20
|
+
* emailVerification: {
|
|
21
|
+
* sendVerificationEmail: async ({ user, url }) => {
|
|
22
|
+
* // 이메일 발송 로직
|
|
23
|
+
* },
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function defineAuth(config) {
|
|
29
|
+
return config;
|
|
30
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,3 +15,5 @@ export { withRetry } from "./retry";
|
|
|
15
15
|
export type { RetryOptions } from "./retry";
|
|
16
16
|
export { cronJobs } from "./crons";
|
|
17
17
|
export type { CronJobsBuilder, CronJobDef, IntervalOptions, DailyOptions, WeeklyOptions } from "./crons";
|
|
18
|
+
export { defineAuth } from "./auth-config";
|
|
19
|
+
export type { GencowAuthConfig, AuthEmailVerification } from "./auth-config";
|
package/dist/index.js
CHANGED
package/dist/reactive.d.ts
CHANGED
|
@@ -55,6 +55,10 @@ export interface AIContext {
|
|
|
55
55
|
temperature?: number;
|
|
56
56
|
maxTokens?: number;
|
|
57
57
|
}) => Promise<AIResult>;
|
|
58
|
+
/** 텍스트 임베딩 (단일) — ctx.ai.embed("검색 텍스트") */
|
|
59
|
+
embed: (text: string) => Promise<number[]>;
|
|
60
|
+
/** 배치 임베딩 — ctx.ai.embedMany(["텍스트1", "텍스트2"]) */
|
|
61
|
+
embedMany: (texts: string[]) => Promise<number[][]>;
|
|
58
62
|
}
|
|
59
63
|
export interface GencowCtx {
|
|
60
64
|
/** Drizzle DB 인스턴스 — ctx.db.select().from(table) */
|
|
@@ -149,13 +153,43 @@ export declare function query<TSchema = any, TReturn = any>(key: string, handler
|
|
|
149
153
|
public?: boolean;
|
|
150
154
|
handler: QueryHandler<InferArgs<TSchema>, TReturn>;
|
|
151
155
|
}): QueryDef<TSchema, TReturn>;
|
|
152
|
-
|
|
156
|
+
/**
|
|
157
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
158
|
+
*
|
|
159
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
164
|
+
* mutation("tasks.create", {
|
|
165
|
+
* invalidates: [],
|
|
166
|
+
* args: { title: v.string() },
|
|
167
|
+
* handler: async (ctx, args) => { ... },
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
171
|
+
* mutation({
|
|
172
|
+
* name: "tasks.create",
|
|
173
|
+
* invalidates: [],
|
|
174
|
+
* handler: async (ctx) => { ... },
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
178
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export declare function mutation<TSchema = any, TReturn = any>(nameOrInvalidatesOrDef: string | string[] | {
|
|
153
182
|
name?: string;
|
|
154
183
|
args?: TSchema;
|
|
155
184
|
public?: boolean;
|
|
156
185
|
invalidates: string[];
|
|
157
186
|
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
158
|
-
},
|
|
187
|
+
}, handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | {
|
|
188
|
+
invalidates?: string[];
|
|
189
|
+
args?: TSchema;
|
|
190
|
+
public?: boolean;
|
|
191
|
+
handler: MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
192
|
+
}, name?: string): MutationDef<TSchema, TReturn>;
|
|
159
193
|
/**
|
|
160
194
|
* 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
|
|
161
195
|
* query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
|
package/dist/reactive.js
CHANGED
|
@@ -36,25 +36,64 @@ export function query(key, handlerOrDef) {
|
|
|
36
36
|
return def;
|
|
37
37
|
}
|
|
38
38
|
let mutationCounter = 0;
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
41
|
+
*
|
|
42
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
47
|
+
* mutation("tasks.create", {
|
|
48
|
+
* invalidates: [],
|
|
49
|
+
* args: { title: v.string() },
|
|
50
|
+
* handler: async (ctx, args) => { ... },
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
54
|
+
* mutation({
|
|
55
|
+
* name: "tasks.create",
|
|
56
|
+
* invalidates: [],
|
|
57
|
+
* handler: async (ctx) => { ... },
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
61
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
|
|
40
65
|
let invalidates;
|
|
41
66
|
let argsSchema;
|
|
42
67
|
let actualHandler;
|
|
43
68
|
let mutName;
|
|
44
69
|
let isPublic = false;
|
|
45
|
-
if (
|
|
70
|
+
if (typeof nameOrInvalidatesOrDef === "string") {
|
|
71
|
+
// New primary style: mutation("name", { invalidates?, args?, public?, handler })
|
|
72
|
+
mutName = nameOrInvalidatesOrDef;
|
|
73
|
+
const def = handlerOrDef;
|
|
74
|
+
invalidates = def.invalidates || [];
|
|
75
|
+
actualHandler = def.handler;
|
|
76
|
+
argsSchema = def.args;
|
|
77
|
+
isPublic = def.public === true;
|
|
78
|
+
}
|
|
79
|
+
else if (Array.isArray(nameOrInvalidatesOrDef)) {
|
|
46
80
|
// Legacy style: mutation([...], handler, "name")
|
|
47
|
-
invalidates =
|
|
48
|
-
actualHandler =
|
|
81
|
+
invalidates = nameOrInvalidatesOrDef;
|
|
82
|
+
actualHandler = handlerOrDef;
|
|
49
83
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
50
84
|
}
|
|
51
85
|
else {
|
|
52
|
-
//
|
|
53
|
-
invalidates =
|
|
54
|
-
actualHandler =
|
|
55
|
-
argsSchema =
|
|
56
|
-
isPublic =
|
|
57
|
-
mutName =
|
|
86
|
+
// Object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
87
|
+
invalidates = nameOrInvalidatesOrDef.invalidates;
|
|
88
|
+
actualHandler = nameOrInvalidatesOrDef.handler;
|
|
89
|
+
argsSchema = nameOrInvalidatesOrDef.args;
|
|
90
|
+
isPublic = nameOrInvalidatesOrDef.public === true;
|
|
91
|
+
mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
|
|
92
|
+
}
|
|
93
|
+
// 이름 미지정 시 경고 — 디버깅 지원
|
|
94
|
+
if (mutName.startsWith("mutation_")) {
|
|
95
|
+
console.warn(`[gencow] mutation registered without explicit name → "${mutName}". ` +
|
|
96
|
+
`Use mutation("myMutation", { handler }) for better debugging.`);
|
|
58
97
|
}
|
|
59
98
|
const def = {
|
|
60
99
|
name: mutName,
|
package/dist/storage.js
CHANGED
|
@@ -104,13 +104,21 @@ export function storageRoutes(storage, rawSql, storageDir) {
|
|
|
104
104
|
if (!meta) {
|
|
105
105
|
return c.json({ error: "Not found" }, 404);
|
|
106
106
|
}
|
|
107
|
+
// Bun 런타임에서는 Bun.file()을 사용하여 바이너리 무결성 보장
|
|
108
|
+
// Node의 fs.readFile()은 Bun에서 Buffer 인코딩 문제가 있을 수 있음
|
|
109
|
+
const headers = {
|
|
110
|
+
"Content-Type": meta.type,
|
|
111
|
+
"Content-Disposition": `inline; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
|
|
112
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
113
|
+
};
|
|
114
|
+
// Bun은 Bun.file()로 직접 BunFile(Blob) 생성 → Response에 그대로 전달
|
|
115
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
116
|
+
const bunFile = Bun.file(meta.path);
|
|
117
|
+
return new Response(bunFile, { headers });
|
|
118
|
+
}
|
|
119
|
+
// Node.js 폴백
|
|
107
120
|
const file = await fs.readFile(meta.path);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"Content-Type": meta.type,
|
|
111
|
-
"Content-Disposition": `attachment; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
|
|
112
|
-
"Content-Length": String(meta.size),
|
|
113
|
-
},
|
|
114
|
-
});
|
|
121
|
+
headers["Content-Length"] = String(file.byteLength);
|
|
122
|
+
return c.body(file, 200, headers);
|
|
115
123
|
};
|
|
116
124
|
}
|
package/package.json
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
"name": "@gencow/core",
|
|
3
|
+
"version": "0.1.8",
|
|
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
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"import": "./dist/server.js",
|
|
15
|
+
"types": "./dist/server.d.ts"
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
"files": [
|
|
19
|
+
"dist/",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"postinstall": "tsc"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
30
|
+
"drizzle-orm": "^0.45.1",
|
|
31
|
+
"hono": "^4.12.0",
|
|
32
|
+
"node-cron": "^4.2.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "^1.3.9",
|
|
36
|
+
"@types/node": "^25.3.0",
|
|
37
|
+
"@types/node-cron": "^3.0.11",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
16
39
|
}
|
|
17
|
-
|
|
18
|
-
"files": [
|
|
19
|
-
"dist/",
|
|
20
|
-
"src/"
|
|
21
|
-
],
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@electric-sql/pglite": "^0.3.15",
|
|
24
|
-
"drizzle-orm": "^0.45.1",
|
|
25
|
-
"hono": "^4.12.0",
|
|
26
|
-
"node-cron": "^4.2.1"
|
|
27
|
-
},
|
|
28
|
-
"devDependencies": {
|
|
29
|
-
"@types/bun": "^1.3.9",
|
|
30
|
-
"@types/node": "^25.3.0",
|
|
31
|
-
"@types/node-cron": "^3.0.11",
|
|
32
|
-
"typescript": "^5.9.3"
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc",
|
|
36
|
-
"typecheck": "tsc --noEmit"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
40
|
+
}
|
|
@@ -235,3 +235,80 @@ describe("Secure by Default — public 플래그", () => {
|
|
|
235
235
|
expect(priv?.isPublic).toBe(false);
|
|
236
236
|
});
|
|
237
237
|
});
|
|
238
|
+
|
|
239
|
+
// ─── mutation("name", def) 새 시그니처 테스트 ────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("mutation(name, def) — query와 동일 패턴", () => {
|
|
242
|
+
it("mutation('name', { handler })로 등록하면 name이 올바르게 설정된다", () => {
|
|
243
|
+
const m = mutation("newsig.basic", {
|
|
244
|
+
handler: async () => ({ ok: true }),
|
|
245
|
+
});
|
|
246
|
+
expect((m as any).name || (getRegisteredMutations().find(x => x.invalidates.length === 0 && x.handler === (m as any).handler) as any)?.name).toBeDefined();
|
|
247
|
+
const all = getRegisteredMutations();
|
|
248
|
+
const found = all.find(x => x.name === "newsig.basic");
|
|
249
|
+
expect(found).toBeDefined();
|
|
250
|
+
expect(found!.isPublic).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("mutation('name', { public: true })로 등록하면 isPublic === true", () => {
|
|
254
|
+
const m = mutation("newsig.public", {
|
|
255
|
+
public: true,
|
|
256
|
+
handler: async () => ({ ok: true }),
|
|
257
|
+
});
|
|
258
|
+
expect(m.isPublic).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("invalidates 미지정 시 빈 배열이 기본값", () => {
|
|
262
|
+
const m = mutation("newsig.noInvalidates", {
|
|
263
|
+
handler: async () => ({ ok: true }),
|
|
264
|
+
});
|
|
265
|
+
const all = getRegisteredMutations();
|
|
266
|
+
const found = all.find(x => x.name === "newsig.noInvalidates");
|
|
267
|
+
expect(found!.invalidates).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("invalidates 지정 시 올바르게 전달된다", () => {
|
|
271
|
+
const m = mutation("newsig.withInvalidates", {
|
|
272
|
+
invalidates: ["tasks.list", "tasks.get"],
|
|
273
|
+
handler: async () => ({ ok: true }),
|
|
274
|
+
});
|
|
275
|
+
const all = getRegisteredMutations();
|
|
276
|
+
const found = all.find(x => x.name === "newsig.withInvalidates");
|
|
277
|
+
expect(found!.invalidates).toEqual(["tasks.list", "tasks.get"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("기존 객체 스타일도 여전히 동작한다 (하위 호환)", () => {
|
|
281
|
+
const m = mutation({
|
|
282
|
+
name: "newsig.compat.object",
|
|
283
|
+
invalidates: ["a.list"],
|
|
284
|
+
handler: async () => ({ ok: true }),
|
|
285
|
+
});
|
|
286
|
+
const all = getRegisteredMutations();
|
|
287
|
+
const found = all.find(x => x.name === "newsig.compat.object");
|
|
288
|
+
expect(found).toBeDefined();
|
|
289
|
+
expect(found!.invalidates).toEqual(["a.list"]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("기존 배열 스타일도 여전히 동작한다 (하위 호환)", () => {
|
|
293
|
+
const m = mutation(["b.list"], async () => ({ ok: true }), "newsig.compat.array");
|
|
294
|
+
const all = getRegisteredMutations();
|
|
295
|
+
const found = all.find(x => x.name === "newsig.compat.array");
|
|
296
|
+
expect(found).toBeDefined();
|
|
297
|
+
expect(found!.invalidates).toEqual(["b.list"]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("이름 미지정 시 console.warn이 호출된다", () => {
|
|
301
|
+
const warnSpy = mock(() => {});
|
|
302
|
+
const originalWarn = console.warn;
|
|
303
|
+
console.warn = warnSpy;
|
|
304
|
+
|
|
305
|
+
mutation(["c.list"], async () => ({ ok: true }));
|
|
306
|
+
|
|
307
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
308
|
+
const warnMsg = warnSpy.mock.calls[0][0] as string;
|
|
309
|
+
expect(warnMsg).toContain("[gencow]");
|
|
310
|
+
expect(warnMsg).toContain("without explicit name");
|
|
311
|
+
|
|
312
|
+
console.warn = originalWarn;
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/auth-config.ts
|
|
3
|
+
*
|
|
4
|
+
* Gencow Auth 설정 타입 및 defineAuth() 함수.
|
|
5
|
+
* 사용자 앱의 gencow/auth.ts 에서 사용.
|
|
6
|
+
*
|
|
7
|
+
* shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
|
|
8
|
+
* defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Email Verification ──────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface AuthEmailVerification {
|
|
14
|
+
/** 가입 시 인증 메일 자동 발송 (default: true) */
|
|
15
|
+
sendOnSignUp?: boolean;
|
|
16
|
+
/** 이메일 미인증 시 로그인 차단 (default: true) */
|
|
17
|
+
requireEmailVerification?: boolean;
|
|
18
|
+
/** 인증 완료 후 자동 로그인 (default: true) */
|
|
19
|
+
autoSignInAfterVerification?: boolean;
|
|
20
|
+
/** 인증 메일 발송 함수 — 사용자가 직접 구현 */
|
|
21
|
+
sendVerificationEmail: (data: {
|
|
22
|
+
user: { email: string; name: string };
|
|
23
|
+
url: string;
|
|
24
|
+
token: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Auth Config ─────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface GencowAuthConfig {
|
|
31
|
+
emailVerification?: AuthEmailVerification;
|
|
32
|
+
// 확장 예정:
|
|
33
|
+
// socialProviders?: { ... }
|
|
34
|
+
// passwordPolicy?: { ... }
|
|
35
|
+
// sessionExpiry?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── defineAuth() ────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Auth 설정 정의 헬퍼.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // gencow/auth.ts
|
|
46
|
+
* import { defineAuth } from "gencow";
|
|
47
|
+
*
|
|
48
|
+
* export default defineAuth({
|
|
49
|
+
* emailVerification: {
|
|
50
|
+
* sendVerificationEmail: async ({ user, url }) => {
|
|
51
|
+
* // 이메일 발송 로직
|
|
52
|
+
* },
|
|
53
|
+
* },
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function defineAuth(config: GencowAuthConfig): GencowAuthConfig {
|
|
58
|
+
return config;
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,5 +16,7 @@ export { withRetry } from "./retry";
|
|
|
16
16
|
export type { RetryOptions } from "./retry";
|
|
17
17
|
export { cronJobs } from "./crons";
|
|
18
18
|
export type { CronJobsBuilder, CronJobDef, IntervalOptions, DailyOptions, WeeklyOptions } from "./crons";
|
|
19
|
+
export { defineAuth } from "./auth-config";
|
|
20
|
+
export type { GencowAuthConfig, AuthEmailVerification } from "./auth-config";
|
|
19
21
|
|
|
20
22
|
|
package/src/reactive.ts
CHANGED
|
@@ -61,6 +61,10 @@ export interface AIContext {
|
|
|
61
61
|
temperature?: number;
|
|
62
62
|
maxTokens?: number;
|
|
63
63
|
}) => Promise<AIResult>;
|
|
64
|
+
/** 텍스트 임베딩 (단일) — ctx.ai.embed("검색 텍스트") */
|
|
65
|
+
embed: (text: string) => Promise<number[]>;
|
|
66
|
+
/** 배치 임베딩 — ctx.ai.embedMany(["텍스트1", "텍스트2"]) */
|
|
67
|
+
embedMany: (texts: string[]) => Promise<number[][]>;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
export interface GencowCtx {
|
|
@@ -216,9 +220,34 @@ export function query<TSchema = any, TReturn = any>(
|
|
|
216
220
|
|
|
217
221
|
let mutationCounter = 0;
|
|
218
222
|
|
|
223
|
+
/**
|
|
224
|
+
* mutation — 데이터 변경 함수를 선언적으로 등록합니다.
|
|
225
|
+
*
|
|
226
|
+
* 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* // ✅ 권장: query와 동일한 (name, def) 패턴
|
|
231
|
+
* mutation("tasks.create", {
|
|
232
|
+
* invalidates: [],
|
|
233
|
+
* args: { title: v.string() },
|
|
234
|
+
* handler: async (ctx, args) => { ... },
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* // ✅ 객체 스타일 (하위 호환)
|
|
238
|
+
* mutation({
|
|
239
|
+
* name: "tasks.create",
|
|
240
|
+
* invalidates: [],
|
|
241
|
+
* handler: async (ctx) => { ... },
|
|
242
|
+
* });
|
|
243
|
+
*
|
|
244
|
+
* // ⚠️ Legacy 배열 스타일 (비권장)
|
|
245
|
+
* mutation(["tasks.list"], handler, "tasks.create");
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
219
248
|
export function mutation<TSchema = any, TReturn = any>(
|
|
220
|
-
|
|
221
|
-
|
|
249
|
+
nameOrInvalidatesOrDef: string | string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
250
|
+
handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
|
|
222
251
|
name?: string
|
|
223
252
|
): MutationDef<TSchema, TReturn> {
|
|
224
253
|
let invalidates: string[];
|
|
@@ -227,19 +256,36 @@ export function mutation<TSchema = any, TReturn = any>(
|
|
|
227
256
|
let mutName: string;
|
|
228
257
|
let isPublic = false;
|
|
229
258
|
|
|
230
|
-
if (
|
|
259
|
+
if (typeof nameOrInvalidatesOrDef === "string") {
|
|
260
|
+
// New primary style: mutation("name", { invalidates?, args?, public?, handler })
|
|
261
|
+
mutName = nameOrInvalidatesOrDef;
|
|
262
|
+
const def = handlerOrDef as { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> };
|
|
263
|
+
invalidates = def.invalidates || [];
|
|
264
|
+
actualHandler = def.handler;
|
|
265
|
+
argsSchema = def.args;
|
|
266
|
+
isPublic = def.public === true;
|
|
267
|
+
} else if (Array.isArray(nameOrInvalidatesOrDef)) {
|
|
231
268
|
// Legacy style: mutation([...], handler, "name")
|
|
232
|
-
invalidates =
|
|
233
|
-
actualHandler =
|
|
269
|
+
invalidates = nameOrInvalidatesOrDef;
|
|
270
|
+
actualHandler = handlerOrDef as MutationHandler<InferArgs<TSchema>, TReturn>;
|
|
234
271
|
mutName = name || `mutation_${++mutationCounter}`;
|
|
235
272
|
} else {
|
|
236
|
-
//
|
|
237
|
-
invalidates =
|
|
238
|
-
actualHandler =
|
|
239
|
-
argsSchema =
|
|
240
|
-
isPublic =
|
|
241
|
-
mutName =
|
|
273
|
+
// Object style: mutation({ name?, invalidates, args?, public?, handler })
|
|
274
|
+
invalidates = nameOrInvalidatesOrDef.invalidates;
|
|
275
|
+
actualHandler = nameOrInvalidatesOrDef.handler;
|
|
276
|
+
argsSchema = nameOrInvalidatesOrDef.args;
|
|
277
|
+
isPublic = nameOrInvalidatesOrDef.public === true;
|
|
278
|
+
mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
|
|
242
279
|
}
|
|
280
|
+
|
|
281
|
+
// 이름 미지정 시 경고 — 디버깅 지원
|
|
282
|
+
if (mutName.startsWith("mutation_")) {
|
|
283
|
+
console.warn(
|
|
284
|
+
`[gencow] mutation registered without explicit name → "${mutName}". ` +
|
|
285
|
+
`Use mutation("myMutation", { handler }) for better debugging.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
243
289
|
const def: MutationDef<TSchema, TReturn> & { name: string } = {
|
|
244
290
|
name: mutName,
|
|
245
291
|
invalidates,
|
package/src/storage.ts
CHANGED
|
@@ -162,13 +162,23 @@ export function storageRoutes(
|
|
|
162
162
|
return c.json({ error: "Not found" }, 404);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// Bun 런타임에서는 Bun.file()을 사용하여 바이너리 무결성 보장
|
|
166
|
+
// Node의 fs.readFile()은 Bun에서 Buffer 인코딩 문제가 있을 수 있음
|
|
167
|
+
const headers: Record<string, string> = {
|
|
168
|
+
"Content-Type": meta.type,
|
|
169
|
+
"Content-Disposition": `inline; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
|
|
170
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Bun은 Bun.file()로 직접 BunFile(Blob) 생성 → Response에 그대로 전달
|
|
174
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
175
|
+
const bunFile = Bun.file(meta.path);
|
|
176
|
+
return new Response(bunFile, { headers });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Node.js 폴백
|
|
165
180
|
const file = await fs.readFile(meta.path);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"Content-Type": meta.type,
|
|
169
|
-
"Content-Disposition": `attachment; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
|
|
170
|
-
"Content-Length": String(meta.size),
|
|
171
|
-
},
|
|
172
|
-
});
|
|
181
|
+
headers["Content-Length"] = String(file.byteLength);
|
|
182
|
+
return c.body(file, 200, headers);
|
|
173
183
|
};
|
|
174
184
|
}
|