@gencow/core 0.1.13 → 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.
- package/dist/crud.js +17 -16
- package/package.json +1 -1
- package/src/__tests__/crud-codegen-integration.test.ts +253 -0
- package/src/__tests__/dist-exports.test.ts +170 -0
- package/src/crud.ts +17 -16
package/dist/crud.js
CHANGED
|
@@ -94,11 +94,12 @@ export function crud(table, options) {
|
|
|
94
94
|
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
95
95
|
}
|
|
96
96
|
// ── 내부 헬퍼: list+count 데이터 가져오기 (realtime push용 재사용) ──
|
|
97
|
+
// 순차 실행: 소규모 커넥션 풀 환경에서 동시 점유 방지
|
|
98
|
+
// ⚠️ limit/offset 없이 전체 SELECT — 대량 데이터 시 성능 저하 주의
|
|
99
|
+
// TODO(P2): realtime emit 시 invalidation 메시지만 전송하고 클라이언트가 re-fetch하는 패턴 검토
|
|
97
100
|
async function fetchListWithTotal(db, whereClause) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
db.select({ count: drizzleCount() }).from(anyTable).where(whereClause),
|
|
101
|
-
]);
|
|
101
|
+
const data = await db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol));
|
|
102
|
+
const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
|
|
102
103
|
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
103
104
|
}
|
|
104
105
|
// ── list ──────────────────────────────────────
|
|
@@ -128,18 +129,18 @@ export function crud(table, options) {
|
|
|
128
129
|
else {
|
|
129
130
|
orderByClause = desc(defaultOrderCol);
|
|
130
131
|
}
|
|
131
|
-
// SELECT + COUNT
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
// SELECT + COUNT 순차 실행 — 소규모 커넥션 풀 환경에서 동시 점유 방지
|
|
133
|
+
// Note: data←→count 사이 INSERT/DELETE 시 total 불일치 가능 (BaaS 단일사용자/저부하에서 무시 가능)
|
|
134
|
+
// TODO(P2): 대규모 시 db.transaction() 래핑 검토
|
|
135
|
+
const results = await ctx.db.select()
|
|
136
|
+
.from(anyTable)
|
|
137
|
+
.where(whereClause)
|
|
138
|
+
.orderBy(orderByClause)
|
|
139
|
+
.limit(limit)
|
|
140
|
+
.offset(offset);
|
|
141
|
+
const countResult = await ctx.db.select({ count: drizzleCount() })
|
|
142
|
+
.from(anyTable)
|
|
143
|
+
.where(whereClause);
|
|
143
144
|
return {
|
|
144
145
|
data: results,
|
|
145
146
|
total: Number(countResult[0]?.count ?? 0),
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/__tests__/dist-exports.test.ts
|
|
3
|
+
*
|
|
4
|
+
* dist/ 빌드 산출물의 필수 export 검증 — npm publish 전 안전망.
|
|
5
|
+
*
|
|
6
|
+
* 이 테스트의 존재 이유:
|
|
7
|
+
* 2026-04-02 사고: 소스코드에 crud v2가 완전 구현되어 있었으나,
|
|
8
|
+
* dist/index.js에 `crud` export가 누락된 구버전이 npm에 배포됨.
|
|
9
|
+
* crud.test.ts(16 pass)가 src/crud.ts를 직접 import하므로
|
|
10
|
+
* dist/ 빌드 결과물의 정합성은 검증하지 못했음.
|
|
11
|
+
*
|
|
12
|
+
* 검증 항목:
|
|
13
|
+
* 1. dist/index.js 파일 존재
|
|
14
|
+
* 2. 필수 named export 존재 (crud, query, mutation, v, ...)
|
|
15
|
+
* 3. gencowCrud deprecated alias 존재 (하위호환)
|
|
16
|
+
* 4. crud가 실제 함수인지 (typeof === "function")
|
|
17
|
+
* 5. dist/index.js의 crud와 src/crud.ts의 crud가 동일 함수인지
|
|
18
|
+
*
|
|
19
|
+
* Run: bun test packages/core/src/__tests__/dist-exports.test.ts
|
|
20
|
+
*
|
|
21
|
+
* @see docs/analysis/analysis-test032-gencow-crud-api-mismatch.md
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from "bun:test";
|
|
25
|
+
import { existsSync } from "fs";
|
|
26
|
+
import { resolve, dirname } from "path";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const CORE_ROOT = resolve(__dirname, "../..");
|
|
31
|
+
const DIST_INDEX = resolve(CORE_ROOT, "dist/index.js");
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// 1. dist/ 파일 존재 확인
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
describe("dist/ 빌드 산출물 존재", () => {
|
|
38
|
+
it("dist/index.js 파일이 존재한다", () => {
|
|
39
|
+
expect(existsSync(DIST_INDEX)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("dist/crud.js 파일이 존재한다", () => {
|
|
43
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/crud.js"))).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("dist/reactive.js 파일이 존재한다", () => {
|
|
47
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/reactive.js"))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("dist/v.js 파일이 존재한다", () => {
|
|
51
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/v.js"))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
// 2. 필수 named export 존재 확인
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
|
|
59
|
+
describe("dist/index.js 필수 export", () => {
|
|
60
|
+
let distModule: Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
// dist/index.js를 dynamic import (빌드 결과물 검증)
|
|
63
|
+
it("dist/index.js를 import할 수 있다", async () => {
|
|
64
|
+
distModule = await import(DIST_INDEX);
|
|
65
|
+
expect(distModule).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Core API exports ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
it("crud export가 존재하고 함수이다", async () => {
|
|
71
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
72
|
+
expect(distModule.crud).toBeDefined();
|
|
73
|
+
expect(typeof distModule.crud).toBe("function");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gencowCrud deprecated alias가 존재하고 crud와 동일하다", async () => {
|
|
77
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
78
|
+
expect(distModule.gencowCrud).toBeDefined();
|
|
79
|
+
expect(distModule.gencowCrud).toBe(distModule.crud);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("query export가 존재하고 함수이다", async () => {
|
|
83
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
84
|
+
expect(distModule.query).toBeDefined();
|
|
85
|
+
expect(typeof distModule.query).toBe("function");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("mutation export가 존재하고 함수이다", async () => {
|
|
89
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
90
|
+
expect(distModule.mutation).toBeDefined();
|
|
91
|
+
expect(typeof distModule.mutation).toBe("function");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("v export가 존재하고 객체이다", async () => {
|
|
95
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
96
|
+
expect(distModule.v).toBeDefined();
|
|
97
|
+
expect(typeof distModule.v).toBe("object");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("httpAction export가 존재하고 함수이다", async () => {
|
|
101
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
102
|
+
expect(distModule.httpAction).toBeDefined();
|
|
103
|
+
expect(typeof distModule.httpAction).toBe("function");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── Registry exports (codegen 의존) ────────────────────────
|
|
107
|
+
|
|
108
|
+
it("getRegisteredQueries export가 존재한다", async () => {
|
|
109
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
110
|
+
expect(distModule.getRegisteredQueries).toBeDefined();
|
|
111
|
+
expect(typeof distModule.getRegisteredQueries).toBe("function");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("getRegisteredMutations export가 존재한다", async () => {
|
|
115
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
116
|
+
expect(distModule.getRegisteredMutations).toBeDefined();
|
|
117
|
+
expect(typeof distModule.getRegisteredMutations).toBe("function");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── RLS + Auth exports ─────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
it("ownerRls export가 존재한다", async () => {
|
|
123
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
124
|
+
expect(distModule.ownerRls).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("defineAuth export가 존재한다", async () => {
|
|
128
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
129
|
+
expect(distModule.defineAuth).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("cronJobs export가 존재한다", async () => {
|
|
133
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
134
|
+
expect(distModule.cronJobs).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
139
|
+
// 3. dist/crud.js가 v2 구현인지 확인 (커링 구조가 아닌지)
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
describe("dist/crud.js — v2 구현 검증", () => {
|
|
143
|
+
it("crud(table) 시그니처이다 (커링 gencowCrud(db)(table) 아님)", async () => {
|
|
144
|
+
const distModule = await import(DIST_INDEX);
|
|
145
|
+
const { crud } = distModule;
|
|
146
|
+
|
|
147
|
+
// v2: crud(table, options?) → { list, get, create, update, remove }
|
|
148
|
+
// 구버전: gencowCrud(db) → (table, options?) → { create, findById, list, ... }
|
|
149
|
+
|
|
150
|
+
// 실제 Drizzle 테이블로 테스트
|
|
151
|
+
const { pgTable, serial, text } = await import("drizzle-orm/pg-core");
|
|
152
|
+
const testTable = pgTable("dist_smoke_test", {
|
|
153
|
+
id: serial("id").primaryKey(),
|
|
154
|
+
name: text("name"),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = crud(testTable, { public: true });
|
|
158
|
+
|
|
159
|
+
// v2는 { list, get, create, update, remove } 반환
|
|
160
|
+
expect(result).toHaveProperty("list");
|
|
161
|
+
expect(result).toHaveProperty("get");
|
|
162
|
+
expect(result).toHaveProperty("create");
|
|
163
|
+
expect(result).toHaveProperty("update");
|
|
164
|
+
expect(result).toHaveProperty("remove");
|
|
165
|
+
|
|
166
|
+
// 구버전(커링)은 함수를 반환하므로 이 검증이 실패함
|
|
167
|
+
expect(typeof result).toBe("object");
|
|
168
|
+
expect(typeof result.list).not.toBe("undefined");
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/crud.ts
CHANGED
|
@@ -137,11 +137,12 @@ export function crud<T extends PgTable>(table: T, options?: CrudOptions<T>) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// ── 내부 헬퍼: list+count 데이터 가져오기 (realtime push용 재사용) ──
|
|
140
|
+
// 순차 실행: 소규모 커넥션 풀 환경에서 동시 점유 방지
|
|
141
|
+
// ⚠️ limit/offset 없이 전체 SELECT — 대량 데이터 시 성능 저하 주의
|
|
142
|
+
// TODO(P2): realtime emit 시 invalidation 메시지만 전송하고 클라이언트가 re-fetch하는 패턴 검토
|
|
140
143
|
async function fetchListWithTotal(db: any, whereClause?: SQL) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
db.select({ count: drizzleCount() }).from(anyTable).where(whereClause),
|
|
144
|
-
]);
|
|
144
|
+
const data = await db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol));
|
|
145
|
+
const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
|
|
145
146
|
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -175,18 +176,18 @@ export function crud<T extends PgTable>(table: T, options?: CrudOptions<T>) {
|
|
|
175
176
|
orderByClause = desc(defaultOrderCol);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
// SELECT + COUNT
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
// SELECT + COUNT 순차 실행 — 소규모 커넥션 풀 환경에서 동시 점유 방지
|
|
180
|
+
// Note: data←→count 사이 INSERT/DELETE 시 total 불일치 가능 (BaaS 단일사용자/저부하에서 무시 가능)
|
|
181
|
+
// TODO(P2): 대규모 시 db.transaction() 래핑 검토
|
|
182
|
+
const results = await ctx.db.select()
|
|
183
|
+
.from(anyTable)
|
|
184
|
+
.where(whereClause)
|
|
185
|
+
.orderBy(orderByClause)
|
|
186
|
+
.limit(limit)
|
|
187
|
+
.offset(offset);
|
|
188
|
+
const countResult = await ctx.db.select({ count: drizzleCount() })
|
|
189
|
+
.from(anyTable)
|
|
190
|
+
.where(whereClause);
|
|
190
191
|
|
|
191
192
|
return {
|
|
192
193
|
data: results,
|