reltype 0.1.3 → 0.1.5

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 (34) hide show
  1. package/CHANGELOG.md +83 -3
  2. package/dist/features/connection/pool.d.ts +8 -1
  3. package/dist/features/connection/pool.d.ts.map +1 -1
  4. package/dist/features/connection/pool.js +14 -5
  5. package/dist/features/query/builder.d.ts +10 -3
  6. package/dist/features/query/builder.d.ts.map +1 -1
  7. package/dist/features/query/builder.js +27 -28
  8. package/dist/features/query/bulkInsert.d.ts +6 -3
  9. package/dist/features/query/bulkInsert.d.ts.map +1 -1
  10. package/dist/features/query/bulkInsert.js +14 -4
  11. package/dist/features/query/delete.d.ts +3 -1
  12. package/dist/features/query/delete.d.ts.map +1 -1
  13. package/dist/features/query/delete.js +11 -2
  14. package/dist/features/query/insert.d.ts +2 -0
  15. package/dist/features/query/insert.d.ts.map +1 -1
  16. package/dist/features/query/insert.js +8 -0
  17. package/dist/features/query/update.d.ts +2 -0
  18. package/dist/features/query/update.d.ts.map +1 -1
  19. package/dist/features/query/update.js +13 -0
  20. package/dist/features/query/upsert.d.ts +3 -1
  21. package/dist/features/query/upsert.d.ts.map +1 -1
  22. package/dist/features/query/upsert.js +19 -1
  23. package/dist/features/repository/base.d.ts +9 -13
  24. package/dist/features/repository/base.d.ts.map +1 -1
  25. package/dist/features/repository/base.js +29 -37
  26. package/dist/features/schema/column.d.ts +26 -11
  27. package/dist/features/schema/column.d.ts.map +1 -1
  28. package/dist/features/schema/column.js +32 -18
  29. package/dist/features/schema/interfaces/Infer.d.ts +8 -1
  30. package/dist/features/schema/interfaces/Infer.d.ts.map +1 -1
  31. package/dist/features/transform/case.d.ts +12 -2
  32. package/dist/features/transform/case.d.ts.map +1 -1
  33. package/dist/features/transform/case.js +19 -4
  34. package/package.json +2 -4
package/CHANGELOG.md CHANGED
@@ -6,7 +6,87 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
- ## [0.1.0] — 2025-03-19
9
+ ## [0.1.5] — 2026-03-19
10
+
11
+ ### Changed
12
+ - `README.md` — 전체 문서 최신 API 기준으로 업데이트
13
+ - `README.ko.md` — 한국어 문서 최신 API 기준으로 업데이트
14
+ - `CHANGELOG.md` — 0.1.1 ~ 0.1.4 릴리즈 내역 소급 작성
15
+
16
+ ---
17
+
18
+ ## [0.1.4] — 2026-03-19
19
+
20
+ ### Changed
21
+ - `package.json` / `package-lock.json` — 버전 범프 (0.1.3 → 0.1.4)
22
+
23
+ ---
24
+
25
+ ## [0.1.3] — 2026-03-19
26
+
27
+ ### Fixed (refactor: type info)
28
+
29
+ #### Query Guard — 빈 입력 방어 로직 추가
30
+ - `buildInsert` — 삽입할 데이터가 없을 때 `logger.error` 출력 후 no-op 반환
31
+ - `buildUpdate` — 수정 데이터 없음 / WHERE 조건 없음 각각 감지 후 no-op 반환 (전체 업데이트 방지)
32
+ - `buildDelete` — WHERE 조건 없음 감지 후 no-op 반환 (전체 삭제 방지), `RETURNING *` 추가
33
+ - `buildUpsert` — 삽입 데이터 없음 감지 후 no-op 반환, 충돌 컬럼만 있을 경우 `DO NOTHING` 처리
34
+ - `buildBulkInsert` — 빈 배열 및 첫 row 데이터 없음 감지 후 no-op 반환
35
+
36
+ #### QueryBuilder — 안정성 개선
37
+ - `one()` — `this._limitVal` 직접 변이 제거 → `clone()`으로 원본 불변 처리
38
+ - `paginate()` — `Promise.all` 제거, 같은 클라이언트에서 COUNT → DATA 순차 실행
39
+ - `cursorPaginate()` — 잘못된 cursor 토큰 시 `logger.error` 출력 후 빈 결과 반환 (cursor 값 메시지 노출 제거)
40
+ - `clone()` — `_execHooks` 복사 누락 수정
41
+
42
+ #### BaseRepo — 일관성 개선
43
+ - `exec()` — 빈 SQL 진입 시 `logger.error` 후 즉시 `[]` 반환 (no-op 계층 추가)
44
+ - `delete()` — 내부 `withClient` 직접 호출 제거, `exec()` 재사용으로 로깅 일관성 확보
45
+ - `selectOne()` — 글로벌 훅(`useHooks`) 자동 적용되도록 수정 (`this.select(where).one()` 위임)
46
+ - `findPkKey()` — primary key 미정의 시 조용한 fallback 대신 `logger.warn` 출력
47
+
48
+ #### Schema / Column
49
+ - `Col` — `isDefaultNow: boolean` 필드 추가로 `default()` / `defaultNow()` 의미 구분
50
+ - `Col.nullable()` / `notNull()` / `primaryKey()` — `isDefaultNow` 상태 올바르게 전파
51
+ - `toSnake()` — 두문자어(Acronym) 처리 개선 (정규식 2단계 적용)
52
+ - `URLParam` → `url_param` ✓ / `userID` → `user_id` ✓
53
+
54
+ #### Connection Pool
55
+ - `PoolStatus` — `isInitialized: boolean` 필드 추가
56
+ - `getPoolStatus()` — pool 미초기화 시 `isHealthy: false` 반환 (이전: `true`)
57
+ - `readPoolStatus()` — `isHealthy` 조건 개선: `!(waitingCount > 0 && idleCount === 0)` (pool 소진 상태만 unhealthy)
58
+
59
+ #### Package
60
+ - `dotenv` — `dependencies` → `devDependencies` 이동 (라이브러리 소비자 의존성 오염 방지)
61
+
62
+ ---
63
+
64
+ ## [0.1.2] — 2026-03-19
65
+
66
+ ### Changed
67
+ - `package.json` / `package-lock.json` — 버전 범프 (0.1.1 → 0.1.2)
68
+
69
+ ---
70
+
71
+ ## [0.1.1] — 2026-03-19
72
+
73
+ ### Added (feat: add schema selection for database connection)
74
+ - `defineTable` — PostgreSQL 스키마 지원 추가
75
+ - `defineTable('audit.activity_logs', cols)` — dot 표기법
76
+ - `defineTable('users', cols, { schema: 'auth' })` — 명시적 옵션
77
+ - `qualifiedName` 자동 생성 (`"auth"."users"` 형태로 SQL 식별자 인용)
78
+ - `TableOpts` 인터페이스 추가 (`{ schema?: string }`)
79
+ - `TableDef` — `qualifiedName` 필드 추가
80
+ - `src/index.ts` — `TableOpts` re-export 추가
81
+
82
+ ### Changed
83
+ - `package.json` — `pg` → `peerDependencies` 이동, `express` / `nodemon` / `typescript` → `devDependencies` 이동
84
+ - `main`, `types`, `exports`, `files`, `keywords`, `repository`, `engines` 필드 추가로 npm 배포 최적화
85
+ - `scripts` — `typecheck`, `prepublishOnly` 추가
86
+
87
+ ---
88
+
89
+ ## [0.1.0] — 2026-03-19
10
90
 
11
91
  ### Added
12
92
 
