@gencow/core 0.1.27 → 0.1.28
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/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +4 -0
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1557 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive.d.ts +13 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +2 -26
- package/dist/storage.js +19 -15
- package/dist/workflow-types.d.ts +3 -1
- package/package.json +8 -7
- package/src/document-types.ts +95 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/index.ts +66 -1
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive.ts +13 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +1 -2
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +29 -46
- package/src/workflow-types.ts +3 -1
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
/** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
|
|
5
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/** 기본 스토리지 쿼터: 1GB */
|
|
8
|
+
export const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export interface StorageFile {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
type: string;
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StoredFile {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
size: number;
|
|
22
|
+
type: string;
|
|
23
|
+
buffer: Buffer;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StorageOptions {
|
|
27
|
+
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
28
|
+
storageQuota?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Storage {
|
|
32
|
+
store(file: File | Blob, filename?: string): Promise<string>;
|
|
33
|
+
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
34
|
+
get(storageId: string): Promise<StoredFile | null>;
|
|
35
|
+
getUrl(storageId: string): string;
|
|
36
|
+
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
37
|
+
delete(storageId: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatBytes(bytes: number): string {
|
|
41
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
42
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
43
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
44
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function loadStorageMeta(params: {
|
|
48
|
+
storageId: string;
|
|
49
|
+
dir: string;
|
|
50
|
+
metaStore: Map<string, StorageFile>;
|
|
51
|
+
}): Promise<StorageFile | null> {
|
|
52
|
+
const cached = params.metaStore.get(params.storageId);
|
|
53
|
+
if (cached) {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const filePath = path.join(params.dir, params.storageId);
|
|
58
|
+
try {
|
|
59
|
+
const raw = await fs.readFile(`${filePath}.meta`, "utf-8");
|
|
60
|
+
const parsed = JSON.parse(raw) as { name?: unknown; type?: unknown; size?: unknown };
|
|
61
|
+
const stat = await fs.stat(filePath);
|
|
62
|
+
const meta: StorageFile = {
|
|
63
|
+
id: params.storageId,
|
|
64
|
+
name: typeof parsed.name === "string" ? parsed.name : params.storageId,
|
|
65
|
+
size: typeof parsed.size === "number" ? parsed.size : stat.size,
|
|
66
|
+
type: typeof parsed.type === "string" ? parsed.type : "application/octet-stream",
|
|
67
|
+
path: filePath,
|
|
68
|
+
};
|
|
69
|
+
params.metaStore.set(params.storageId, meta);
|
|
70
|
+
return meta;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -2,51 +2,15 @@ import * as fs from "fs/promises";
|
|
|
2
2
|
import * as fsSync from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import * as crypto from "crypto";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_STORAGE_QUOTA,
|
|
7
|
+
MAX_FILE_SIZE,
|
|
8
|
+
formatBytes,
|
|
9
|
+
loadStorageMeta,
|
|
10
|
+
} from "./storage-shared.js";
|
|
11
|
+
import type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
|
|
5
12
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
|
|
9
|
-
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
10
|
-
|
|
11
|
-
/** 기본 스토리지 쿼터: 1GB */
|
|
12
|
-
const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
|
|
13
|
-
|
|
14
|
-
function formatBytes(bytes: number): string {
|
|
15
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
16
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
17
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
18
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ─── Types ──────────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
interface StorageFile {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
size: number;
|
|
27
|
-
type: string;
|
|
28
|
-
path: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface StorageOptions {
|
|
32
|
-
/** Raw SQL 실행 함수 — DB 자동 기록 + 쿼터 검증에 필요 */
|
|
33
|
-
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
34
|
-
/** 앱별 스토리지 쿼터 (bytes). 0 = 무제한. 기본: 1GB */
|
|
35
|
-
storageQuota?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Storage {
|
|
39
|
-
/** Store a file and return a storageId — Convex의 ctx.storage.store() */
|
|
40
|
-
store(file: File | Blob, filename?: string): Promise<string>;
|
|
41
|
-
/** Store from raw buffer */
|
|
42
|
-
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
43
|
-
/** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
|
|
44
|
-
getUrl(storageId: string): string;
|
|
45
|
-
/** Get file metadata */
|
|
46
|
-
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
47
|
-
/** Delete a stored file — Convex의 ctx.storage.delete() */
|
|
48
|
-
delete(storageId: string): Promise<void>;
|
|
49
|
-
}
|
|
13
|
+
export type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
|
|
50
14
|
|
|
51
15
|
// ─── Implementation ─────────────────────────────────────
|
|
52
16
|
|
|
@@ -147,7 +111,6 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
|
|
|
147
111
|
|
|
148
112
|
// Ensure directory exists
|
|
149
113
|
fs.mkdir(dir, { recursive: true }).catch(() => {});
|
|
150
|
-
|
|
151
114
|
return {
|
|
152
115
|
async store(file: File | Blob, filename?: string): Promise<string> {
|
|
153
116
|
// 크기 제한 검증 (단일 파일)
|
|
@@ -248,8 +211,28 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
|
|
|
248
211
|
return `/api/storage/${storageId}`;
|
|
249
212
|
},
|
|
250
213
|
|
|
214
|
+
async get(storageId: string): Promise<StoredFile | null> {
|
|
215
|
+
const meta = await loadStorageMeta({ storageId, dir, metaStore });
|
|
216
|
+
if (!meta) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const buffer = await fs.readFile(meta.path).catch(() => null);
|
|
221
|
+
if (!buffer) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
id: meta.id,
|
|
227
|
+
name: meta.name,
|
|
228
|
+
size: meta.size,
|
|
229
|
+
type: meta.type,
|
|
230
|
+
buffer,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
|
|
251
234
|
async getMeta(storageId: string): Promise<StorageFile | null> {
|
|
252
|
-
return
|
|
235
|
+
return loadStorageMeta({ storageId, dir, metaStore });
|
|
253
236
|
},
|
|
254
237
|
|
|
255
238
|
async delete(storageId: string): Promise<void> {
|
package/src/workflow-types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GencowCtx } from "./reactive.js";
|
|
2
|
+
import type { WorkflowDocumentServicesCtx } from "./document-types.js";
|
|
2
3
|
import type { InferArgs } from "./v.js";
|
|
3
4
|
|
|
4
5
|
export type WorkflowStatus = "pending" | "running" | "completed" | "failed";
|
|
@@ -77,9 +78,10 @@ export interface WorkflowResumePayload {
|
|
|
77
78
|
workflowId: string;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
export interface WorkflowCtx extends GencowCtx {
|
|
81
|
+
export interface WorkflowCtx extends Omit<GencowCtx, "services"> {
|
|
81
82
|
workflowId: string;
|
|
82
83
|
workflowName: string;
|
|
84
|
+
services: GencowCtx["services"] & WorkflowDocumentServicesCtx;
|
|
83
85
|
step<TResult>(name: string, run: () => Promise<TResult>): Promise<TResult>;
|
|
84
86
|
sleep(duration: WorkflowDuration): Promise<void>;
|
|
85
87
|
waitForEvent<TPayload = unknown>(name: string, timeout?: WorkflowDuration): Promise<TPayload>;
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* packages/core/src/__tests__/auth.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests for auth module — AuthCtx, defineAuth, auth-config.
|
|
5
|
-
*
|
|
6
|
-
* Run: bun test packages/core/src/__tests__/auth.test.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from "bun:test";
|
|
10
|
-
import { defineAuth } from "../auth-config.js";
|
|
11
|
-
import type { GencowAuthConfig } from "../auth-config.js";
|
|
12
|
-
|
|
13
|
-
// ─── defineAuth() ───────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
describe("defineAuth()", () => {
|
|
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
|
-
},
|
|
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);
|
|
36
|
-
});
|
|
37
|
-
|
|
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
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ─── AuthCtx interface 패턴 ─────────────────────────────
|
|
51
|
-
|
|
52
|
-
describe("AuthCtx 패턴 (mock)", () => {
|
|
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
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ─── Secure by Default 검증 ─────────────────────────────
|
|
102
|
-
|
|
103
|
-
describe("Secure by Default — 기본 인증 필수", () => {
|
|
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
|
-
});
|
|
118
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { cronJobs } from "../crons.js";
|
|
3
|
-
|
|
4
|
-
describe("cronJobs 빌더", () => {
|
|
5
|
-
test("interval — minutes → cron 패턴 변환", () => {
|
|
6
|
-
const crons = cronJobs();
|
|
7
|
-
crons.interval("test", { minutes: 15 }, "test.action");
|
|
8
|
-
const jobs = crons.getJobs();
|
|
9
|
-
expect(jobs).toHaveLength(1);
|
|
10
|
-
expect(jobs[0].name).toBe("test");
|
|
11
|
-
expect(jobs[0].pattern).toBe("*/15 * * * *");
|
|
12
|
-
expect(jobs[0].action).toBe("test.action");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("interval — hours → cron 패턴 변환", () => {
|
|
16
|
-
const crons = cronJobs();
|
|
17
|
-
crons.interval("hourly", { hours: 6 }, "cleanup");
|
|
18
|
-
expect(crons.getJobs()[0].pattern).toBe("0 */6 * * *");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("interval — seconds → 6자리 cron 패턴", () => {
|
|
22
|
-
const crons = cronJobs();
|
|
23
|
-
crons.interval("fast", { seconds: 30 }, "tick");
|
|
24
|
-
expect(crons.getJobs()[0].pattern).toBe("*/30 * * * * *");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("daily — 특정 시각", () => {
|
|
28
|
-
const crons = cronJobs();
|
|
29
|
-
crons.daily("report", { hour: 9, minute: 30 }, "reports.gen");
|
|
30
|
-
const job = crons.getJobs()[0];
|
|
31
|
-
expect(job.pattern).toBe("30 9 * * *");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("daily — minute 기본값 0", () => {
|
|
35
|
-
const crons = cronJobs();
|
|
36
|
-
crons.daily("cleanup", { hour: 2 }, "admin.cleanup");
|
|
37
|
-
expect(crons.getJobs()[0].pattern).toBe("0 2 * * *");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("weekly — 요일 + 시각", () => {
|
|
41
|
-
const crons = cronJobs();
|
|
42
|
-
crons.weekly("monday-report", { dayOfWeek: 1, hour: 9 }, "reports.weekly");
|
|
43
|
-
expect(crons.getJobs()[0].pattern).toBe("0 9 * * 1");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("cron — 직접 패턴 지정", () => {
|
|
47
|
-
const crons = cronJobs();
|
|
48
|
-
crons.cron("custom", "0 */2 * * *", "custom.handler");
|
|
49
|
-
expect(crons.getJobs()[0].pattern).toBe("0 */2 * * *");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("체이닝 지원", () => {
|
|
53
|
-
const crons = cronJobs();
|
|
54
|
-
crons
|
|
55
|
-
.interval("a", { minutes: 5 }, "a.action")
|
|
56
|
-
.daily("b", { hour: 3 }, "b.action")
|
|
57
|
-
.cron("c", "0 * * * *", "c.action");
|
|
58
|
-
expect(crons.getJobs()).toHaveLength(3);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("인라인 핸들러 지원", () => {
|
|
62
|
-
const crons = cronJobs();
|
|
63
|
-
const fn = async () => {
|
|
64
|
-
/* noop */
|
|
65
|
-
};
|
|
66
|
-
crons.interval("inline", { minutes: 1 }, fn);
|
|
67
|
-
expect(typeof crons.getJobs()[0].action).toBe("function");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("getJobs는 복사본 반환 (원본 보호)", () => {
|
|
71
|
-
const crons = cronJobs();
|
|
72
|
-
crons.interval("test", { minutes: 1 }, "test");
|
|
73
|
-
const jobs1 = crons.getJobs();
|
|
74
|
-
const jobs2 = crons.getJobs();
|
|
75
|
-
expect(jobs1).not.toBe(jobs2); // 서로 다른 참조
|
|
76
|
-
expect(jobs1).toEqual(jobs2); // 내용은 동일
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("interval — 옵션 없으면 에러", () => {
|
|
80
|
-
const crons = cronJobs();
|
|
81
|
-
expect(() => crons.interval("bad", {}, "bad")).toThrow("minutes, hours, 또는 seconds");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* packages/core/src/__tests__/crud-codegen-integration.test.ts
|
|
3
|
-
*
|
|
4
|
-
* crud() → 레지스트리 등록 → codegen 인식 통합 테스트.
|
|
5
|
-
*
|
|
6
|
-
* 이 테스트의 존재 이유:
|
|
7
|
-
* 2026-04-02 사고: crud()가 query/mutation을 레지스트리에 자동 등록하지만,
|
|
8
|
-
* codegen(gencow-extract.ts)이 getRegisteredQueries()/getRegisteredMutations()를
|
|
9
|
-
* 통해 이를 인식하는 전체 파이프라인이 검증되지 않았음.
|
|
10
|
-
* 결과: 사용자 모듈이 crud()를 사용하면 api.ts에 CRUD 엔드포인트가 누락됨.
|
|
11
|
-
*
|
|
12
|
-
* 검증 항목:
|
|
13
|
-
* 1. crud() 호출 후 getRegisteredQueries()에 {prefix}.list, {prefix}.get 포함
|
|
14
|
-
* 2. crud() 호출 후 getRegisteredMutations()에 {prefix}.create/update/remove 포함
|
|
15
|
-
* 3. 수동 query()/mutation()과 crud() 혼용 시 양쪽 모두 레지스트리에 존재
|
|
16
|
-
* 4. public/private 상태가 레지스트리에 정확히 반영
|
|
17
|
-
* 5. 여러 테이블의 crud()를 동시 호출해도 충돌 없음
|
|
18
|
-
*
|
|
19
|
-
* Run: bun test packages/core/src/__tests__/crud-codegen-integration.test.ts
|
|
20
|
-
*
|
|
21
|
-
* @see docs/analysis/analysis-test032-gencow-crud-api-mismatch.md
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { describe, it, expect, beforeAll } from "bun:test";
|
|
25
|
-
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
26
|
-
|
|
27
|
-
import { crud } from "../crud.js";
|
|
28
|
-
import { query, mutation, getRegisteredQueries, getRegisteredMutations, getQueryDef } from "../reactive.js";
|
|
29
|
-
|
|
30
|
-
// ─── 테스트용 테이블 정의 ─────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const keywords = pgTable("cg_keywords", {
|
|
33
|
-
id: serial("id").primaryKey(),
|
|
34
|
-
keyword: text("keyword").notNull(),
|
|
35
|
-
userId: text("user_id"),
|
|
36
|
-
createdAt: timestamp("created_at").defaultNow(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const crawlLogs = pgTable("cg_crawl_logs", {
|
|
40
|
-
id: serial("id").primaryKey(),
|
|
41
|
-
status: text("status"),
|
|
42
|
-
createdAt: timestamp("created_at").defaultNow(),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const digests = pgTable("cg_digests", {
|
|
46
|
-
id: serial("id").primaryKey(),
|
|
47
|
-
title: text("title"),
|
|
48
|
-
content: text("content"),
|
|
49
|
-
createdAt: timestamp("created_at").defaultNow(),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const appSettings = pgTable("cg_app_settings", {
|
|
53
|
-
id: serial("id").primaryKey(),
|
|
54
|
-
key: text("key"),
|
|
55
|
-
value: text("value"),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
59
|
-
// 1. crud() → 레지스트리 등록 → codegen 인식 전체 파이프라인
|
|
60
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
61
|
-
|
|
62
|
-
describe("crud() → codegen 통합 — 다중 테이블", () => {
|
|
63
|
-
beforeAll(() => {
|
|
64
|
-
// test032 패턴 재현: 4개 모듈이 각각 crud() 호출
|
|
65
|
-
crud(keywords, { public: true });
|
|
66
|
-
crud(crawlLogs, { public: true });
|
|
67
|
-
crud(digests, { public: true });
|
|
68
|
-
crud(appSettings, { public: true });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("4개 테이블의 list/get query가 모두 레지스트리에 등록된다", () => {
|
|
72
|
-
const queries = getRegisteredQueries();
|
|
73
|
-
|
|
74
|
-
// 각 테이블의 list + get = 8개
|
|
75
|
-
expect(queries).toContain("cg_keywords.list");
|
|
76
|
-
expect(queries).toContain("cg_keywords.get");
|
|
77
|
-
expect(queries).toContain("cg_crawl_logs.list");
|
|
78
|
-
expect(queries).toContain("cg_crawl_logs.get");
|
|
79
|
-
expect(queries).toContain("cg_digests.list");
|
|
80
|
-
expect(queries).toContain("cg_digests.get");
|
|
81
|
-
expect(queries).toContain("cg_app_settings.list");
|
|
82
|
-
expect(queries).toContain("cg_app_settings.get");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("4개 테이블의 create/update/remove mutation이 모두 레지스트리에 등록된다", () => {
|
|
86
|
-
const mutations = getRegisteredMutations();
|
|
87
|
-
const names = mutations.map((m) => m.name);
|
|
88
|
-
|
|
89
|
-
// 각 테이블의 create + update + remove = 12개
|
|
90
|
-
expect(names).toContain("cg_keywords.create");
|
|
91
|
-
expect(names).toContain("cg_keywords.update");
|
|
92
|
-
expect(names).toContain("cg_keywords.remove");
|
|
93
|
-
expect(names).toContain("cg_crawl_logs.create");
|
|
94
|
-
expect(names).toContain("cg_crawl_logs.update");
|
|
95
|
-
expect(names).toContain("cg_crawl_logs.remove");
|
|
96
|
-
expect(names).toContain("cg_digests.create");
|
|
97
|
-
expect(names).toContain("cg_digests.update");
|
|
98
|
-
expect(names).toContain("cg_digests.remove");
|
|
99
|
-
expect(names).toContain("cg_app_settings.create");
|
|
100
|
-
expect(names).toContain("cg_app_settings.update");
|
|
101
|
-
expect(names).toContain("cg_app_settings.remove");
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
106
|
-
// 2. crud() + 수동 query/mutation 혼용 — 양쪽 모두 codegen에 포함
|
|
107
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
108
|
-
|
|
109
|
-
describe("crud() + 수동 query/mutation 혼용", () => {
|
|
110
|
-
beforeAll(() => {
|
|
111
|
-
// crud로 자동 등록
|
|
112
|
-
const articlesTable = pgTable("cg_articles", {
|
|
113
|
-
id: serial("id").primaryKey(),
|
|
114
|
-
title: text("title"),
|
|
115
|
-
createdAt: timestamp("created_at").defaultNow(),
|
|
116
|
-
});
|
|
117
|
-
crud(articlesTable, { public: true });
|
|
118
|
-
|
|
119
|
-
// 수동으로 추가 등록 (test032의 digestsModule.generate 패턴)
|
|
120
|
-
query("cg_digests.latest", {
|
|
121
|
-
public: true,
|
|
122
|
-
handler: async (ctx) => {
|
|
123
|
-
return { id: 1, title: "latest" };
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
mutation("cg_digests.generate", {
|
|
128
|
-
public: true,
|
|
129
|
-
handler: async (ctx) => {
|
|
130
|
-
return { success: true };
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("crud 등록 + 수동 등록이 모두 getRegisteredQueries()에 포함된다", () => {
|
|
136
|
-
const queries = getRegisteredQueries();
|
|
137
|
-
|
|
138
|
-
// crud 자동 등록
|
|
139
|
-
expect(queries).toContain("cg_articles.list");
|
|
140
|
-
expect(queries).toContain("cg_articles.get");
|
|
141
|
-
|
|
142
|
-
// 수동 등록
|
|
143
|
-
expect(queries).toContain("cg_digests.latest");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("crud 등록 + 수동 등록이 모두 getRegisteredMutations()에 포함된다", () => {
|
|
147
|
-
const mutations = getRegisteredMutations();
|
|
148
|
-
const names = mutations.map((m) => m.name);
|
|
149
|
-
|
|
150
|
-
// crud 자동 등록
|
|
151
|
-
expect(names).toContain("cg_articles.create");
|
|
152
|
-
expect(names).toContain("cg_articles.update");
|
|
153
|
-
expect(names).toContain("cg_articles.remove");
|
|
154
|
-
|
|
155
|
-
// 수동 등록
|
|
156
|
-
expect(names).toContain("cg_digests.generate");
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
-
// 3. public/private 상태 정확성 — codegen이 isPublic 기반으로 auth 분기
|
|
162
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
163
|
-
|
|
164
|
-
describe("crud() isPublic 상태 — codegen auth 분기 정확성", () => {
|
|
165
|
-
beforeAll(() => {
|
|
166
|
-
const publicTable = pgTable("cg_public_data", {
|
|
167
|
-
id: serial("id").primaryKey(),
|
|
168
|
-
name: text("name"),
|
|
169
|
-
});
|
|
170
|
-
const privateTable = pgTable("cg_private_data", {
|
|
171
|
-
id: serial("id").primaryKey(),
|
|
172
|
-
name: text("name"),
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
crud(publicTable, { public: true });
|
|
176
|
-
crud(privateTable); // default: auth 필수
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("public: true 테이블의 모든 엔드포인트가 isPublic === true", () => {
|
|
180
|
-
const listDef = getQueryDef("cg_public_data.list");
|
|
181
|
-
const getDef = getQueryDef("cg_public_data.get");
|
|
182
|
-
|
|
183
|
-
expect(listDef!.isPublic).toBe(true);
|
|
184
|
-
expect(getDef!.isPublic).toBe(true);
|
|
185
|
-
|
|
186
|
-
const mutations = getRegisteredMutations();
|
|
187
|
-
const createDef = mutations.find((m) => m.name === "cg_public_data.create");
|
|
188
|
-
expect(createDef!.isPublic).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("기본(private) 테이블의 모든 엔드포인트가 isPublic === false", () => {
|
|
192
|
-
const listDef = getQueryDef("cg_private_data.list");
|
|
193
|
-
const getDef = getQueryDef("cg_private_data.get");
|
|
194
|
-
|
|
195
|
-
expect(listDef!.isPublic).toBe(false);
|
|
196
|
-
expect(getDef!.isPublic).toBe(false);
|
|
197
|
-
|
|
198
|
-
const mutations = getRegisteredMutations();
|
|
199
|
-
const createDef = mutations.find((m) => m.name === "cg_private_data.create");
|
|
200
|
-
expect(createDef!.isPublic).toBe(false);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
205
|
-
// 4. codegen 시뮬레이션 — getRegisteredQueries/Mutations로 api.ts 생성
|
|
206
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
207
|
-
|
|
208
|
-
describe("codegen 시뮬레이션 — api.ts 생성 가능 여부", () => {
|
|
209
|
-
it("getRegisteredQueries()로 모든 query key를 열거할 수 있다", () => {
|
|
210
|
-
const queries = getRegisteredQueries();
|
|
211
|
-
expect(queries.length).toBeGreaterThan(0);
|
|
212
|
-
|
|
213
|
-
// 모든 query key가 "namespace.action" 패턴이어야 함
|
|
214
|
-
for (const key of queries) {
|
|
215
|
-
expect(key).toMatch(/^[a-z_]+\.[a-z_]+$/i);
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("getRegisteredMutations()로 모든 mutation을 열거할 수 있다", () => {
|
|
220
|
-
const mutations = getRegisteredMutations();
|
|
221
|
-
expect(mutations.length).toBeGreaterThan(0);
|
|
222
|
-
|
|
223
|
-
// 모든 mutation이 name + handler를 가져야 함
|
|
224
|
-
for (const mut of mutations) {
|
|
225
|
-
expect(mut.name).toBeTruthy();
|
|
226
|
-
expect(typeof mut.handler).toBe("function");
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it("getQueryDef()로 개별 query의 argsSchema에 접근할 수 있다", () => {
|
|
231
|
-
const listDef = getQueryDef("cg_keywords.list");
|
|
232
|
-
expect(listDef).toBeDefined();
|
|
233
|
-
// list handler에는 argsSchema가 있음 (limit, offset, sort, filters)
|
|
234
|
-
expect(listDef!.handler).toBeDefined();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("codegen 대상 query/mutation 총 개수가 올바르다", () => {
|
|
238
|
-
const queries = getRegisteredQueries();
|
|
239
|
-
const mutations = getRegisteredMutations();
|
|
240
|
-
|
|
241
|
-
// 최소 8개 query (4테이블 × list+get) + 수동 1개 + 이전 테스트들
|
|
242
|
-
expect(queries.length).toBeGreaterThanOrEqual(8);
|
|
243
|
-
// 최소 12개 mutation (4테이블 × create+update+remove) + 수동 1개
|
|
244
|
-
expect(mutations.length).toBeGreaterThanOrEqual(12);
|
|
245
|
-
});
|
|
246
|
-
});
|