@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,387 +0,0 @@
1
- /**
2
- * packages/core/src/__tests__/crud-owner-rls.test.ts
3
- *
4
- * ownerRls + crud() 통합 테스트 — 2-Layer 방어 Layer 1 검증
5
- *
6
- * 검증 항목:
7
- * 1. 격리: 타 사용자 데이터 접근 차단 (list, get, update, remove)
8
- * 2. 자동주입: create 시 userId 강제 설정 (사용자 입력 무시)
9
- * 3. read: "public": SELECT 필터 생략 + CUD 소유자 강제
10
- * 4. 하위호환: ownerRls 미적용 테이블은 기존 동작 100% 유지
11
- *
12
- * Run: bun test packages/core/src/__tests__/crud-owner-rls.test.ts
13
- */
14
-
15
- import { describe, it, expect } from "bun:test";
16
- import { pgTable, serial, text, timestamp, pgPolicy } from "drizzle-orm/pg-core";
17
- import { sql } from "drizzle-orm";
18
- import { crud } from "../crud.js";
19
- import { ownerRls, getOwnerRlsMeta, registerOwnerRls } from "../rls.js";
20
- import { getQueryDef, getRegisteredMutations } from "../reactive.js";
21
-
22
- // ─── 테스트 테이블 정의 ────────────────────────────────────────────────────
23
-
24
- // ownerRls 적용 테이블
25
- const rlsTasks = pgTable(
26
- "rls_tasks",
27
- {
28
- id: serial("id").primaryKey(),
29
- title: text("title").notNull(),
30
- userId: text("user_id").notNull(),
31
- createdAt: timestamp("created_at").defaultNow(),
32
- updatedAt: timestamp("updated_at").defaultNow(),
33
- },
34
- (t) => ownerRls(t.userId),
35
- );
36
-
37
- // ownerRls(read: "public") 테이블
38
- const rlsPosts = pgTable(
39
- "rls_posts",
40
- {
41
- id: serial("id").primaryKey(),
42
- content: text("content").notNull(),
43
- userId: text("user_id").notNull(),
44
- createdAt: timestamp("created_at").defaultNow(),
45
- },
46
- (t) => ownerRls(t.userId, { read: "public" }),
47
- );
48
-
49
- // ownerRls 미적용 테이블 (하위호환 검증)
50
- const plainItems = pgTable("plain_items", {
51
- id: serial("id").primaryKey(),
52
- name: text("name"),
53
- userId: text("user_id"),
54
- createdAt: timestamp("created_at").defaultNow(),
55
- });
56
-
57
- // ─── Mock 유틸 ─────────────────────────────────────────────────────────
58
-
59
- function createMockCtx(userId: string, mockData: any[] = []) {
60
- const dataChain: any = {
61
- from: () => dataChain,
62
- where: () => dataChain,
63
- orderBy: () => dataChain,
64
- limit: (n?: number) =>
65
- n === 1
66
- ? Promise.resolve(mockData.slice(0, 1)) // get handler: .limit(1) → 단일 행 반환
67
- : dataChain, // list handler: .limit(n).offset(m)
68
- offset: () => Promise.resolve(mockData),
69
- };
70
- const countChain = {
71
- from: () => countChain,
72
- where: () => Promise.resolve([{ count: mockData.length }]),
73
- };
74
-
75
- let capturedValues: any = null;
76
- let capturedWhereArg: any = null;
77
- const emitted: { key: string; data: any }[] = [];
78
-
79
- return {
80
- ctx: {
81
- auth: {
82
- requireAuth: () => ({ id: userId, email: `${userId}@test.com` }),
83
- getUserIdentity: () => ({ id: userId }),
84
- getSession: () => null,
85
- },
86
- db: {
87
- select: (selectArg?: any) => {
88
- if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
89
- return countChain;
90
- }
91
- return dataChain;
92
- },
93
- insert: () => ({
94
- values: (v: any) => {
95
- capturedValues = v;
96
- return { returning: () => Promise.resolve([{ ...v, id: 1 }]) };
97
- },
98
- }),
99
- update: () => ({
100
- set: (data: any) => ({
101
- where: (w: any) => {
102
- capturedWhereArg = w;
103
- return { returning: () => Promise.resolve([{ ...data, id: 1 }]) };
104
- },
105
- }),
106
- }),
107
- delete: () => ({
108
- where: (w: any) => {
109
- capturedWhereArg = w;
110
- return Promise.resolve();
111
- },
112
- }),
113
- },
114
- realtime: {
115
- emit: (key: string, data: any) => emitted.push({ key, data }),
116
- },
117
- },
118
- getCapturedValues: () => capturedValues,
119
- getCapturedWhere: () => capturedWhereArg,
120
- emitted,
121
- };
122
- }
123
-
124
- function createUnauthCtx(mockData: any[] = []) {
125
- const dataChain = {
126
- from: () => dataChain,
127
- where: () => dataChain,
128
- orderBy: () => dataChain,
129
- limit: () => dataChain,
130
- offset: () => Promise.resolve(mockData),
131
- };
132
- const countChain = {
133
- from: () => countChain,
134
- where: () => Promise.resolve([{ count: mockData.length }]),
135
- };
136
-
137
- return {
138
- auth: {
139
- requireAuth: () => {
140
- throw new Error("Unauthorized");
141
- },
142
- getUserIdentity: () => null,
143
- getSession: () => null,
144
- },
145
- db: {
146
- select: (selectArg?: any) => {
147
- if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
148
- return countChain;
149
- }
150
- return dataChain;
151
- },
152
- },
153
- realtime: {
154
- emit: () => {},
155
- },
156
- };
157
- }
158
-
159
- // ═══════════════════════════════════════════════════════════════════════════════
160
- // Phase 1 검증: ownerRls() 메타데이터
161
- // ═══════════════════════════════════════════════════════════════════════════════
162
-
163
- describe("ownerRls() — 메타데이터 레지스트리", () => {
164
- it("ownerRls 적용 테이블에서 메타데이터를 감지한다", () => {
165
- // ownerRls()가 호출된 rlsTasks 테이블에서 WeakMap으로 건 못 찾더라도
166
- // crud()가 getTableConfig().policies로 fallback 감지
167
- // 여기서는 registerOwnerRls를 직접 호출하여 테스트
168
- registerOwnerRls(rlsTasks, { columnName: "user_id", readPublic: false });
169
-
170
- const meta = getOwnerRlsMeta(rlsTasks);
171
- expect(meta).toBeDefined();
172
- expect(meta!.columnName).toBe("user_id");
173
- expect(meta!.readPublic).toBe(false);
174
- });
175
-
176
- it("read: public 옵션이 메타데이터에 반영된다", () => {
177
- registerOwnerRls(rlsPosts, { columnName: "user_id", readPublic: true });
178
-
179
- const meta = getOwnerRlsMeta(rlsPosts);
180
- expect(meta).toBeDefined();
181
- expect(meta!.readPublic).toBe(true);
182
- });
183
-
184
- it("ownerRls 미적용 테이블은 undefined 반환", () => {
185
- const meta = getOwnerRlsMeta(plainItems);
186
- expect(meta).toBeUndefined();
187
- });
188
- });
189
-
190
- // ═══════════════════════════════════════════════════════════════════════════════
191
- // Phase 2 검증: crud() ownerRls 감지 + 필터 주입
192
- // ═══════════════════════════════════════════════════════════════════════════════
193
-
194
- describe("crud() + ownerRls — 데이터 격리", () => {
195
- // 초기화: crud 등록
196
- crud(rlsTasks);
197
- crud(rlsPosts);
198
- crud(plainItems);
199
-
200
- // ── list 격리 ──
201
-
202
- it("list: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
203
- const listDef = getQueryDef("rls_tasks.list");
204
- expect(listDef).toBeDefined();
205
-
206
- const { ctx } = createMockCtx("user-A", []);
207
- // 에러 없이 실행되면 requireAuth 호출 성공
208
- const result = await listDef!.handler(ctx, {});
209
- expect(result).toHaveProperty("data");
210
- expect(result).toHaveProperty("total");
211
- });
212
-
213
- it("list: ownerRls 테이블은 인증 없이 접근 불가", async () => {
214
- const listDef = getQueryDef("rls_tasks.list");
215
- const unauthCtx = createUnauthCtx();
216
-
217
- await expect(listDef!.handler(unauthCtx, {})).rejects.toThrow("Unauthorized");
218
- });
219
-
220
- // ── get 격리 ──
221
-
222
- it("get: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
223
- const getDef = getQueryDef("rls_tasks.get");
224
- expect(getDef).toBeDefined();
225
-
226
- const { ctx } = createMockCtx("user-A", [{ id: 1, title: "test", userId: "user-A" }]);
227
- const result = await getDef!.handler(ctx, { id: 1 });
228
- // null 또는 데이터 반환 (mock이므로 체이닝 결과)
229
- // 핵심: 에러 없이 실행되면 성공
230
- });
231
-
232
- // ── create 자동 주입 ──
233
-
234
- it("create: ownerRls 테이블에서 userId가 인증 사용자로 강제 설정된다", async () => {
235
- const mutations = getRegisteredMutations();
236
- const createDef = mutations.find((m: any) => m.name === "rls_tasks.create");
237
- expect(createDef).toBeDefined();
238
-
239
- const { ctx, getCapturedValues } = createMockCtx("user-A");
240
- await createDef!.handler(ctx, { title: "New Task" });
241
-
242
- // userId가 인증된 사용자 ID로 강제 설정됨 (JS 프로퍼티명 사용)
243
- const values = getCapturedValues();
244
- expect(values).toBeDefined();
245
- expect(values.userId).toBe("user-A");
246
- });
247
-
248
- it("create: 타인 user_id 주입 시도는 거부되고 insert까지 가지 않음 (보안)", async () => {
249
- const mutations = getRegisteredMutations();
250
- const createDef = mutations.find((m: any) => m.name === "rls_tasks.create");
251
-
252
- const { ctx, getCapturedValues } = createMockCtx("user-A");
253
- // 해커가 user_id를 "hacker-id"로 조작 시도 — Layer 1은 즉시 Forbidden (덮어쓰기 전 차단)
254
- await expect(createDef!.handler(ctx, { title: "Spoofed", user_id: "hacker-id" })).rejects.toThrow(
255
- "Forbidden: cannot create resource for another user",
256
- );
257
-
258
- expect(getCapturedValues()).toBeNull();
259
- });
260
-
261
- // ── update 격리 ──
262
-
263
- it("update: ownerRls 테이블에서 userId 변경 시도가 차단된다", async () => {
264
- const mutations = getRegisteredMutations();
265
- const updateDef = mutations.find((m: any) => m.name === "rls_tasks.update");
266
- expect(updateDef).toBeDefined();
267
-
268
- // update에서는 set()에 전달되는 데이터에서 user_id 키가 삭제되어야 함
269
- let capturedSetData: any = null;
270
- const mockCtx = {
271
- auth: {
272
- requireAuth: () => ({ id: "user-A", email: "a@test.com" }),
273
- getUserIdentity: () => ({ id: "user-A" }),
274
- },
275
- db: {
276
- select: (selectArg?: any) => {
277
- if (selectArg && typeof selectArg === "object" && "count" in selectArg) {
278
- return { from: () => ({ where: () => Promise.resolve([{ count: 0 }]) }) };
279
- }
280
- return {
281
- from: () => ({ where: () => ({ orderBy: () => Promise.resolve([]) }) }),
282
- };
283
- },
284
- update: () => ({
285
- set: (data: any) => {
286
- capturedSetData = data;
287
- return {
288
- where: () => ({
289
- returning: () => Promise.resolve([{ id: 1, title: "Updated" }]),
290
- }),
291
- };
292
- },
293
- }),
294
- },
295
- realtime: { emit: () => {} },
296
- };
297
-
298
- await updateDef!.handler(mockCtx, {
299
- id: 1,
300
- title: "Updated",
301
- user_id: "hacker-id", // userId 변경 시도
302
- });
303
-
304
- // user_id가 set 데이터에서 삭제되었는지 확인 (JS 프로퍼티명 + DB명 양쪽)
305
- expect(capturedSetData).toBeDefined();
306
- expect(capturedSetData).not.toHaveProperty("userId");
307
- expect(capturedSetData).not.toHaveProperty("user_id");
308
- expect(capturedSetData.title).toBe("Updated");
309
- });
310
-
311
- // ── remove 격리 ──
312
-
313
- it("remove: ownerRls 테이블에서 requireAuth가 호출된다", async () => {
314
- const mutations = getRegisteredMutations();
315
- const removeDef = mutations.find((m: any) => m.name === "rls_tasks.remove");
316
- expect(removeDef).toBeDefined();
317
-
318
- const { ctx } = createMockCtx("user-A");
319
- const result = await removeDef!.handler(ctx, { id: 1 });
320
- expect(result).toEqual({ success: true });
321
- });
322
- });
323
-
324
- // ═══════════════════════════════════════════════════════════════════════════════
325
- // read: "public" 테스트
326
- // ═══════════════════════════════════════════════════════════════════════════════
327
-
328
- describe("crud() + ownerRls(read: 'public') — 공개 읽기", () => {
329
- it("list: 인증 없이 접근 가능 (read: public이므로 auth skip)", async () => {
330
- // rlsPosts는 ownerRls(userId, { read: "public" })
331
- // readPublic: true → list에서 userId 필터 생략
332
- const listDef = getQueryDef("rls_posts.list");
333
- expect(listDef).toBeDefined();
334
-
335
- // crud는 isPublic=false로 생성되었으므로 requireAuth는 호출됨
336
- // 하지만 readPublic=true이므로 userId 필터는 추가되지 않음
337
- const { ctx } = createMockCtx("user-A", [{ id: 1, content: "hello" }]);
338
- const result = await listDef!.handler(ctx, {});
339
- expect(result.data).toHaveLength(1);
340
- });
341
-
342
- it("create: 인증 필수 + userId 강제 주입", async () => {
343
- const mutations = getRegisteredMutations();
344
- const createDef = mutations.find((m: any) => m.name === "rls_posts.create");
345
- expect(createDef).toBeDefined();
346
-
347
- const { ctx, getCapturedValues } = createMockCtx("user-B");
348
- await createDef!.handler(ctx, { content: "Public post" });
349
-
350
- const values = getCapturedValues();
351
- expect(values.userId).toBe("user-B");
352
- });
353
- });
354
-
355
- // ═══════════════════════════════════════════════════════════════════════════════
356
- // 하위호환 테스트 — ownerRls 미적용 테이블
357
- // ═══════════════════════════════════════════════════════════════════════════════
358
-
359
- describe("crud() — ownerRls 미적용 하위호환", () => {
360
- it("ownerRls 없는 테이블에서 기존 userId 자동 주입 동작 유지", async () => {
361
- const mutations = getRegisteredMutations();
362
- const createDef = mutations.find((m: any) => m.name === "plain_items.create");
363
- expect(createDef).toBeDefined();
364
-
365
- const { ctx, getCapturedValues } = createMockCtx("user-C");
366
- await createDef!.handler(ctx, { name: "Widget" });
367
-
368
- const values = getCapturedValues();
369
- // ownerRls 미적용 → 기존 자동 주입 로직: userId 컬럼이 있고 인증됐으면 주입
370
- expect(values.userId).toBe("user-C");
371
- });
372
-
373
- it("ownerRls 없는 테이블의 list는 userId 필터 없이 전체 반환", async () => {
374
- const listDef = getQueryDef("plain_items.list");
375
- expect(listDef).toBeDefined();
376
-
377
- const allData = [
378
- { id: 1, name: "A", userId: "user-A" },
379
- { id: 2, name: "B", userId: "user-B" },
380
- ];
381
- const { ctx } = createMockCtx("user-A", allData);
382
- const result = await listDef!.handler(ctx, {});
383
-
384
- // ownerRls 미적용 → 모든 데이터 반환 (기존 동작)
385
- expect(result.data).toHaveLength(2);
386
- });
387
- });