@gencow/core 0.1.12 → 0.1.14

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.
@@ -0,0 +1,253 @@
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";
28
+ import {
29
+ query,
30
+ mutation,
31
+ getRegisteredQueries,
32
+ getRegisteredMutations,
33
+ getQueryDef,
34
+ } from "../reactive";
35
+
36
+ // ─── 테스트용 테이블 정의 ─────────────────────────────────────────────
37
+
38
+ const keywords = pgTable("cg_keywords", {
39
+ id: serial("id").primaryKey(),
40
+ keyword: text("keyword").notNull(),
41
+ userId: text("user_id"),
42
+ createdAt: timestamp("created_at").defaultNow(),
43
+ });
44
+
45
+ const crawlLogs = pgTable("cg_crawl_logs", {
46
+ id: serial("id").primaryKey(),
47
+ status: text("status"),
48
+ createdAt: timestamp("created_at").defaultNow(),
49
+ });
50
+
51
+ const digests = pgTable("cg_digests", {
52
+ id: serial("id").primaryKey(),
53
+ title: text("title"),
54
+ content: text("content"),
55
+ createdAt: timestamp("created_at").defaultNow(),
56
+ });
57
+
58
+ const appSettings = pgTable("cg_app_settings", {
59
+ id: serial("id").primaryKey(),
60
+ key: text("key"),
61
+ value: text("value"),
62
+ });
63
+
64
+ // ═══════════════════════════════════════════════════════════════════════════════
65
+ // 1. crud() → 레지스트리 등록 → codegen 인식 전체 파이프라인
66
+ // ═══════════════════════════════════════════════════════════════════════════════
67
+
68
+ describe("crud() → codegen 통합 — 다중 테이블", () => {
69
+ beforeAll(() => {
70
+ // test032 패턴 재현: 4개 모듈이 각각 crud() 호출
71
+ crud(keywords, { public: true });
72
+ crud(crawlLogs, { public: true });
73
+ crud(digests, { public: true });
74
+ crud(appSettings, { public: true });
75
+ });
76
+
77
+ it("4개 테이블의 list/get query가 모두 레지스트리에 등록된다", () => {
78
+ const queries = getRegisteredQueries();
79
+
80
+ // 각 테이블의 list + get = 8개
81
+ expect(queries).toContain("cg_keywords.list");
82
+ expect(queries).toContain("cg_keywords.get");
83
+ expect(queries).toContain("cg_crawl_logs.list");
84
+ expect(queries).toContain("cg_crawl_logs.get");
85
+ expect(queries).toContain("cg_digests.list");
86
+ expect(queries).toContain("cg_digests.get");
87
+ expect(queries).toContain("cg_app_settings.list");
88
+ expect(queries).toContain("cg_app_settings.get");
89
+ });
90
+
91
+ it("4개 테이블의 create/update/remove mutation이 모두 레지스트리에 등록된다", () => {
92
+ const mutations = getRegisteredMutations();
93
+ const names = mutations.map((m) => m.name);
94
+
95
+ // 각 테이블의 create + update + remove = 12개
96
+ expect(names).toContain("cg_keywords.create");
97
+ expect(names).toContain("cg_keywords.update");
98
+ expect(names).toContain("cg_keywords.remove");
99
+ expect(names).toContain("cg_crawl_logs.create");
100
+ expect(names).toContain("cg_crawl_logs.update");
101
+ expect(names).toContain("cg_crawl_logs.remove");
102
+ expect(names).toContain("cg_digests.create");
103
+ expect(names).toContain("cg_digests.update");
104
+ expect(names).toContain("cg_digests.remove");
105
+ expect(names).toContain("cg_app_settings.create");
106
+ expect(names).toContain("cg_app_settings.update");
107
+ expect(names).toContain("cg_app_settings.remove");
108
+ });
109
+ });
110
+
111
+ // ═══════════════════════════════════════════════════════════════════════════════
112
+ // 2. crud() + 수동 query/mutation 혼용 — 양쪽 모두 codegen에 포함
113
+ // ═══════════════════════════════════════════════════════════════════════════════
114
+
115
+ describe("crud() + 수동 query/mutation 혼용", () => {
116
+ beforeAll(() => {
117
+ // crud로 자동 등록
118
+ const articlesTable = pgTable("cg_articles", {
119
+ id: serial("id").primaryKey(),
120
+ title: text("title"),
121
+ createdAt: timestamp("created_at").defaultNow(),
122
+ });
123
+ crud(articlesTable, { public: true });
124
+
125
+ // 수동으로 추가 등록 (test032의 digestsModule.generate 패턴)
126
+ query("cg_digests.latest", {
127
+ public: true,
128
+ handler: async (ctx) => {
129
+ return { id: 1, title: "latest" };
130
+ },
131
+ });
132
+
133
+ mutation("cg_digests.generate", {
134
+ invalidates: ["cg_digests.list"],
135
+ public: true,
136
+ handler: async (ctx) => {
137
+ return { success: true };
138
+ },
139
+ });
140
+ });
141
+
142
+ it("crud 등록 + 수동 등록이 모두 getRegisteredQueries()에 포함된다", () => {
143
+ const queries = getRegisteredQueries();
144
+
145
+ // crud 자동 등록
146
+ expect(queries).toContain("cg_articles.list");
147
+ expect(queries).toContain("cg_articles.get");
148
+
149
+ // 수동 등록
150
+ expect(queries).toContain("cg_digests.latest");
151
+ });
152
+
153
+ it("crud 등록 + 수동 등록이 모두 getRegisteredMutations()에 포함된다", () => {
154
+ const mutations = getRegisteredMutations();
155
+ const names = mutations.map((m) => m.name);
156
+
157
+ // crud 자동 등록
158
+ expect(names).toContain("cg_articles.create");
159
+ expect(names).toContain("cg_articles.update");
160
+ expect(names).toContain("cg_articles.remove");
161
+
162
+ // 수동 등록
163
+ expect(names).toContain("cg_digests.generate");
164
+ });
165
+ });
166
+
167
+ // ═══════════════════════════════════════════════════════════════════════════════
168
+ // 3. public/private 상태 정확성 — codegen이 isPublic 기반으로 auth 분기
169
+ // ═══════════════════════════════════════════════════════════════════════════════
170
+
171
+ describe("crud() isPublic 상태 — codegen auth 분기 정확성", () => {
172
+ beforeAll(() => {
173
+ const publicTable = pgTable("cg_public_data", {
174
+ id: serial("id").primaryKey(),
175
+ name: text("name"),
176
+ });
177
+ const privateTable = pgTable("cg_private_data", {
178
+ id: serial("id").primaryKey(),
179
+ name: text("name"),
180
+ });
181
+
182
+ crud(publicTable, { public: true });
183
+ crud(privateTable); // default: auth 필수
184
+ });
185
+
186
+ it("public: true 테이블의 모든 엔드포인트가 isPublic === true", () => {
187
+ const listDef = getQueryDef("cg_public_data.list");
188
+ const getDef = getQueryDef("cg_public_data.get");
189
+
190
+ expect(listDef!.isPublic).toBe(true);
191
+ expect(getDef!.isPublic).toBe(true);
192
+
193
+ const mutations = getRegisteredMutations();
194
+ const createDef = mutations.find((m) => m.name === "cg_public_data.create");
195
+ expect(createDef!.isPublic).toBe(true);
196
+ });
197
+
198
+ it("기본(private) 테이블의 모든 엔드포인트가 isPublic === false", () => {
199
+ const listDef = getQueryDef("cg_private_data.list");
200
+ const getDef = getQueryDef("cg_private_data.get");
201
+
202
+ expect(listDef!.isPublic).toBe(false);
203
+ expect(getDef!.isPublic).toBe(false);
204
+
205
+ const mutations = getRegisteredMutations();
206
+ const createDef = mutations.find((m) => m.name === "cg_private_data.create");
207
+ expect(createDef!.isPublic).toBe(false);
208
+ });
209
+ });
210
+
211
+ // ═══════════════════════════════════════════════════════════════════════════════
212
+ // 4. codegen 시뮬레이션 — getRegisteredQueries/Mutations로 api.ts 생성
213
+ // ═══════════════════════════════════════════════════════════════════════════════
214
+
215
+ describe("codegen 시뮬레이션 — api.ts 생성 가능 여부", () => {
216
+ it("getRegisteredQueries()로 모든 query key를 열거할 수 있다", () => {
217
+ const queries = getRegisteredQueries();
218
+ expect(queries.length).toBeGreaterThan(0);
219
+
220
+ // 모든 query key가 "namespace.action" 패턴이어야 함
221
+ for (const key of queries) {
222
+ expect(key).toMatch(/^[a-z_]+\.[a-z_]+$/i);
223
+ }
224
+ });
225
+
226
+ it("getRegisteredMutations()로 모든 mutation을 열거할 수 있다", () => {
227
+ const mutations = getRegisteredMutations();
228
+ expect(mutations.length).toBeGreaterThan(0);
229
+
230
+ // 모든 mutation이 name + handler를 가져야 함
231
+ for (const mut of mutations) {
232
+ expect(mut.name).toBeTruthy();
233
+ expect(typeof mut.handler).toBe("function");
234
+ }
235
+ });
236
+
237
+ it("getQueryDef()로 개별 query의 argsSchema에 접근할 수 있다", () => {
238
+ const listDef = getQueryDef("cg_keywords.list");
239
+ expect(listDef).toBeDefined();
240
+ // list handler에는 argsSchema가 있음 (limit, offset, sort, filters)
241
+ expect(listDef!.handler).toBeDefined();
242
+ });
243
+
244
+ it("codegen 대상 query/mutation 총 개수가 올바르다", () => {
245
+ const queries = getRegisteredQueries();
246
+ const mutations = getRegisteredMutations();
247
+
248
+ // 최소 8개 query (4테이블 × list+get) + 수동 1개 + 이전 테스트들
249
+ expect(queries.length).toBeGreaterThanOrEqual(8);
250
+ // 최소 12개 mutation (4테이블 × create+update+remove) + 수동 1개
251
+ expect(mutations.length).toBeGreaterThanOrEqual(12);
252
+ });
253
+ });