@gencow/core 0.1.27 → 0.1.29

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 (130) hide show
  1. package/dist/auth-config.d.ts +92 -5
  2. package/dist/config.d.ts +107 -0
  3. package/dist/config.js +12 -0
  4. package/dist/context.d.ts +139 -0
  5. package/dist/context.js +3 -0
  6. package/dist/crud.d.ts +5 -5
  7. package/dist/crud.js +19 -35
  8. package/dist/document-types.d.ts +65 -0
  9. package/dist/document-types.js +15 -0
  10. package/dist/grounded-answer-types.d.ts +62 -0
  11. package/dist/grounded-answer-types.js +6 -0
  12. package/dist/http-action.d.ts +77 -0
  13. package/dist/http-action.js +41 -0
  14. package/dist/index.d.ts +30 -5
  15. package/dist/index.js +15 -2
  16. package/dist/platform-capacity-profile.d.ts +19 -0
  17. package/dist/platform-capacity-profile.js +94 -0
  18. package/dist/procedure.d.ts +58 -0
  19. package/dist/procedure.js +115 -0
  20. package/dist/rag-ingest-types.d.ts +39 -0
  21. package/dist/rag-ingest-types.js +1 -0
  22. package/dist/rag-operations-types.d.ts +81 -0
  23. package/dist/rag-operations-types.js +1 -0
  24. package/dist/rag-schema.d.ts +1466 -0
  25. package/dist/rag-schema.js +87 -0
  26. package/dist/reactive-mutation-types.d.ts +11 -0
  27. package/dist/reactive-mutation-types.js +1 -0
  28. package/dist/reactive-mutation.d.ts +51 -0
  29. package/dist/reactive-mutation.js +75 -0
  30. package/dist/reactive-query-types.d.ts +12 -0
  31. package/dist/reactive-query-types.js +1 -0
  32. package/dist/reactive-query.d.ts +14 -0
  33. package/dist/reactive-query.js +28 -0
  34. package/dist/reactive-realtime.d.ts +48 -0
  35. package/dist/reactive-realtime.js +236 -0
  36. package/dist/reactive.d.ts +29 -5
  37. package/dist/reactive.js +65 -0
  38. package/dist/rls-db.d.ts +9 -2
  39. package/dist/runtime-env-policy.d.ts +5 -0
  40. package/dist/runtime-env-policy.js +56 -0
  41. package/dist/search-types.d.ts +83 -0
  42. package/dist/search-types.js +1 -0
  43. package/dist/server.d.ts +1 -2
  44. package/dist/server.js +0 -1
  45. package/dist/storage-metering.d.ts +13 -0
  46. package/dist/storage-metering.js +18 -0
  47. package/dist/storage-shared.d.ts +36 -0
  48. package/dist/storage-shared.js +39 -0
  49. package/dist/storage.d.ts +5 -27
  50. package/dist/storage.js +30 -22
  51. package/dist/wake-app-result.d.ts +22 -0
  52. package/dist/wake-app-result.js +11 -0
  53. package/dist/workflow-types.d.ts +16 -2
  54. package/dist/workflow.d.ts +1 -1
  55. package/dist/workflow.js +136 -11
  56. package/dist/workflows-api.js +71 -3
  57. package/package.json +11 -7
  58. package/src/auth-config.ts +104 -3
  59. package/src/config.ts +119 -0
  60. package/src/context.ts +152 -0
  61. package/src/crud.ts +18 -35
  62. package/src/document-types.ts +102 -0
  63. package/src/grounded-answer-types.ts +78 -0
  64. package/src/http-action.ts +101 -0
  65. package/src/index.ts +142 -19
  66. package/src/platform-capacity-profile.ts +114 -0
  67. package/src/procedure.ts +283 -0
  68. package/src/rag-ingest-types.ts +52 -0
  69. package/src/rag-operations-types.ts +90 -0
  70. package/src/rag-schema.ts +94 -0
  71. package/src/reactive-mutation-types.ts +13 -0
  72. package/src/reactive-mutation.ts +115 -0
  73. package/src/reactive-query-types.ts +14 -0
  74. package/src/reactive-query.ts +48 -0
  75. package/src/reactive-realtime.ts +267 -0
  76. package/src/rls-db.ts +9 -4
  77. package/src/runtime-env-policy.ts +66 -0
  78. package/src/search-types.ts +91 -0
  79. package/src/server.ts +6 -2
  80. package/src/storage-metering.ts +35 -0
  81. package/src/storage-shared.ts +74 -0
  82. package/src/storage.ts +44 -53
  83. package/src/wake-app-result.ts +37 -0
  84. package/src/workflow-types.ts +16 -2
  85. package/src/workflow.ts +166 -12
  86. package/src/workflows-api.ts +82 -3
  87. package/src/__tests__/auth.test.ts +0 -118
  88. package/src/__tests__/crons.test.ts +0 -83
  89. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  90. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  91. package/src/__tests__/crud.test.ts +0 -930
  92. package/src/__tests__/dist-exports.test.ts +0 -176
  93. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  94. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  95. package/src/__tests__/fixtures/basic/index.ts +0 -6
  96. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  97. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  98. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  99. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  100. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  101. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  102. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  103. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  104. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  105. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  106. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  107. package/src/__tests__/httpaction.test.ts +0 -122
  108. package/src/__tests__/image-optimization.test.ts +0 -648
  109. package/src/__tests__/load.test.ts +0 -389
  110. package/src/__tests__/network-sim.test.ts +0 -319
  111. package/src/__tests__/reactive.test.ts +0 -479
  112. package/src/__tests__/retry.test.ts +0 -113
  113. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  114. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  115. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  116. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  117. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  118. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  119. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  120. package/src/__tests__/scheduler-durable.test.ts +0 -173
  121. package/src/__tests__/scheduler-exec.test.ts +0 -328
  122. package/src/__tests__/scheduler.test.ts +0 -187
  123. package/src/__tests__/storage.test.ts +0 -334
  124. package/src/__tests__/tsconfig.json +0 -8
  125. package/src/__tests__/validator.test.ts +0 -323
  126. package/src/__tests__/workflow.test.ts +0 -606
  127. package/src/__tests__/ws-integration.test.ts +0 -309
  128. package/src/__tests__/ws-scale.test.ts +0 -241
  129. package/src/auth.ts +0 -155
  130. package/src/reactive.ts +0 -580
@@ -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
- });
@@ -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
- });