@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.
Files changed (83) hide show
  1. package/dist/document-types.d.ts +65 -0
  2. package/dist/document-types.js +15 -0
  3. package/dist/grounded-answer-types.d.ts +62 -0
  4. package/dist/grounded-answer-types.js +6 -0
  5. package/dist/index.d.ts +10 -1
  6. package/dist/index.js +4 -0
  7. package/dist/rag-ingest-types.d.ts +39 -0
  8. package/dist/rag-ingest-types.js +1 -0
  9. package/dist/rag-operations-types.d.ts +81 -0
  10. package/dist/rag-operations-types.js +1 -0
  11. package/dist/rag-schema.d.ts +1557 -0
  12. package/dist/rag-schema.js +87 -0
  13. package/dist/reactive.d.ts +13 -0
  14. package/dist/rls-db.d.ts +9 -2
  15. package/dist/runtime-env-policy.d.ts +5 -0
  16. package/dist/runtime-env-policy.js +56 -0
  17. package/dist/search-types.d.ts +83 -0
  18. package/dist/search-types.js +1 -0
  19. package/dist/server.d.ts +1 -2
  20. package/dist/server.js +0 -1
  21. package/dist/storage-shared.d.ts +36 -0
  22. package/dist/storage-shared.js +39 -0
  23. package/dist/storage.d.ts +2 -26
  24. package/dist/storage.js +19 -15
  25. package/dist/workflow-types.d.ts +3 -1
  26. package/package.json +8 -7
  27. package/src/document-types.ts +95 -0
  28. package/src/grounded-answer-types.ts +78 -0
  29. package/src/index.ts +66 -1
  30. package/src/rag-ingest-types.ts +52 -0
  31. package/src/rag-operations-types.ts +90 -0
  32. package/src/rag-schema.ts +94 -0
  33. package/src/reactive.ts +13 -0
  34. package/src/rls-db.ts +9 -4
  35. package/src/runtime-env-policy.ts +66 -0
  36. package/src/search-types.ts +91 -0
  37. package/src/server.ts +1 -2
  38. package/src/storage-shared.ts +74 -0
  39. package/src/storage.ts +29 -46
  40. package/src/workflow-types.ts +3 -1
  41. package/src/__tests__/auth.test.ts +0 -118
  42. package/src/__tests__/crons.test.ts +0 -83
  43. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  44. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  45. package/src/__tests__/crud.test.ts +0 -930
  46. package/src/__tests__/dist-exports.test.ts +0 -176
  47. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  48. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  49. package/src/__tests__/fixtures/basic/index.ts +0 -6
  50. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  51. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  52. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  53. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  54. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  55. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  56. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  57. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  58. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  59. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  60. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  61. package/src/__tests__/httpaction.test.ts +0 -122
  62. package/src/__tests__/image-optimization.test.ts +0 -648
  63. package/src/__tests__/load.test.ts +0 -389
  64. package/src/__tests__/network-sim.test.ts +0 -319
  65. package/src/__tests__/reactive.test.ts +0 -479
  66. package/src/__tests__/retry.test.ts +0 -113
  67. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  68. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  69. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  70. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  71. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  72. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  73. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  74. package/src/__tests__/scheduler-durable.test.ts +0 -173
  75. package/src/__tests__/scheduler-exec.test.ts +0 -328
  76. package/src/__tests__/scheduler.test.ts +0 -187
  77. package/src/__tests__/storage.test.ts +0 -334
  78. package/src/__tests__/tsconfig.json +0 -8
  79. package/src/__tests__/validator.test.ts +0 -323
  80. package/src/__tests__/workflow.test.ts +0 -606
  81. package/src/__tests__/ws-integration.test.ts +0 -309
  82. package/src/__tests__/ws-scale.test.ts +0 -241
  83. package/src/auth.ts +0 -155
