@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 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 [data, countResult] = await Promise.all([
99
- db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol)),
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
- const [results, countResult] = await Promise.all([
133
- ctx.db.select()
134
- .from(anyTable)
135
- .where(whereClause)
136
- .orderBy(orderByClause)
137
- .limit(limit)
138
- .offset(offset),
139
- ctx.db.select({ count: drizzleCount() })
140
- .from(anyTable)
141
- .where(whereClause),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gencow/core",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 [data, countResult] = await Promise.all([
142
- db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol)),
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
- const [results, countResult] = await Promise.all([
180
- ctx.db.select()
181
- .from(anyTable)
182
- .where(whereClause)
183
- .orderBy(orderByClause)
184
- .limit(limit)
185
- .offset(offset),
186
- ctx.db.select({ count: drizzleCount() })
187
- .from(anyTable)
188
- .where(whereClause),
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,