@gencow/core 0.1.26 → 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 (88) hide show
  1. package/dist/crud.d.ts +12 -0
  2. package/dist/crud.js +16 -0
  3. package/dist/db.d.ts +13 -0
  4. package/dist/db.js +16 -0
  5. package/dist/document-types.d.ts +65 -0
  6. package/dist/document-types.js +15 -0
  7. package/dist/grounded-answer-types.d.ts +62 -0
  8. package/dist/grounded-answer-types.js +6 -0
  9. package/dist/index.d.ts +12 -2
  10. package/dist/index.js +5 -1
  11. package/dist/rag-ingest-types.d.ts +39 -0
  12. package/dist/rag-ingest-types.js +1 -0
  13. package/dist/rag-operations-types.d.ts +81 -0
  14. package/dist/rag-operations-types.js +1 -0
  15. package/dist/rag-schema.d.ts +1557 -0
  16. package/dist/rag-schema.js +87 -0
  17. package/dist/reactive.d.ts +13 -0
  18. package/dist/rls-db.d.ts +9 -2
  19. package/dist/runtime-env-policy.d.ts +5 -0
  20. package/dist/runtime-env-policy.js +56 -0
  21. package/dist/search-types.d.ts +83 -0
  22. package/dist/search-types.js +1 -0
  23. package/dist/server.d.ts +1 -2
  24. package/dist/server.js +0 -1
  25. package/dist/storage-shared.d.ts +36 -0
  26. package/dist/storage-shared.js +39 -0
  27. package/dist/storage.d.ts +2 -26
  28. package/dist/storage.js +19 -15
  29. package/dist/workflow-types.d.ts +3 -1
  30. package/package.json +1 -1
  31. package/src/crud.ts +33 -0
  32. package/src/document-types.ts +95 -0
  33. package/src/grounded-answer-types.ts +78 -0
  34. package/src/index.ts +68 -2
  35. package/src/rag-ingest-types.ts +52 -0
  36. package/src/rag-operations-types.ts +90 -0
  37. package/src/rag-schema.ts +94 -0
  38. package/src/reactive.ts +13 -0
  39. package/src/rls-db.ts +9 -4
  40. package/src/runtime-env-policy.ts +66 -0
  41. package/src/search-types.ts +91 -0
  42. package/src/server.ts +1 -2
  43. package/src/storage-shared.ts +74 -0
  44. package/src/storage.ts +29 -46
  45. package/src/workflow-types.ts +3 -1
  46. package/src/__tests__/auth.test.ts +0 -118
  47. package/src/__tests__/crons.test.ts +0 -83
  48. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  49. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  50. package/src/__tests__/crud.test.ts +0 -930
  51. package/src/__tests__/dist-exports.test.ts +0 -176
  52. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  53. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  54. package/src/__tests__/fixtures/basic/index.ts +0 -6
  55. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  56. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  57. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  58. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  59. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  60. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  61. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  62. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  63. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  64. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  65. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  66. package/src/__tests__/httpaction.test.ts +0 -122
  67. package/src/__tests__/image-optimization.test.ts +0 -648
  68. package/src/__tests__/load.test.ts +0 -389
  69. package/src/__tests__/network-sim.test.ts +0 -319
  70. package/src/__tests__/reactive.test.ts +0 -479
  71. package/src/__tests__/retry.test.ts +0 -113
  72. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  73. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  74. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  75. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  76. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  77. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  78. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  79. package/src/__tests__/scheduler-durable.test.ts +0 -173
  80. package/src/__tests__/scheduler-exec.test.ts +0 -328
  81. package/src/__tests__/scheduler.test.ts +0 -187
  82. package/src/__tests__/storage.test.ts +0 -334
  83. package/src/__tests__/tsconfig.json +0 -8
  84. package/src/__tests__/validator.test.ts +0 -323
  85. package/src/__tests__/workflow.test.ts +0 -606
  86. package/src/__tests__/ws-integration.test.ts +0 -309
  87. package/src/__tests__/ws-scale.test.ts +0 -241
  88. 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
- });