@@ -22,7 +102,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
22
102
  - `.exec()` — 쿼리 실행 → `T[]`
23
103
  - `.one()` — 단건 실행 → `T | null`
24
104
  - `.calculate(fns)` — COUNT / SUM / AVG / MIN / MAX 집계
25
- - `.paginate(opts)` — OFFSET 기반 페이지네이션 (COUNT + DATA 병렬 실행)
105
+ - `.paginate(opts)` — OFFSET 기반 페이지네이션
26
106
  - `.toSQL()` — 생성될 SQL 미리 확인 (디버깅용)
27
107
  - `QueryBuilder.raw(sql, params?)` — 독립 Raw SQL 실행
28
108
 
@@ -53,7 +133,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
53
133
 
54
134
  ---
55
135
 
56
- ## [0.0.1] — Initial Release
136
+ ## [0.0.1] — 2026-02-13
57
137
 
58
138
  ### Added
59
139
  - `defineTable` / `col` — 테이블 스키마 정의 및 타입 추론 (`InferRow`, `InferInsert`, `InferUpdate`)
@@ -1,13 +1,20 @@
1
1
  import { Pool, PoolClient, PoolConfig } from 'pg';
2
2
  /** Pool 상태 스냅샷 */
3
3
  export interface PoolStatus {
4
+ /** Pool이 초기화되어 있는지 여부 */
5
+ isInitialized: boolean;
4
6
  /** 총 생성된 연결 수 */
5
7
  totalCount: number;
6
8
  /** 현재 유휴 연결 수 */
7
9
  idleCount: number;
8
10
  /** 연결 대기 중인 요청 수 */
9
11
  waitingCount: number;
10
- /** Pool이 정상 상태인지 여부 */
12
+ /**
13
+ * Pool이 정상 상태인지 여부.
14
+ * - `isInitialized === false` → false
15
+ * - `waitingCount > 0 && idleCount === 0` → false (pool 소진)
16
+ * - 그 외 → true
17
+ */
11
18
  isHealthy: boolean;
12
19
  }