@@ -1,334 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/storage.test.ts
3
- *
4
- * Tests for createStorage() — file storage API
5
- * Uses temp directory for file I/O.
6
- *
7
- * Run: bun test packages/core/src/__tests__/storage.test.ts
8
- */
9
-
10
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
11
- import { createStorage } from "../storage.js";
12
- import * as fs from "fs/promises";
13
- import * as path from "path";
14
- import * as os from "os";
15
-
16
- describe("createStorage()", () => {
17
- let tmpDir: string;
18
-
19
- beforeEach(async () => {
20
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-storage-test-"));
21
- });
22
-
23
- afterEach(async () => {
24
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
25
- });
26
-
27
- // ─── store + getUrl ─────────────────────────────────
28
-
29
- describe("store()", () => {
30
- it("File 객체를 저장하고 storageId를 반환한다", async () => {
31
- const storage = createStorage(tmpDir);
32
- const file = new File(["hello world"], "test.txt", { type: "text/plain" });
33
-
34
- const id = await storage.store(file);
35
- expect(typeof id).toBe("string");
36
- expect(id.length).toBeGreaterThan(0);
37
- });
38
-
39
- it("저장된 파일이 디스크에 존재한다", async () => {
40
- const storage = createStorage(tmpDir);
41
- const file = new File(["content"], "doc.txt", { type: "text/plain" });
42
-
43
- const id = await storage.store(file);
44
- const filePath = path.join(tmpDir, id);
45
- const stat = await fs.stat(filePath);
46
- expect(stat.isFile()).toBe(true);
47
- });
48
-
49
- it("Blob 객체도 저장 가능하다", async () => {
50
- const storage = createStorage(tmpDir);
51
- const blob = new Blob(["blob content"], { type: "application/octet-stream" });
52
-
53
- const id = await storage.store(blob);
54
- expect(typeof id).toBe("string");
55
- });
56
-
57
- it("50MB 초과 파일 → 에러", async () => {
58
- const storage = createStorage(tmpDir);
59
- // 51MB File mock — File constructor에서 size를 직접 제어
60
- const bigContent = new Uint8Array(51 * 1024 * 1024);
61
- const file = new File([bigContent], "big.bin");
62
-
63
- await expect(storage.store(file)).rejects.toThrow("File too large");
64
- });
65
-
66
- it("커스텀 filename 적용", async () => {
67
- const storage = createStorage(tmpDir);
68
- const file = new File(["data"], "original.txt");
69
-
70
- const id = await storage.store(file, "custom-name.txt");
71
- const meta = await storage.getMeta(id);
72
- expect(meta?.name).toBe("custom-name.txt");
73
- });
74
- });
75
-
76
- // ─── storeBuffer ────────────────────────────────────
77
-
78
- describe("storeBuffer()", () => {
79
- it("Buffer를 저장하고 storageId를 반환한다", async () => {
80
- const storage = createStorage(tmpDir);
81
- const buffer = Buffer.from("buffer content");
82
-
83
- const id = await storage.storeBuffer(buffer, "buffer.txt", "text/plain");
84
- expect(typeof id).toBe("string");
85
- });
86
-
87
- it("저장된 파일의 메타데이터가 올바르다", async () => {
88
- const storage = createStorage(tmpDir);
89
- const buffer = Buffer.from("meta test");
90
-
91
- const id = await storage.storeBuffer(buffer, "meta.txt", "text/plain");
92
- const meta = await storage.getMeta(id);
93
- expect(meta).not.toBeNull();
94
- expect(meta!.name).toBe("meta.txt");
95
- expect(meta!.type).toBe("text/plain");
96
- expect(meta!.size).toBe(buffer.length);
97
- });
98
- });
99
-
100
- // ─── getUrl ─────────────────────────────────────────
101
-
102
- describe("getUrl()", () => {
103
- it("storageId로 URL 반환", () => {
104
- const storage = createStorage(tmpDir);
105
- const url = storage.getUrl("some-uuid-123");
106
- expect(url).toBe("/api/storage/some-uuid-123");
107
- });
108
-
109
- it("URL 패턴이 /api/storage/{id} 형식이다", async () => {
110
- const storage = createStorage(tmpDir);
111
- const file = new File(["test"], "test.txt");
112
- const id = await storage.store(file);
113
- const url = storage.getUrl(id);
114
- expect(url).toMatch(/^\/api\/storage\/.+$/);
115
- });
116
- });
117
-
118
- // ─── getMeta ────────────────────────────────────────
119
-
120
- describe("getMeta()", () => {
121
- it("저장된 파일의 메타데이터를 반환한다", async () => {
122
- const storage = createStorage(tmpDir);
123
- const file = new File(["hello"], "hello.txt", { type: "text/plain" });
124
- const id = await storage.store(file);
125
-
126
- const meta = await storage.getMeta(id);
127
- expect(meta).not.toBeNull();
128
- expect(meta!.id).toBe(id);
129
- expect(meta!.name).toBe("hello.txt");
130
- expect(meta!.type).toContain("text/plain");
131
- });
132
-
133
- it("존재하지 않는 storageId → null 반환", async () => {
134
- const storage = createStorage(tmpDir);
135
- const meta = await storage.getMeta("nonexistent-id");
136
- expect(meta).toBeNull();
137
- });
138
- });
139
-
140
- // ─── delete ─────────────────────────────────────────
141
-
142
- describe("delete()", () => {
143
- it("파일을 삭제한다", async () => {
144
- const storage = createStorage(tmpDir);
145
- const file = new File(["to delete"], "delete-me.txt");
146
- const id = await storage.store(file);
147
-
148
- // 파일 존재 확인
149
- const filePath = path.join(tmpDir, id);
150
- const statBefore = await fs.stat(filePath);
151
- expect(statBefore.isFile()).toBe(true);
152
-
153
- // 삭제
154
- await storage.delete(id);
155
-
156
- // 파일 삭제 확인
157
- const exists = await fs
158
- .access(filePath)
159
- .then(() => true)
160
- .catch(() => false);
161
- expect(exists).toBe(false);
162
-
163
- // 메타데이터 삭제 확인
164
- const meta = await storage.getMeta(id);
165
- expect(meta).toBeNull();
166
- });
167
-
168
- it("존재하지 않는 storageId 삭제 시 에러 없음", async () => {
169
- const storage = createStorage(tmpDir);
170
- await expect(storage.delete("nonexistent-id")).resolves.toBeUndefined();
171
- });
172
- });
173
-
174
- // ─── Storage Quota ──────────────────────────────────
175
-
176
- describe("스토리지 쿼터", () => {
177
- it("쿼터 초과 시 에러 (rawSql 있을 때)", async () => {
178
- const mockRawSql = async (sql: string, _params?: unknown[]) => {
179
- if (sql.includes("information_schema")) return []; // ensureFilesTable
180
- if (sql.includes("SUM")) return [{ total: "999999999" }]; // 쿼터에 근접
181
- return [];
182
- };
183
-
184
- const storage = createStorage(tmpDir, {
185
- rawSql: mockRawSql,
186
- storageQuota: 1000000000, // 1GB
187
- });
188
-
189
- const file = new File(["x".repeat(1024)], "test.txt");
190
- await expect(storage.store(file)).rejects.toThrow("Storage quota exceeded");
191
- });
192
-
193
- it("쿼터 0 = 무제한 (에러 없음)", async () => {
194
- const mockRawSql = async (sql: string, _params?: unknown[]) => {
195
- if (sql.includes("information_schema")) return [];
196
- if (sql.includes("SUM")) return [{ total: "999999999999" }];
197
- if (sql.includes("INSERT")) return [];
198
- return [];
199
- };
200
-
201
- const storage = createStorage(tmpDir, {
202
- rawSql: mockRawSql,
203
- storageQuota: 0, // 무제한
204
- });
205
-
206
- const file = new File(["small"], "small.txt");
207
- const id = await storage.store(file);
208
- expect(typeof id).toBe("string");
209
- });
210
- });
211
- });
212
-
213
- // ─── _system_files 테이블 리네이밍 관련 테스트 ────────────
214
- // 2026-04-10 WSOD 사고 후 추가: files → _system_files 리네이밍 검증
215
- // 📄 참고: docs/analysis/analysis-files-page-wsod.md
216
-
217
- describe("_system_files 시스템 테이블 네이밍", () => {
218
- it("rawSql로 실행되는 모든 SQL에 old 'files' 테이블 참조가 없다", async () => {
219
- const executedSql: string[] = [];
220
- const mockRawSql = async (sql: string) => {
221
- executedSql.push(sql);
222
- if (sql.includes("SUM")) return [{ total: "0" }];
223
- if (sql.includes("INSERT")) return [];
224
- return [];
225
- };
226
-
227
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-naming-"));
228
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
229
-
230
- // store 트리거 (ensureFilesTable + checkQuota + recordFileToDb)
231
- const file = new File(["test"], "test.txt", { type: "text/plain" });
232
- try {
233
- await storage.store(file);
234
- } catch {}
235
-
236
- // 모든 실행된 SQL에서 old 테이블명 'files'가 아닌 '_system_files'만 참조하는지 확인
237
- // (files 단독 참조를 찾되, _system_files는 통과)
238
- for (const sql of executedSql) {
239
- // "FROM files", "INTO files", "TABLE files" 같은 old 패턴이 없어야 함
240
- expect(sql).not.toMatch(/\bFROM\s+files\b(?!_)/i);
241
- expect(sql).not.toMatch(/\bINTO\s+files\b(?!_)/i);
242
- expect(sql).not.toMatch(/\bTABLE\s+files\b(?!_)/i);
243
- }
244
-
245
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
246
- });
247
-
248
- it("쿼터 검증 SQL이 _system_files 테이블을 참조한다", async () => {
249
- const executedSql: string[] = [];
250
- const mockRawSql = async (sql: string) => {
251
- executedSql.push(sql);
252
- if (sql.includes("SUM")) return [{ total: "0" }];
253
- if (sql.includes("INSERT")) return [];
254
- return [];
255
- };
256
-
257
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-quota-"));
258
- const storage = createStorage(tmpDir, {
259
- rawSql: mockRawSql,
260
- storageQuota: 1000000000,
261
- });
262
-
263
- const file = new File(["small"], "small.txt");
264
- try {
265
- await storage.store(file);
266
- } catch {}
267
-
268
- // SUM 쿼리가 _system_files 테이블을 참조하는지 확인
269
- const sumSql = executedSql.find((s) => s.includes("SUM"));
270
- if (sumSql) {
271
- expect(sumSql).toContain("_system_files");
272
- expect(sumSql).not.toMatch(/FROM\s+files[^_]/);
273
- }
274
-
275
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
276
- });
277
-
278
- it("recordFileToDb SQL이 _system_files 테이블에 INSERT한다", async () => {
279
- const executedSql: string[] = [];
280
- const mockRawSql = async (sql: string) => {
281
- executedSql.push(sql);
282
- if (sql.includes("SUM")) return [{ total: "0" }];
283
- return [];
284
- };
285
-
286
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-insert-"));
287
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
288
-
289
- const file = new File(["data"], "data.txt", { type: "text/plain" });
290
- try {
291
- await storage.store(file);
292
- } catch {}
293
-
294
- const insertSql = executedSql.find((s) => s.includes("INSERT"));
295
- if (insertSql) {
296
- expect(insertSql).toContain("_system_files");
297
- expect(insertSql).not.toMatch(/INTO\s+files[^_]/);
298
- }
299
-
300
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
301
- });
302
-
303
- it("delete SQL이 _system_files 테이블에서 삭제한다", async () => {
304
- const executedSql: string[] = [];
305
- const mockRawSql = async (sql: string) => {
306
- executedSql.push(sql);
307
- if (sql.includes("SUM")) return [{ total: "0" }];
308
- return [];
309
- };
310
-
311
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gencow-delete-"));
312
- const storage = createStorage(tmpDir, { rawSql: mockRawSql });
313
-
314
- // store 후 delete
315
- const file = new File(["delete-me"], "del.txt");
316
- let storageId: string | undefined;
317
- try {
318
- storageId = await storage.store(file);
319
- } catch {}
320
- if (storageId) {
321
- try {
322
- await storage.delete(storageId);
323
- } catch {}
324
- }
325
-
326
- const deleteSql = executedSql.find((s) => s.includes("DELETE"));
327
- if (deleteSql) {
328
- expect(deleteSql).toContain("_system_files");
329
- expect(deleteSql).not.toMatch(/FROM\s+files[^_]/);
330
- }
331
-
332
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
333
- });
334
- });
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": true
5
- },
6
- "include": ["./**/*.ts"],
7
- "exclude": []
8
- }
@@ -1,323 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/validator.test.ts
3
- *
4
- * Tests for the `v` validator module — runtime argument validation.
5
- *
6
- * Run: bun test packages/core/src/__tests__/validator.test.ts
7
- */
8
-
9
- import { describe, it, expect } from "bun:test";
10
- import { v, parseArgs, GencowValidationError } from "../v.js";
11
-
12
- // ─── v.string() ─────────────────────────────────────────
13
-
14
- describe("v.string()", () => {
15
- const validator = v.string();
16
-
17
- it("문자열 통과", () => {
18
- expect(validator.parse("hello")).toBe("hello");
19
- });
20
-
21
- it("빈 문자열 통과", () => {
22
- expect(validator.parse("")).toBe("");
23
- });
24
-
25
- it("숫자 → 에러", () => {
26
- expect(() => validator.parse(42)).toThrow("Expected string");
27
- });
28
-
29
- it("불리언 → 에러", () => {
30
- expect(() => validator.parse(true)).toThrow("Expected string");
31
- });
32
-
33
- it("null → 에러", () => {
34
- expect(() => validator.parse(null)).toThrow("Expected string");
35
- });
36
-
37
- it("undefined → 에러", () => {
38
- expect(() => validator.parse(undefined)).toThrow("Expected string");
39
- });
40
- });
41
-
42
- // ─── v.number() ─────────────────────────────────────────
43
-
44
- describe("v.number()", () => {
45
- const validator = v.number();
46
-
47
- it("정수 통과", () => {
48
- expect(validator.parse(42)).toBe(42);
49
- });
50
-
51
- it("실수 통과", () => {
52
- expect(validator.parse(3.14)).toBe(3.14);
53
- });
54
-
55
- it("0 통과", () => {
56
- expect(validator.parse(0)).toBe(0);
57
- });
58
-
59
- it("음수 통과", () => {
60
- expect(validator.parse(-10)).toBe(-10);
61
- });
62
-
63
- it("숫자 문자열 → 자동 변환", () => {
64
- expect(validator.parse("42")).toBe(42);
65
- });
66
-
67
- it("비숫자 문자열 → 에러", () => {
68
- expect(() => validator.parse("abc")).toThrow("Expected number");
69
- });
70
-
71
- it("불리언 → 에러", () => {
72
- expect(() => validator.parse(true)).toThrow("Expected number");
73
- });
74
-
75
- it("NaN 입력 → 에러", () => {
76
- expect(() => validator.parse(NaN)).toThrow("Expected number");
77
- });
78
- });
79
-
80
- // ─── v.boolean() ────────────────────────────────────────
81
-
82
- describe("v.boolean()", () => {
83
- const validator = v.boolean();
84
-
85
- it("true 통과", () => {
86
- expect(validator.parse(true)).toBe(true);
87
- });
88
-
89
- it("false 통과", () => {
90
- expect(validator.parse(false)).toBe(false);
91
- });
92
-
93
- it("문자열 → 에러", () => {
94
- expect(() => validator.parse("true")).toThrow("Expected boolean");
95
- });
96
-
97
- it("숫자 → 에러", () => {
98
- expect(() => validator.parse(1)).toThrow("Expected boolean");
99
- });
100
- });
101
-
102
- // ─── v.any() ────────────────────────────────────────────
103
-
104
- describe("v.any()", () => {
105
- const validator = v.any();
106
-
107
- it("모든 값 통과 — 문자열", () => {
108
- expect(validator.parse("hello")).toBe("hello");
109
- });
110
-
111
- it("모든 값 통과 — 숫자", () => {
112
- expect(validator.parse(42)).toBe(42);
113
- });
114
-
115
- it("모든 값 통과 — null", () => {
116
- expect(validator.parse(null)).toBe(null);
117
- });
118
-
119
- it("모든 값 통과 — undefined", () => {
120
- expect(validator.parse(undefined)).toBe(undefined);
121
- });
122
-
123
- it("모든 값 통과 — 객체", () => {
124
- const obj = { key: "value" };
125
- expect(validator.parse(obj)).toBe(obj);
126
- });
127
- });
128
-
129
- // ─── v.optional() ───────────────────────────────────────
130
-
131
- describe("v.optional()", () => {
132
- const validator = v.optional(v.string());
133
-
134
- it("문자열 통과", () => {
135
- expect(validator.parse("hello")).toBe("hello");
136
- });
137
-
138
- it("undefined → undefined 반환", () => {
139
- expect(validator.parse(undefined)).toBe(undefined);
140
- });
141
-
142
- it("null → undefined 반환", () => {
143
- expect(validator.parse(null)).toBe(undefined);
144
- });
145
-
146
- it("숫자 → 에러 (내부 validator 적용)", () => {
147
- expect(() => validator.parse(42)).toThrow("Expected string");
148
- });
149
- });
150
-
151
- // ─── v.array() ──────────────────────────────────────────
152
-
153
- describe("v.array()", () => {
154
- const validator = v.array(v.string());
155
-
156
- it("문자열 배열 통과", () => {
157
- expect(validator.parse(["a", "b", "c"])).toEqual(["a", "b", "c"]);
158
- });
159
-
160
- it("빈 배열 통과", () => {
161
- expect(validator.parse([])).toEqual([]);
162
- });
163
-
164
- it("비배열 → 에러", () => {
165
- expect(() => validator.parse("not array")).toThrow("Expected array");
166
- });
167
-
168
- it("원소 타입 불일치 → 에러", () => {
169
- expect(() => validator.parse([1, 2, 3])).toThrow("Expected string");
170
- });
171
-
172
- it("혼합 배열 → 에러", () => {
173
- expect(() => validator.parse(["ok", 42])).toThrow("Expected string");
174
- });
175
- });
176
-
177
- // ─── v.object() ─────────────────────────────────────────
178
-
179
- describe("v.object()", () => {
180
- const validator = v.object({
181
- name: v.string(),
182
- age: v.number(),
183
- });
184
-
185
- it("유효한 객체 통과", () => {
186
- expect(validator.parse({ name: "Alice", age: 30 })).toEqual({ name: "Alice", age: 30 });
187
- });
188
-
189
- it("비객체 → 에러", () => {
190
- expect(() => validator.parse("string")).toThrow("Expected object");
191
- });
192
-
193
- it("null → 에러", () => {
194
- expect(() => validator.parse(null)).toThrow("Expected object");
195
- });
196
-
197
- it("필드 타입 불일치 → 에러", () => {
198
- expect(() => validator.parse({ name: 42, age: 30 })).toThrow("Expected string");
199
- });
200
-
201
- it("필드 누락 → inner validator 에러", () => {
202
- expect(() => validator.parse({ name: "Alice" })).toThrow("Expected number");
203
- });
204
- });
205
-
206
- // ─── 복합 중첩 검증 ────────────────────────────────────
207
-
208
- describe("복합 중첩 검증", () => {
209
- it("v.optional(v.array(v.string()))", () => {
210
- const validator = v.optional(v.array(v.string()));
211
- expect(validator.parse(undefined)).toBe(undefined);
212
- expect(validator.parse(["a", "b"])).toEqual(["a", "b"]);
213
- expect(() => validator.parse([1])).toThrow("Expected string");
214
- });
215
-
216
- it("v.array(v.object(...))", () => {
217
- const validator = v.array(
218
- v.object({
219
- id: v.number(),
220
- label: v.string(),
221
- }),
222
- );
223
- expect(
224
- validator.parse([
225
- { id: 1, label: "A" },
226
- { id: 2, label: "B" },
227
- ]),
228
- ).toEqual([
229
- { id: 1, label: "A" },
230
- { id: 2, label: "B" },
231
- ]);
232
- });
233
-
234
- it("v.object({ tags: v.array(v.string()), meta: v.optional(v.object(...)) })", () => {
235
- const validator = v.object({
236
- tags: v.array(v.string()),
237
- meta: v.optional(v.object({ key: v.string() })),
238
- });
239
- expect(validator.parse({ tags: ["a"], meta: undefined })).toEqual({ tags: ["a"], meta: undefined });
240
- expect(validator.parse({ tags: [], meta: { key: "val" } })).toEqual({ tags: [], meta: { key: "val" } });
241
- });
242
- });
243
-
244
- // ─── parseArgs() ────────────────────────────────────────
245
-
246
- describe("parseArgs()", () => {
247
- it("schema 없으면 args 그대로 반환", () => {
248
- expect(parseArgs(null, { foo: 1 })).toEqual({ foo: 1 });
249
- expect(parseArgs(undefined, "hello")).toBe("hello");
250
- });
251
-
252
- it("Validator 스키마로 직접 파싱", () => {
253
- const schema = v.string();
254
- expect(parseArgs(schema, "hello")).toBe("hello");
255
- });
256
-
257
- it("Validator 스키마 검증 실패 → GencowValidationError", () => {
258
- const schema = v.string();
259
- expect(() => parseArgs(schema, 42)).toThrow(GencowValidationError);
260
- });
261
-
262
- it("Shorthand record 스키마 (Convex 스타일)", () => {
263
- const schema = { title: v.string(), count: v.number() };
264
- expect(parseArgs(schema, { title: "Hello", count: 5 })).toEqual({ title: "Hello", count: 5 });
265
- });
266
-
267
- it("Shorthand record 검증 실패 → GencowValidationError + 필드명 포함", () => {
268
- const schema = { title: v.string(), count: v.number() };
269
- try {
270
- parseArgs(schema, { title: 42, count: 5 });
271
- expect(true).toBe(false); // 여기 도달하면 안 됨
272
- } catch (e) {
273
- expect(e).toBeInstanceOf(GencowValidationError);
274
- expect((e as Error).message).toContain("title");
275
- }
276
- });
277
-
278
- it("Shorthand record에 비객체 전달 → GencowValidationError", () => {
279
- const schema = { title: v.string() };
280
- expect(() => parseArgs(schema, "not an object")).toThrow(GencowValidationError);
281
- });
282
-
283
- it("GencowValidationError.statusCode === 400", () => {
284
- const err = new GencowValidationError("test");
285
- expect(err.statusCode).toBe(400);
286
- expect(err.name).toBe("GencowValidationError");
287
- });
288
-
289
- // ─── 빈 스키마 passthrough (FormData 업로드 버그 회귀 방지) ────
290
-
291
- it("빈 스키마 {} → args 전체 passthrough (FormData file 필드 보존)", () => {
292
- const schema = {};
293
- const args = { file: new File(["hello"], "test.txt"), _mutation: "upload.store" };
294
- const result = parseArgs(schema, args);
295
- // 빈 스키마이므로 args가 그대로 반환되어야 함 (file 포함)
296
- expect(result).toBe(args); // 참조 동일
297
- expect(result.file).toBeInstanceOf(File);
298
- expect(result._mutation).toBe("upload.store");
299
- });
300
-
301
- it("빈 스키마 {} + 일반 객체 → passthrough", () => {
302
- const schema = {};
303
- const args = { name: "test", count: 42, nested: { a: 1 } };
304
- const result = parseArgs(schema, args);
305
- expect(result).toBe(args);
306
- expect(result.name).toBe("test");
307
- expect(result.count).toBe(42);
308
- });
309
-
310
- it("빈 스키마 {} + 빈 args {} → 빈 객체 반환", () => {
311
- const result = parseArgs({}, {});
312
- expect(result).toEqual({});
313
- });
314
-
315
- it("키가 있는 스키마는 여전히 지정된 키만 추출 (file 제거됨)", () => {
316
- const schema = { title: v.string() };
317
- const args = { title: "hello", file: "should-be-stripped", extra: 123 };
318
- const result = parseArgs(schema, args);
319
- expect(result).toEqual({ title: "hello" });
320
- expect(result.file).toBeUndefined();
321
- expect(result.extra).toBeUndefined();
322
- });
323
- });