13
20
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../../src/features/connection/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAUlD,kBAAkB;AAClB,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAOD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAuDjD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CASxD;AAED;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAM/C"}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../../../src/features/connection/pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAUlD,kBAAkB;AAClB,MAAM,WAAW,UAAU;IACzB,yBAAyB;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAOD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAuDjD;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAChC,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAgBZ;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAW1C;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CASxD;AAED;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAM/C"}
@@ -91,7 +91,13 @@ async function withClient(fn) {
91
91
  */
92
92
  function getPoolStatus() {
93
93
  if (!_pool) {
94
- return { totalCount: 0, idleCount: 0, waitingCount: 0, isHealthy: true };
94
+ return {
95
+ isInitialized: false,
96
+ totalCount: 0,
97
+ idleCount: 0,
98
+ waitingCount: 0,
99
+ isHealthy: false,
100
+ };
95
101
  }
96
102
  return readPoolStatus(_pool);
97
103
  }
@@ -122,10 +128,13 @@ async function closePool() {
122
128
  logger.info('Pool 종료 완료');
123
129
  }
124
130
  function readPoolStatus(pool) {
131
+ const { totalCount, idleCount, waitingCount } = pool;
125
132
  return {
126
- totalCount: pool.totalCount,
127
- idleCount: pool.idleCount,
128
- waitingCount: pool.waitingCount,
129
- isHealthy: pool.idleCount > 0 || pool.waitingCount === 0,
133
+ isInitialized: true,
134
+ totalCount,
135
+ idleCount,
136
+ waitingCount,
137
+ // pool이 소진된 경우(대기 요청이 있는데 유휴 연결이 없음)에만 unhealthy
138
+ isHealthy: !(waitingCount > 0 && idleCount === 0),
130
139
  };
131
140
  }
@@ -72,8 +72,12 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
72
72
  groupBy(columns: Array<keyof T | string>): this;
73
73
  /**
74
74
  * JOIN 추가 (여러 번 호출 가능)
75
+ *
75
76
  * @example
76
77
  * .join({ table: 'orders', on: 'users.id = orders.user_id', type: 'LEFT' })
78
+ *
79
+ * @security `on` 절은 애플리케이션 코드에서 정적으로 구성해야 합니다.
80
+ * 사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.
77
81
  */
78
82
  join(j: JoinClause): this;
79
83
  /**
@@ -83,6 +87,7 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
83
87
  columns(cols: Array<keyof T | string>): this;
84
88
  /**
85
89
  * 쿼리 실행 라이프사이클 훅을 등록합니다.
90
+ * 전역 훅(`useHooks`)이 설정되어 있으면 per-query 훅이 우선합니다.
86
91
  *
87
92
  * @example
88
93
  * ```ts
@@ -103,6 +108,7 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
103
108
  exec(): Promise<T[]>;
104
109
  /**
105
110
  * 첫 번째 row 하나를 반환합니다. 없으면 null입니다.
111
+ * 내부적으로 clone()을 사용하여 원본 builder 상태를 변경하지 않습니다.
106
112
  */
107
113
  one(): Promise<T | null>;
108
114
  /**
@@ -121,7 +127,7 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
121
127
  calculate(fns: AggregateCalc[]): Promise<Record<string, unknown>>;
122
128
  /**
123
129
  * OFFSET 기반 페이지네이션.
124
- * COUNT + DATA 쿼리를 병렬로 실행합니다.
130
+ * COUNT + DATA 쿼리를 같은 트랜잭션 안에서 순차 실행합니다.
125
131
  *
126
132
  * > 수백만 건 이상의 테이블에서는 `cursorPaginate()`를 사용하세요.
127
133
  *
@@ -161,7 +167,7 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
161
167
  *
162
168
  * 대용량 테이블의 일괄 처리(ETL, 이메일 발송, 마이그레이션 등)에 적합합니다.
163
169
  *
164
- * @param fn - 배치 배열을 받아 처리하는 비동기 함수
170
+ * @param fn - 배치 배열을 받아 처리하는 비동기 함수
165
171
  * @param opts.batchSize - 한 번에 처리할 row 수 (기본값: 500)
166
172
  *
167
173
  * @example
@@ -235,7 +241,8 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
235
241
  static raw<R extends Record<string, unknown> = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<R[]>;
236
242
  /**
237
243
  * builder 상태를 독립적으로 복사합니다.
238
- * stream/forEach 내부에서 LIMIT/OFFSET을 덮어쓸 때 원본을 보호하기 위해 사용합니다.
244
+ * `one()`, `cursorPaginate()`, `stream()`, `forEach()` 내부에서
245
+ * LIMIT/OFFSET 등 상태를 임시 변경할 때 원본을 보호하기 위해 사용합니다.
239
246
  */
240
247
  clone(): QueryBuilder<T>;
241
248
  then<R>(onfulfilled: ((value: T[]) => R | PromiseLike<R>) | null | undefined, onrejected?: ((reason: unknown) => R | PromiseLike<R>) | null | undefined): Promise<R>;
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/features/query/builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/features/query/builder.ts"],"names":[],"mappings":"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"}
@@ -145,8 +145,12 @@ class QueryBuilder {
145
145
  }
146
146
  /**
147
147
  * JOIN 추가 (여러 번 호출 가능)
148
+ *
148
149
  * @example
149
150
  * .join({ table: 'orders', on: 'users.id = orders.user_id', type: 'LEFT' })
151
+ *
152
+ * @security `on` 절은 애플리케이션 코드에서 정적으로 구성해야 합니다.
153
+ * 사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.
150
154
  */
151
155
  join(j) {
152
156
  this._joins.push(j);
@@ -162,6 +166,7 @@ class QueryBuilder {
162
166
  }
163
167
  /**
164
168
  * 쿼리 실행 라이프사이클 훅을 등록합니다.
169
+ * 전역 훅(`useHooks`)이 설정되어 있으면 per-query 훅이 우선합니다.
165
170
  *
166
171
  * @example
167
172
  * ```ts
@@ -189,17 +194,11 @@ class QueryBuilder {
189
194
  }
190
195
  /**
191
196
  * 첫 번째 row 하나를 반환합니다. 없으면 null입니다.
197
+ * 내부적으로 clone()을 사용하여 원본 builder 상태를 변경하지 않습니다.
192
198
  */
193
199
  async one() {
194
- const saved = this._limitVal;
195
- this._limitVal = 1;
196
- try {
197
- const rows = await this.exec();
198
- return rows[0] ?? null;
199
- }
200
- finally {
201
- this._limitVal = saved;
202
- }
200
+ const rows = await this.clone().limit(1).exec();
201
+ return rows[0] ?? null;
203
202
  }
204
203
  /**
205
204
  * 집계 함수를 실행합니다.
@@ -233,7 +232,7 @@ class QueryBuilder {
233
232
  }
234
233
  /**
235
234
  * OFFSET 기반 페이지네이션.
236
- * COUNT + DATA 쿼리를 병렬로 실행합니다.
235
+ * COUNT + DATA 쿼리를 같은 트랜잭션 안에서 순차 실행합니다.
237
236
  *
238
237
  * > 수백만 건 이상의 테이블에서는 `cursorPaginate()`를 사용하세요.
239
238
  *
@@ -254,32 +253,32 @@ class QueryBuilder {
254
253
  `SELECT COUNT(*) AS count FROM ${this._table}`,
255
254
  joinSQL, whereSQL, groupSQL,
256
255
  ].filter(Boolean).join(' ');
257
- const dataParams = [...whereParams];
258
- dataParams.push(pageSize);
259
- const limitIdx = dataParams.length;
260
- dataParams.push((page - 1) * pageSize);
261
- const offsetIdx = dataParams.length;
256
+ // LIMIT/OFFSET 파라미터를 whereParams 뒤에 이어붙임
257
+ const dataParams = [...whereParams, pageSize, (page - 1) * pageSize];
258
+ const limitIdx = whereParams.length + 1;
259
+ const offsetIdx = whereParams.length + 2;
262
260
  const dataSql = [
263
261
  `SELECT ${this._cols} FROM ${this._table}`,
264
262
  joinSQL, whereSQL, groupSQL, orderSQL,
265
263
  `LIMIT $${limitIdx} OFFSET $${offsetIdx}`,
266
264
  ].filter(Boolean).join(' ');
267
265
  try {
266
+ // 같은 클라이언트로 순차 실행하여 커넥션 절약
268
267
  return await (0, pool_1.withClient)(async (client) => {
269
268
  if (this._execHooks?.beforeExec) {
270
269
  await this._execHooks.beforeExec({ sql: dataSql, params: dataParams });
271
270
  }
272
271
  const start = Date.now();
273
- logger.debug(`COUNT SQL: ${countSql}`, whereParams);
274
- logger.debug(`DATA SQL: ${dataSql}`, dataParams);
275
- const [countResult, dataResult] = await Promise.all([
276
- client.query(countSql, whereParams),
277
- client.query(dataSql, dataParams),
278
- ]);
272
+ logger.debug(`PAGINATE COUNT SQL: ${countSql}`, whereParams);
273
+ logger.debug(`PAGINATE DATA SQL: ${dataSql}`, dataParams);
274
+ const countResult = await client.query(countSql, whereParams);
275
+ const dataResult = await client.query(dataSql, dataParams);
279
276
  const total = parseInt(String(countResult.rows[0].count ?? '0'), 10);
280
277
  const data = (0, mapper_1.mapRows)(dataResult.rows);
278
+ const elapsed = Date.now() - start;
279
+ logger.debug(`PAGINATE 완료 (${elapsed}ms) total=${total}`);
281
280
  if (this._execHooks?.afterExec) {
282
- await this._execHooks.afterExec({ rows: data, elapsed: Date.now() - start, sql: dataSql });
281
+ await this._execHooks.afterExec({ rows: data, elapsed, sql: dataSql });
283
282
  }
284
283
  return {
285
284
  data,
@@ -324,17 +323,17 @@ class QueryBuilder {
324
323
  async cursorPaginate(opts) {
325
324
  const { pageSize, cursor, cursorColumn, direction = 'asc' } = opts;
326
325
  const colSnake = (0, case_1.toSnake)(cursorColumn);
327
- // 커서 값 디코딩
328
326
  let cursorValue;
329
327
  if (cursor) {
330
328
  try {
331
329
  cursorValue = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
332
330
  }
333
331
  catch {
334
- throw new Error(`유효하지 않은 cursor 값입니다: ${cursor}`);
332
+ logger.error(`cursorPaginate [${this._table}]: 유효하지 않은 cursor 토큰입니다. 이전 페이지 응답의 nextCursor 값을 그대로 사용하세요.`);
333
+ return { data: [], nextCursor: null, pageSize, hasNext: false };
335
334
  }
336
335
  }
337
- // 기존 조건에 커서 조건 추가 (원본 builder를 변경하지 않음)
336
+ // 원본 builder를 변경하지 않고 clone으로 처리
338
337
  const qb = this.clone();
339
338
  if (cursorValue !== undefined) {
340
339
  qb._andConds.push({
@@ -349,7 +348,6 @@ class QueryBuilder {
349
348
  const rows = await qb.exec();
350
349
  const hasNext = rows.length > pageSize;
351
350
  const data = hasNext ? rows.slice(0, pageSize) : rows;
352
- // 다음 커서 인코딩 (마지막 row의 cursorColumn 값)
353
351
  let nextCursor = null;
354
352
  if (hasNext && data.length > 0) {
355
353
  const lastRow = data[data.length - 1];
@@ -363,7 +361,7 @@ class QueryBuilder {
363
361
  *
364
362
  * 대용량 테이블의 일괄 처리(ETL, 이메일 발송, 마이그레이션 등)에 적합합니다.
365
363
  *
366
- * @param fn - 배치 배열을 받아 처리하는 비동기 함수
364
+ * @param fn - 배치 배열을 받아 처리하는 비동기 함수
367
365
  * @param opts.batchSize - 한 번에 처리할 row 수 (기본값: 500)
368
366
  *
369
367
  * @example
@@ -499,7 +497,8 @@ class QueryBuilder {
499
497
  }
500
498
  /**
501
499
  * builder 상태를 독립적으로 복사합니다.
502
- * stream/forEach 내부에서 LIMIT/OFFSET을 덮어쓸 때 원본을 보호하기 위해 사용합니다.
500
+ * `one()`, `cursorPaginate()`, `stream()`, `forEach()` 내부에서
501
+ * LIMIT/OFFSET 등 상태를 임시 변경할 때 원본을 보호하기 위해 사용합니다.
503
502
  */
504
503
  clone() {
505
504
  const c = new QueryBuilder(this._table);
@@ -1,10 +1,13 @@
1
1
  import { BuiltQuery } from './interfaces/Query';
2
2
  /**
3
3
  * 여러 row를 한 번의 INSERT 쿼리로 삽입합니다.
4
- * 빈 배열이 전달되면 빈 BuiltQuery를 반환합니다.
5
4
  *
6
- * @param table - 테이블명 (snake_case)
7
- * @param rows - 삽입할 데이터 배열 (camelCase key, 모든 row는 동일한 key 구조여야 합니다)
5
+ * @param table - 테이블명
6
+ * @param rows - 삽입할 데이터 배열 (camelCase key)
7
+ * 모든 row가 동일한 key 구조를 가져야 합니다.
8
+ * 첫 번째 row의 key 목록을 기준으로 컬럼을 결정합니다.
9
+ *
10
+ * rows가 빈 배열이거나 첫 row의 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
8
11
  */
9
12
  export declare function buildBulkInsert(table: string, rows: Record<string, unknown>[]): BuiltQuery;
10
13
  //# sourceMappingURL=bulkInsert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bulkInsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/bulkInsert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,UAAU,CAqBZ"}
1
+ {"version":3,"file":"bulkInsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/bulkInsert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,UAAU,CA4BZ"}
@@ -2,23 +2,33 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildBulkInsert = buildBulkInsert;
4
4
  const case_1 = require("../transform/case");
5
+ const logger_1 = require("../../utils/logger");
6
+ const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
5
7
  /**
6
8
  * 여러 row를 한 번의 INSERT 쿼리로 삽입합니다.
7
- * 빈 배열이 전달되면 빈 BuiltQuery를 반환합니다.
8
9
  *
9
- * @param table - 테이블명 (snake_case)
10
- * @param rows - 삽입할 데이터 배열 (camelCase key, 모든 row는 동일한 key 구조여야 합니다)
10
+ * @param table - 테이블명
11
+ * @param rows - 삽입할 데이터 배열 (camelCase key)
12
+ * 모든 row가 동일한 key 구조를 가져야 합니다.
13
+ * 첫 번째 row의 key 목록을 기준으로 컬럼을 결정합니다.
14
+ *
15
+ * rows가 빈 배열이거나 첫 row의 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
11
16
  */
12
17
  function buildBulkInsert(table, rows) {
13
18
  if (rows.length === 0) {
19
+ logger.error(`buildBulkInsert [${table}]: 삽입할 row가 없습니다.`);
14
20
  return { sql: '', params: [] };
15
21
  }
16
22
  const keys = Object.keys(rows[0]).filter((k) => rows[0][k] !== undefined);
23
+ if (keys.length === 0) {
24
+ logger.error(`buildBulkInsert [${table}]: 첫 번째 row에 삽입할 데이터가 없습니다.`);
25
+ return { sql: '', params: [] };
26
+ }
17
27
  const cols = keys.map(case_1.toSnake).join(', ');
18
28
  const params = [];
19
29
  const valueSets = rows.map((row) => {
20
30
  const placeholders = keys.map((k) => {
21
- params.push(row[k]);
31
+ params.push(row[k] ?? null);
22
32
  return `$${params.length}`;
23
33
  });
24
34
  return `(${placeholders.join(', ')})`;
@@ -1,8 +1,10 @@
1
1
  import { BuiltQuery } from './interfaces/Query';
2
2
  import { WhereInput } from './interfaces/Where';
3
3
  /**
4
- * DELETE 쿼리를 생성합니다.
4
+ * DELETE 쿼리를 생성합니다. RETURNING * 으로 삭제된 row를 반환합니다.
5
5
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
6
+ *
7
+ * WHERE 조건이 없으면 빈 쿼리를 반환합니다 (전체 삭제 방지, 실행 시 no-op).
6
8
  */
7
9
  export declare function buildDelete<T extends Record<string, unknown>>(table: string, where: WhereInput<T>): BuiltQuery;
8
10
  //# sourceMappingURL=delete.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/features/query/delete.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GACnB,UAAU,CAOZ"}
1
+ {"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/features/query/delete.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GACnB,UAAU,CAcZ"}
@@ -2,14 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildDelete = buildDelete;
4
4
  const where_1 = require("./where");
5
+ const logger_1 = require("../../utils/logger");
6
+ const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
5
7
  /**
6
- * DELETE 쿼리를 생성합니다.
8
+ * DELETE 쿼리를 생성합니다. RETURNING * 으로 삭제된 row를 반환합니다.
7
9
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
10
+ *
11
+ * WHERE 조건이 없으면 빈 쿼리를 반환합니다 (전체 삭제 방지, 실행 시 no-op).
8
12
  */
9
13
  function buildDelete(table, where) {
14
+ const whereEntries = Object.entries(where).filter(([, v]) => v !== undefined);
15
+ if (whereEntries.length === 0) {
16
+ logger.error(`buildDelete [${table}]: WHERE 조건이 없습니다. 전체 행 삭제를 방지합니다.`);
17
+ return { sql: '', params: [] };
18
+ }
10
19
  const { sql: whereSql, params } = (0, where_1.buildWhere)(where);
11
20
  return {
12
- sql: [`DELETE FROM ${table}`, whereSql].filter(Boolean).join(' '),
21
+ sql: [`DELETE FROM ${table}`, whereSql, 'RETURNING *'].filter(Boolean).join(' '),
13
22
  params,
14
23
  };
15
24
  }
@@ -2,6 +2,8 @@ import { BuiltQuery } from './interfaces/Query';
2
2
  /**
3
3
  * INSERT 쿼리를 생성합니다. RETURNING * 으로 삽입된 row를 반환합니다.
4
4
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
5
+ *
6
+ * 삽입할 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
5
7
  */
6
8
  export declare function buildInsert(table: string, data: Record<string, unknown>): BuiltQuery;
7
9
  //# sourceMappingURL=insert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/features/query/insert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,UAAU,CAWZ"}
1
+ {"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/features/query/insert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,UAAU,CAgBZ"}
@@ -2,12 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildInsert = buildInsert;
4
4
  const case_1 = require("../transform/case");
5
+ const logger_1 = require("../../utils/logger");
6
+ const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
5
7
  /**
6
8
  * INSERT 쿼리를 생성합니다. RETURNING * 으로 삽입된 row를 반환합니다.
7
9
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
10
+ *
11
+ * 삽입할 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
8
12
  */
9
13
  function buildInsert(table, data) {
10
14
  const entries = Object.entries(data).filter(([, v]) => v !== undefined);
15
+ if (entries.length === 0) {
16
+ logger.error(`buildInsert [${table}]: 삽입할 데이터가 없습니다.`);
17
+ return { sql: '', params: [] };
18
+ }
11
19
  const cols = entries.map(([k]) => (0, case_1.toSnake)(k)).join(', ');
12
20
  const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
13
21
  const params = entries.map(([, v]) => v);
@@ -3,6 +3,8 @@ import { WhereInput } from './interfaces/Where';
3
3
  /**
4
4
  * UPDATE 쿼리를 생성합니다. RETURNING * 으로 수정된 row를 반환합니다.
5
5
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
6
+ *
7
+ * 수정할 데이터가 없거나 WHERE 조건이 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
6
8
  */
7
9
  export declare function buildUpdate<T extends Record<string, unknown>>(table: string, data: Partial<T>, where: WhereInput<T>): BuiltQuery;
8
10
  //# sourceMappingURL=update.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/features/query/update.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GACnB,UAAU,CAsBZ"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/features/query/update.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAChB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,GACnB,UAAU,CAiCZ"}
@@ -3,12 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildUpdate = buildUpdate;
4
4
  const case_1 = require("../transform/case");
5
5
  const where_1 = require("./where");
6
+ const logger_1 = require("../../utils/logger");
7
+ const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
6
8
  /**
7
9
  * UPDATE 쿼리를 생성합니다. RETURNING * 으로 수정된 row를 반환합니다.
8
10
  * camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
11
+ *
12
+ * 수정할 데이터가 없거나 WHERE 조건이 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
9
13
  */
10
14
  function buildUpdate(table, data, where) {
11
15
  const entries = Object.entries(data).filter(([, v]) => v !== undefined);
16
+ const whereEntries = Object.entries(where).filter(([, v]) => v !== undefined);
17
+ if (entries.length === 0) {
18
+ logger.error(`buildUpdate [${table}]: 수정할 데이터가 없습니다.`);
19
+ return { sql: '', params: [] };
20
+ }
21
+ if (whereEntries.length === 0) {
22
+ logger.error(`buildUpdate [${table}]: WHERE 조건이 없습니다. 전체 행 업데이트를 방지합니다.`);
23
+ return { sql: '', params: [] };
24
+ }
12
25
  const params = [];
13
26
  const setClauses = entries.map(([k, v]) => {
14
27
  params.push(v);
@@ -3,9 +3,11 @@ import { BuiltQuery } from './interfaces/Query';
3
3
  * INSERT ... ON CONFLICT DO UPDATE 쿼리를 생성합니다.
4
4
  * 충돌 컬럼을 제외한 나머지 컬럼을 EXCLUDED 값으로 업데이트합니다.
5
5
  *
6
- * @param table - 테이블명 (snake_case)
6
+ * @param table - 테이블명
7
7
  * @param data - 삽입할 데이터 (camelCase key)
8
8
  * @param conflictCol - 충돌 기준 컬럼명 (snake_case)
9
+ *
10
+ * 삽입할 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
9
11
  */
10
12
  export declare function buildUpsert(table: string, data: Record<string, unknown>, conflictCol: string): BuiltQuery;
11
13
  //# sourceMappingURL=upsert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/upsert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE,MAAM,GAClB,UAAU,CAqBZ"}
1
+ {"version":3,"file":"upsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/upsert.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE,MAAM,GAClB,UAAU,CAqCZ"}
@@ -2,16 +2,24 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildUpsert = buildUpsert;
4
4
  const case_1 = require("../transform/case");
5
+ const logger_1 = require("../../utils/logger");
6
+ const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
5
7
  /**
6
8
  * INSERT ... ON CONFLICT DO UPDATE 쿼리를 생성합니다.
7
9
  * 충돌 컬럼을 제외한 나머지 컬럼을 EXCLUDED 값으로 업데이트합니다.
8
10
  *
9
- * @param table - 테이블명 (snake_case)
11
+ * @param table - 테이블명
10
12
  * @param data - 삽입할 데이터 (camelCase key)
11
13
  * @param conflictCol - 충돌 기준 컬럼명 (snake_case)
14
+ *
15
+ * 삽입할 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
12
16
  */
13
17
  function buildUpsert(table, data, conflictCol) {
14
18
  const entries = Object.entries(data).filter(([, v]) => v !== undefined);
19
+ if (entries.length === 0) {
20
+ logger.error(`buildUpsert [${table}]: 삽입할 데이터가 없습니다.`);
21
+ return { sql: '', params: [] };
22
+ }
15
23
  const cols = entries.map(([k]) => (0, case_1.toSnake)(k)).join(', ');
16
24
  const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
17
25
  const params = entries.map(([, v]) => v);
@@ -20,6 +28,16 @@ function buildUpsert(table, data, conflictCol) {
20
28
  .filter((col) => col !== conflictCol)
21
29
  .map((col) => `${col} = EXCLUDED.${col}`)
22
30
  .join(', ');
31
+ if (!updateSet) {
32
+ return {
33
+ sql: [
34
+ `INSERT INTO ${table} (${cols}) VALUES (${placeholders})`,
35
+ `ON CONFLICT (${conflictCol}) DO NOTHING`,
36
+ 'RETURNING *',
37
+ ].join(' '),
38
+ params,
39
+ };
40
+ }
23
41
  return {
24
42
  sql: [
25
43
  `INSERT INTO ${table} (${cols}) VALUES (${placeholders})`,
@@ -44,6 +44,10 @@ export declare class BaseRepo<TDef extends TableDef<string, Cols>> implements IR
44
44
  findOne(where: Partial<InferRow<TDef>>): Promise<InferRow<TDef> | null>;
45
45
  create(data: InferInsert<TDef>): Promise<InferRow<TDef>>;
46
46
  update(id: number | string, data: InferUpdate<TDef>): Promise<InferRow<TDef> | null>;
47
+ /**
48
+ * ID로 단건 삭제합니다.
49
+ * `exec()`를 재사용하여 로깅과 에러 처리를 일관성 있게 처리합니다.
50
+ */
47
51
  delete(id: number | string): Promise<boolean>;
48
52
  /**
49
53
  * 데이터를 삽입하거나, 충돌 시 UPDATE합니다.
@@ -52,41 +56,33 @@ export declare class BaseRepo<TDef extends TableDef<string, Cols>> implements IR
52
56
  upsert(data: InferInsert<TDef>, conflictCol?: string): Promise<InferRow<TDef>>;
53
57
  /**
54
58
  * 여러 row를 단일 INSERT 쿼리로 삽입합니다.
59
+ *
60
+ * @throws rows가 빈 배열이면 Error를 던집니다.
55
61
  */
56
62
  bulkCreate(rows: InferInsert<TDef>[]): Promise<InferRow<TDef>[]>;
57
63
  /**
58
64
  * 유연한 플루언트 쿼리 빌더를 반환합니다.
59
- *
60
- * WHERE, OR, ORDER BY, GROUP BY, JOIN, LIMIT, OFFSET, paginate, calculate 등을
61
- * 메서드 체인으로 조합할 수 있습니다.
65
+ * 전역 훅(`useHooks`)이 설정된 경우 빌더에 자동으로 주입됩니다.
62
66
  *
63
67
  * @example
64
68
  * ```ts
65
- * // 기본 조회 (await 직접 사용 가능)
66
69
  * const users = await repo.select({ isActive: true })
67
70
  * .orderBy([{ column: 'createdAt', direction: 'DESC' }])
68
71
  * .limit(20);
69
72
  *
70
- * // OR 조건
71
- * const results = await repo.select()
72
- * .or({ email: { operator: 'LIKE', value: '%@gmail.com' } });
73
- *
74
- * // 페이지네이션
75
73
  * const page = await repo.select()
76
74
  * .paginate({ page: 1, pageSize: 20 });
77
- *
78
- * // 집계
79
- * const agg = await repo.select({ isActive: true })
80
- * .calculate([{ fn: 'COUNT', alias: 'count' }]);
81
75
  * ```
82
76
  */
83
77
  select(where?: AdvancedWhere<InferRow<TDef>>): QueryBuilder<InferRow<TDef>>;
84
78
  /**
85
79
  * 단건 조회 (없으면 null). `select(where).one()` 의 단축형입니다.
80
+ * 전역 훅이 적용됩니다.
86
81
  *
87
82
  * @example
88
83
  * ```ts
89
84
  * const user = await repo.selectOne({ id: 1 });
85
+ * const user = await repo.selectOne({ email: 'foo@bar.com' });
90
86
  * ```
91
87
  */
92
88
  selectOne(where: AdvancedWhere<InferRow<TDef>>): Promise<InferRow<TDef> | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/features/repository/base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/features/repository/base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAOhF,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAMxE,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAwB7C;;;;;;GAMG;AACH,qBAAa,QAAQ,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CACvD,YAAW,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IAQ1D,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI;IANxC,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IACrC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACjC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEjC,OAAO,CAAC,YAAY,CAAC,CAA4B;gBAElB,GAAG,EAAE,IAAI;IAMxC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI;IAK5C;;;;;OAKG;cACa,IAAI,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpD,KAAK,EAAE,UAAU,EACjB,MAAM,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,CAAC,EAAE,CAAC;IA6BT,OAAO,CAAC,IAAI,GAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAUvE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAc7D,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAWvE,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAWxD,MAAM,CACV,EAAE,EAAE,MAAM,GAAG,MAAM,EACnB,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,GACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAgBjC;;;OAGG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYnD;;;OAGG;IACG,MAAM,CACV,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,EACvB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAe1B;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IAetE;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAM3E;;;;;;;;;OASG;IACG,SAAS,CACb,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAQjC;;;;;;;;;;;OAWG;IACG,GAAG,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,CAAC,EAAE,CAAC;CAGhB"}
@@ -14,10 +14,19 @@ const pool_1 = require("../connection/pool");
14
14
  const logger_1 = require("../../utils/logger");
15
15
  const dbError_1 = require("../../utils/dbError");
16
16
  const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Repo]' });
17
- /** 런타임에서 primary key의 camelCase 키를 찾습니다. */
18
- function findPkKey(cols) {
17
+ /**
18
+ * 런타임에서 primary key의 camelCase 키를 찾습니다.
19
+ * primary key가 없으면 경고를 출력하고 'id'를 기본값으로 사용합니다.
20
+ */
21
+ function findPkKey(cols, tableName) {
19
22
  const entry = Object.entries(cols).find(([, c]) => c.isPrimary);
20
- return entry ? entry[0] : 'id';
23
+ if (!entry) {
24
+ logger.warn(`[Repo] '${tableName}' 테이블에 primary key가 정의되지 않았습니다. ` +
25
+ `findById, delete 등에서 'id' 컬럼을 기본값으로 사용합니다. ` +
26
+ `col.xxx().primaryKey() 로 명시적으로 지정하세요.`);
27
+ return 'id';
28
+ }
29
+ return entry[0];
21
30
  }
22
31
  /**
23
32
  * 기본 CRUD 레포지토리.
@@ -29,9 +38,8 @@ function findPkKey(cols) {
29
38
  class BaseRepo {
30
39
  constructor(def) {
31
40
  this.def = def;
32
- // qualifiedName이 있으면 사용 ("schema"."table"), 없으면 name 사용
33
41
  this.tableName = def.qualifiedName ?? def.name;
34
- this.pkKey = findPkKey(def.cols);
42
+ this.pkKey = findPkKey(def.cols, this.tableName);
35
43
  this.pkCol = (0, case_1.toSnake)(this.pkKey);
36
44
  }
37
45
  /**
@@ -56,6 +64,10 @@ class BaseRepo {
56
64
  * - `client`를 넘기면 해당 client를 재사용합니다 (트랜잭션 지원).
57
65
  */
58
66
  async exec(built, client) {
67
+ if (!built.sql) {
68
+ logger.error(`[${this.tableName}] 빈 SQL이 전달되었습니다. 실행을 건너뜁니다.`);
69
+ return [];
70
+ }
59
71
  const run = async (c) => {
60
72
  const start = Date.now();
61
73
  logger.debug(`SQL: ${built.sql}`, built.params);
@@ -126,27 +138,15 @@ class BaseRepo {
126
138
  throw dbError_1.DbError.from(err);
127
139
  }
128
140
  }
141
+ /**
142
+ * ID로 단건 삭제합니다.
143
+ * `exec()`를 재사용하여 로깅과 에러 처리를 일관성 있게 처리합니다.
144
+ */
129
145
  async delete(id) {
130
146
  try {
131
147
  const where = { [this.pkKey]: id };
132
- const built = (0, delete_1.buildDelete)(this.tableName, where);
133
- return await (0, pool_1.withClient)(async (client) => {
134
- const start = Date.now();
135
- logger.debug(`SQL: ${built.sql}`, built.params);
136
- try {
137
- const result = await client.query(built.sql, built.params);
138
- logger.debug(`완료 (${Date.now() - start}ms)`);
139
- return (result.rowCount ?? 0) > 0;
140
- }
141
- catch (err) {
142
- const dbErr = dbError_1.DbError.from(err);
143
- logger.error(`삭제 실패 [${this.tableName}]`, {
144
- ...dbErr.toLogContext(),
145
- elapsed: `${Date.now() - start}ms`,
146
- });
147
- throw dbErr;
148
- }
149
- });
148
+ const rows = await this.exec((0, delete_1.buildDelete)(this.tableName, where));
149
+ return rows.length > 0;
150
150
  }
151
151
  catch (err) {
152
152
  throw dbError_1.DbError.from(err);
@@ -167,6 +167,8 @@ class BaseRepo {
167
167
  }
168
168
  /**
169
169
  * 여러 row를 단일 INSERT 쿼리로 삽입합니다.
170
+ *
171
+ * @throws rows가 빈 배열이면 Error를 던집니다.
170
172
  */
171
173
  async bulkCreate(rows) {
172
174
  try {
@@ -179,28 +181,16 @@ class BaseRepo {
179
181
  // ── Fluent QueryBuilder ────────────────────────────────────────────────────
180
182
  /**
181
183
  * 유연한 플루언트 쿼리 빌더를 반환합니다.
182
- *
183
- * WHERE, OR, ORDER BY, GROUP BY, JOIN, LIMIT, OFFSET, paginate, calculate 등을
184
- * 메서드 체인으로 조합할 수 있습니다.
184
+ * 전역 훅(`useHooks`)이 설정된 경우 빌더에 자동으로 주입됩니다.
185
185
  *
186
186
  * @example
187
187
  * ```ts
188
- * // 기본 조회 (await 직접 사용 가능)
189
188
  * const users = await repo.select({ isActive: true })
190
189
  * .orderBy([{ column: 'createdAt', direction: 'DESC' }])
191
190
  * .limit(20);
192
191
  *
193
- * // OR 조건
194
- * const results = await repo.select()
195
- * .or({ email: { operator: 'LIKE', value: '%@gmail.com' } });
196
- *
197
- * // 페이지네이션
198
192
  * const page = await repo.select()
199
193
  * .paginate({ page: 1, pageSize: 20 });
200
- *
201
- * // 집계
202
- * const agg = await repo.select({ isActive: true })
203
- * .calculate([{ fn: 'COUNT', alias: 'count' }]);
204
194
  * ```
205
195
  */
206
196
  select(where) {
@@ -211,15 +201,17 @@ class BaseRepo {
211
201
  }
212
202
  /**
213
203
  * 단건 조회 (없으면 null). `select(where).one()` 의 단축형입니다.
204
+ * 전역 훅이 적용됩니다.
214
205
  *
215
206
  * @example
216
207
  * ```ts
217
208
  * const user = await repo.selectOne({ id: 1 });
209
+ * const user = await repo.selectOne({ email: 'foo@bar.com' });
218
210
  * ```
219
211
  */
220
212
  async selectOne(where) {
221
213
  try {
222
- return await new builder_1.QueryBuilder(this.tableName, where).one();
214
+ return await this.select(where).one();
223
215
  }
224
216
  catch (err) {
225
217
  throw dbError_1.DbError.from(err);
@@ -13,25 +13,38 @@ export declare class Col<T = unknown, IsOpt extends boolean = false, IsPrimary e
13
13
  readonly hasDefault: boolean;
14
14
  readonly isPrimary: boolean;
15
15
  readonly length?: number | undefined;
16
+ /** DEFAULT NOW() 여부 — SQL 힌트 용도 */
17
+ readonly isDefaultNow: boolean;
16
18
  readonly _type: T;
17
19
  readonly _isOpt: IsOpt;
18
20
  readonly _isPrimary: IsPrimary;
19
- constructor(pgType: string, isNullable?: boolean, hasDefault?: boolean, isPrimary?: boolean, length?: number | undefined);
21
+ constructor(pgType: string, isNullable?: boolean, hasDefault?: boolean, isPrimary?: boolean, length?: number | undefined,
22
+ /** DEFAULT NOW() 여부 — SQL 힌트 용도 */
23
+ isDefaultNow?: boolean);
20
24
  /** 컬럼을 nullable로 설정합니다. (INSERT도 optional) */
21
25
  nullable(): Col<T | null, true, IsPrimary>;
22
- /**
23
- * 런타임에서 INSERT optional 여부를 반환합니다.
24
- * isNullable, hasDefault, isPrimary 중 하나라도 true 이면 optional입니다.
25
- */
26
- get isOptional(): boolean;
27
26
  /** 컬럼을 NOT NULL로 설정합니다. */
28
27
  notNull(): Col<Exclude<T, null>, false, IsPrimary>;
29
28
  /** primary key로 설정합니다. (INSERT optional) */
30
29
  primaryKey(): Col<T, true, true>;
31
- /** DB 기본값이 있는 컬럼으로 설정합니다. (INSERT optional) */
30
+ /**
31
+ * DB 기본값이 있는 컬럼으로 설정합니다. (INSERT optional)
32
+ *
33
+ * @example col.boolean().default() // BOOLEAN DEFAULT false 등
34
+ */
32
35
  default(): Col<T, true, IsPrimary>;
33
- /** DEFAULT NOW() 컬럼으로 설정합니다. (INSERT optional) */
36
+ /**
37
+ * DEFAULT NOW() 컬럼으로 설정합니다. (INSERT optional)
38
+ * `default()`와 타입은 동일하나 `isDefaultNow: true`로 구분됩니다.
39
+ *
40
+ * @example col.timestamptz().defaultNow()
41
+ */
34
42
  defaultNow(): Col<T, true, IsPrimary>;
43
+ /**
44
+ * 런타임에서 INSERT optional 여부를 반환합니다.
45
+ * isNullable, hasDefault, isPrimary 중 하나라도 true이면 optional입니다.
46
+ */
47
+ get isOptional(): boolean;
35
48
  }
36
49
  /**
37
50
  * 컬럼 빌더 팩토리.
@@ -39,9 +52,11 @@ export declare class Col<T = unknown, IsOpt extends boolean = false, IsPrimary e
39
52
  * @example
40
53
  * ```ts
41
54
  * const usersTable = defineTable('users', {
42
- * id: col.serial().primaryKey(),
43
- * name: col.varchar(255).notNull(),
44
- * email: col.text().notNull(),
55
+ * id: col.serial().primaryKey(),
56
+ * name: col.varchar(255).notNull(),
57
+ * email: col.text().notNull(),
58
+ * isActive: col.boolean().default(),
59
+ * createdAt: col.timestamptz().defaultNow(),
45
60
  * });
46
61
  * ```
47
62
  */
@@ -1 +1 @@
1
- {"version":3,"file":"column.d.ts","sourceRoot":"","sources":["../../../src/features/schema/column.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C;;;;;;;GAOG;AACH,qBAAa,GAAG,CACd,CAAC,GAAG,OAAO,EACX,KAAK,SAAS,OAAO,GAAG,KAAK,EAC7B,SAAS,SAAS,OAAO,GAAG,KAAK,CACjC,YAAW,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC;IAMtC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,UAAU,EAAE,OAAO;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAT1B,SAAiB,KAAK,EAAE,CAAC,CAAC;IAC1B,SAAiB,MAAM,EAAE,KAAK,CAAC;IAC/B,SAAiB,UAAU,EAAE,SAAS,CAAC;gBAG5B,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,OAAe,EAC3B,UAAU,GAAE,OAAe,EAC3B,SAAS,GAAE,OAAe,EAC1B,MAAM,CAAC,EAAE,MAAM,YAAA;IAG1B,8CAA8C;IAC9C,QAAQ,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC;IAI1C;;;OAGG;IACH,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,2BAA2B;IAC3B,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC;IAIlD,4CAA4C;IAC5C,UAAU,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC;IAIhC,+CAA+C;IAC/C,OAAO,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;IAIlC,kDAAkD;IAClD,UAAU,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;CAGtC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,GAAG;;;;;oBAKM,MAAM;;;;;;;YAOZ,CAAC;CAChB,CAAC"}
1
+ {"version":3,"file":"column.d.ts","sourceRoot":"","sources":["../../../src/features/schema/column.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C;;;;;;;GAOG;AACH,qBAAa,GAAG,CACd,CAAC,GAAG,OAAO,EACX,KAAK,SAAS,OAAO,GAAG,KAAK,EAC7B,SAAS,SAAS,OAAO,GAAG,KAAK,CACjC,YAAW,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC;IAMtC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,UAAU,EAAE,OAAO;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IACxB,mCAAmC;IACnC,QAAQ,CAAC,YAAY,EAAE,OAAO;IAXhC,SAAiB,KAAK,EAAE,CAAC,CAAC;IAC1B,SAAiB,MAAM,EAAE,KAAK,CAAC;IAC/B,SAAiB,UAAU,EAAE,SAAS,CAAC;gBAG5B,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,OAAe,EAC3B,UAAU,GAAE,OAAe,EAC3B,SAAS,GAAE,OAAe,EAC1B,MAAM,CAAC,EAAE,MAAM,YAAA;IACxB,mCAAmC;IAC1B,YAAY,GAAE,OAAe;IAGxC,8CAA8C;IAC9C,QAAQ,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC;IAM1C,2BAA2B;IAC3B,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC;IAMlD,4CAA4C;IAC5C,UAAU,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC;IAMhC;;;;OAIG;IACH,OAAO,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;IAMlC;;;;;OAKG;IACH,UAAU,IAAI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC;IAMrC;;;OAGG;IACH,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,GAAG;;;;;oBAKM,MAAM;;;;;;;YAOZ,CAAC;CAChB,CAAC"}
@@ -10,39 +10,51 @@ exports.col = exports.Col = void 0;
10
10
  * - IsPrimary: primary key 여부
11
11
  */
12
12
  class Col {
13
- constructor(pgType, isNullable = false, hasDefault = false, isPrimary = false, length) {
13
+ constructor(pgType, isNullable = false, hasDefault = false, isPrimary = false, length,
14
+ /** DEFAULT NOW() 여부 — SQL 힌트 용도 */
15
+ isDefaultNow = false) {
14
16
  this.pgType = pgType;
15
17
  this.isNullable = isNullable;
16
18
  this.hasDefault = hasDefault;
17
19
  this.isPrimary = isPrimary;
18
20
  this.length = length;
21
+ this.isDefaultNow = isDefaultNow;
19
22
  }
20
23
  /** 컬럼을 nullable로 설정합니다. (INSERT도 optional) */
21
24
  nullable() {
22
- return new Col(this.pgType, true, this.hasDefault, this.isPrimary, this.length);
23
- }
24
- /**
25
- * 런타임에서 INSERT optional 여부를 반환합니다.
26
- * isNullable, hasDefault, isPrimary 중 하나라도 true 이면 optional입니다.
27
- */
28
- get isOptional() {
29
- return this.isNullable || this.hasDefault || this.isPrimary;
25
+ return new Col(this.pgType, true, this.hasDefault, this.isPrimary, this.length, this.isDefaultNow);
30
26
  }
31
27
  /** 컬럼을 NOT NULL로 설정합니다. */
32
28
  notNull() {
33
- return new Col(this.pgType, false, this.hasDefault, this.isPrimary, this.length);
29
+ return new Col(this.pgType, false, this.hasDefault, this.isPrimary, this.length, this.isDefaultNow);
34
30
  }
35
31
  /** primary key로 설정합니다. (INSERT optional) */
36
32
  primaryKey() {
37
- return new Col(this.pgType, this.isNullable, true, true, this.length);
33
+ return new Col(this.pgType, this.isNullable, true, true, this.length, this.isDefaultNow);
38
34
  }
39
- /** DB 기본값이 있는 컬럼으로 설정합니다. (INSERT optional) */
35
+ /**
36
+ * DB 기본값이 있는 컬럼으로 설정합니다. (INSERT optional)
37
+ *
38
+ * @example col.boolean().default() // BOOLEAN DEFAULT false 등
39
+ */
40
40
  default() {
41
- return new Col(this.pgType, this.isNullable, true, this.isPrimary, this.length);
41
+ return new Col(this.pgType, this.isNullable, true, this.isPrimary, this.length, false);
42
42
  }
43
- /** DEFAULT NOW() 컬럼으로 설정합니다. (INSERT optional) */
43
+ /**
44
+ * DEFAULT NOW() 컬럼으로 설정합니다. (INSERT optional)
45
+ * `default()`와 타입은 동일하나 `isDefaultNow: true`로 구분됩니다.
46
+ *
47
+ * @example col.timestamptz().defaultNow()
48
+ */
44
49
  defaultNow() {
45
- return new Col(this.pgType, this.isNullable, true, this.isPrimary, this.length);
50
+ return new Col(this.pgType, this.isNullable, true, this.isPrimary, this.length, true);
51
+ }
52
+ /**
53
+ * 런타임에서 INSERT optional 여부를 반환합니다.
54
+ * isNullable, hasDefault, isPrimary 중 하나라도 true이면 optional입니다.
55
+ */
56
+ get isOptional() {
57
+ return this.isNullable || this.hasDefault || this.isPrimary;
46
58
  }
47
59
  }
48
60
  exports.Col = Col;
@@ -52,9 +64,11 @@ exports.Col = Col;
52
64
  * @example
53
65
  * ```ts
54
66
  * const usersTable = defineTable('users', {
55
- * id: col.serial().primaryKey(),
56
- * name: col.varchar(255).notNull(),
57
- * email: col.text().notNull(),
67
+ * id: col.serial().primaryKey(),
68
+ * name: col.varchar(255).notNull(),
69
+ * email: col.text().notNull(),
70
+ * isActive: col.boolean().default(),
71
+ * createdAt: col.timestamptz().defaultNow(),
58
72
  * });
59
73
  * ```
60
74
  */
@@ -13,7 +13,14 @@ export type InferInsert<TDef extends TableDef<string, Cols>> = {
13
13
  } & {
14
14
  [K in keyof TDef['cols'] as TDef['cols'][K]['_isOpt'] extends true ? K : never]?: TDef['cols'][K]['_type'];
15
15
  };
16
- /** UPDATE 입력 타입: primary key 제외, 모두 optional */
16
+ /**
17
+ * UPDATE 입력 타입: primary key 제외, 모두 optional.
18
+ *
19
+ * 빈 객체(`{}`) 방지는 `buildUpdate`의 런타임 가드가 담당합니다.
20
+ *
21
+ * @example
22
+ * const update: InferUpdate<typeof usersTable> = { name: 'Alice' };
23
+ */
17
24
  export type InferUpdate<TDef extends TableDef<string, Cols>> = {
18
25
  [K in keyof TDef['cols'] as TDef['cols'][K]['_isPrimary'] extends true ? never : K]?: TDef['cols'][K]['_type'];
19
26
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Infer.d.ts","sourceRoot":"","sources":["../../../../src/features/schema/interfaces/Infer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEzC,6BAA6B;AAC7B,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI;KACzD,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CACpD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IACzD;KAAG,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAAE,GAC7G;KAAG,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAAE,CAAC;AAEjH,gDAAgD;AAChD,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI;KAC5D,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAC/G,CAAC"}
1
+ {"version":3,"file":"Infer.d.ts","sourceRoot":"","sources":["../../../../src/features/schema/interfaces/Infer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEzC,6BAA6B;AAC7B,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI;KACzD,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CACpD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IACzD;KAAG,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAAE,GAC7G;KAAG,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAAE,CAAC;AAEjH;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI;KAC5D,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;CAC/G,CAAC"}
@@ -1,11 +1,21 @@
1
1
  /**
2
2
  * snake_case 문자열을 camelCase로 변환합니다.
3
- * 예: "first_name" → "firstName"
3
+ *
4
+ * @example
5
+ * toCamel('first_name') // 'firstName'
6
+ * toCamel('user_id') // 'userId'
7
+ * toCamel('url_param') // 'urlParam'
4
8
  */
5
9
  export declare function toCamel(str: string): string;
6
10
  /**
7
11
  * camelCase 문자열을 snake_case로 변환합니다.
8
- * 예: "firstName" → "first_name"
12
+ * 두문자어(Acronym)도 올바르게 처리합니다.
13
+ *
14
+ * @example
15
+ * toSnake('firstName') // 'first_name'
16
+ * toSnake('userId') // 'user_id'
17
+ * toSnake('URLParam') // 'url_param'
18
+ * toSnake('userID') // 'user_id'
9
19
  */
10
20
  export declare function toSnake(str: string): string;
11
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"case.d.ts","sourceRoot":"","sources":["../../../src/features/transform/case.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjF"}
1
+ {"version":3,"file":"case.d.ts","sourceRoot":"","sources":["../../../src/features/transform/case.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3C;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIjF"}
@@ -6,17 +6,32 @@ exports.keysToCamel = keysToCamel;
6
6
  exports.keysToSnake = keysToSnake;
7
7
  /**
8
8
  * snake_case 문자열을 camelCase로 변환합니다.
9
- * 예: "first_name" → "firstName"
9
+ *
10
+ * @example
11
+ * toCamel('first_name') // 'firstName'
12
+ * toCamel('user_id') // 'userId'
13
+ * toCamel('url_param') // 'urlParam'
10
14
  */
11
15
  function toCamel(str) {
12
- return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
16
+ return str.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
13
17
  }
14
18
  /**
15
19
  * camelCase 문자열을 snake_case로 변환합니다.
16
- * 예: "firstName" → "first_name"
20
+ * 두문자어(Acronym)도 올바르게 처리합니다.
21
+ *
22
+ * @example
23
+ * toSnake('firstName') // 'first_name'
24
+ * toSnake('userId') // 'user_id'
25
+ * toSnake('URLParam') // 'url_param'
26
+ * toSnake('userID') // 'user_id'
17
27
  */
18
28
  function toSnake(str) {
19
- return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
29
+ return str
30
+ // 연속 대문자 뒤에 소문자가 오는 경우: "URLParam" → "URL_Param"
31
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
32
+ // 소문자/숫자 뒤에 대문자: "camelCase" → "camel_Case"
33
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
34
+ .toLowerCase();
20
35
  }
21
36
  /**
22
37
  * 객체의 모든 key를 camelCase로 변환합니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reltype",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Type-first relational modeling for PostgreSQL in TypeScript. Fluent query builder with automatic camelCase ↔ snake_case conversion, CRUD, streaming, cursor pagination, and hooks.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,12 +59,10 @@
59
59
  "peerDependencies": {
60
60
  "pg": ">=8.0.0"
61
61
  },
62
- "dependencies": {
63
- "dotenv": "^17.3.1"
64
- },
65
62
  "devDependencies": {
66
63
  "@types/express": "^4.17.25",
67
64
  "@types/pg": "^8.16.0",
65
+ "dotenv": "^17.3.1",
68
66
  "express": "^5.2.1",
69
67
  "nodemon": "^3.1.11",
70
68
  "pg": "^8.18.0",