reltype 0.1.2 → 0.1.4

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 (40) hide show
  1. package/dist/features/connection/pool.d.ts +8 -1
  2. package/dist/features/connection/pool.d.ts.map +1 -1
  3. package/dist/features/connection/pool.js +14 -5
  4. package/dist/features/query/builder.d.ts +10 -3
  5. package/dist/features/query/builder.d.ts.map +1 -1
  6. package/dist/features/query/builder.js +27 -28
  7. package/dist/features/query/bulkInsert.d.ts +6 -3
  8. package/dist/features/query/bulkInsert.d.ts.map +1 -1
  9. package/dist/features/query/bulkInsert.js +14 -4
  10. package/dist/features/query/delete.d.ts +3 -1
  11. package/dist/features/query/delete.d.ts.map +1 -1
  12. package/dist/features/query/delete.js +11 -2
  13. package/dist/features/query/insert.d.ts +2 -0
  14. package/dist/features/query/insert.d.ts.map +1 -1
  15. package/dist/features/query/insert.js +8 -0
  16. package/dist/features/query/update.d.ts +2 -0
  17. package/dist/features/query/update.d.ts.map +1 -1
  18. package/dist/features/query/update.js +13 -0
  19. package/dist/features/query/upsert.d.ts +3 -1
  20. package/dist/features/query/upsert.d.ts.map +1 -1
  21. package/dist/features/query/upsert.js +19 -1
  22. package/dist/features/repository/base.d.ts +9 -13
  23. package/dist/features/repository/base.d.ts.map +1 -1
  24. package/dist/features/repository/base.js +30 -37
  25. package/dist/features/schema/column.d.ts +26 -11
  26. package/dist/features/schema/column.d.ts.map +1 -1
  27. package/dist/features/schema/column.js +32 -18
  28. package/dist/features/schema/interfaces/Infer.d.ts +8 -1
  29. package/dist/features/schema/interfaces/Infer.d.ts.map +1 -1
  30. package/dist/features/schema/interfaces/Table.d.ts +18 -0
  31. package/dist/features/schema/interfaces/Table.d.ts.map +1 -1
  32. package/dist/features/schema/table.d.ts +35 -5
  33. package/dist/features/schema/table.d.ts.map +1 -1
  34. package/dist/features/schema/table.js +48 -6
  35. package/dist/features/transform/case.d.ts +12 -2
  36. package/dist/features/transform/case.d.ts.map +1 -1
  37. package/dist/features/transform/case.js +19 -4
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/package.json +2 -4
@@ -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":"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"}
@@ -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,8 +38,8 @@ function findPkKey(cols) {
29
38
  class BaseRepo {
30
39
  constructor(def) {
31
40
  this.def = def;
32
- this.tableName = def.name;
33
- this.pkKey = findPkKey(def.cols);
41
+ this.tableName = def.qualifiedName ?? def.name;
42
+ this.pkKey = findPkKey(def.cols, this.tableName);
34
43
  this.pkCol = (0, case_1.toSnake)(this.pkKey);
35
44
  }
36
45
  /**
@@ -55,6 +64,10 @@ class BaseRepo {
55
64
  * - `client`를 넘기면 해당 client를 재사용합니다 (트랜잭션 지원).
56
65
  */
57
66
  async exec(built, client) {
67
+ if (!built.sql) {
68
+ logger.error(`[${this.tableName}] 빈 SQL이 전달되었습니다. 실행을 건너뜁니다.`);
69
+ return [];
70
+ }
58
71
  const run = async (c) => {
59
72
  const start = Date.now();
60
73
  logger.debug(`SQL: ${built.sql}`, built.params);
@@ -125,27 +138,15 @@ class BaseRepo {
125
138
  throw dbError_1.DbError.from(err);
126
139
  }
127
140
  }
141
+ /**
142
+ * ID로 단건 삭제합니다.
143
+ * `exec()`를 재사용하여 로깅과 에러 처리를 일관성 있게 처리합니다.
144
+ */
128
145
  async delete(id) {
129
146
  try {
130
147
  const where = { [this.pkKey]: id };
131
- const built = (0, delete_1.buildDelete)(this.tableName, where);
132
- return await (0, pool_1.withClient)(async (client) => {
133
- const start = Date.now();
134
- logger.debug(`SQL: ${built.sql}`, built.params);
135
- try {
136
- const result = await client.query(built.sql, built.params);
137
- logger.debug(`완료 (${Date.now() - start}ms)`);
138
- return (result.rowCount ?? 0) > 0;
139
- }
140
- catch (err) {
141
- const dbErr = dbError_1.DbError.from(err);
142
- logger.error(`삭제 실패 [${this.tableName}]`, {
143
- ...dbErr.toLogContext(),
144
- elapsed: `${Date.now() - start}ms`,
145
- });
146
- throw dbErr;
147
- }
148
- });
148
+ const rows = await this.exec((0, delete_1.buildDelete)(this.tableName, where));
149
+ return rows.length > 0;
149
150
  }
150
151
  catch (err) {
151
152
  throw dbError_1.DbError.from(err);
@@ -166,6 +167,8 @@ class BaseRepo {
166
167
  }
167
168
  /**
168
169
  * 여러 row를 단일 INSERT 쿼리로 삽입합니다.
170
+ *
171
+ * @throws rows가 빈 배열이면 Error를 던집니다.
169
172
  */
170
173
  async bulkCreate(rows) {
171
174
  try {
@@ -178,28 +181,16 @@ class BaseRepo {
178
181
  // ── Fluent QueryBuilder ────────────────────────────────────────────────────
179
182
  /**
180
183
  * 유연한 플루언트 쿼리 빌더를 반환합니다.
181
- *
182
- * WHERE, OR, ORDER BY, GROUP BY, JOIN, LIMIT, OFFSET, paginate, calculate 등을
183
- * 메서드 체인으로 조합할 수 있습니다.
184
+ * 전역 훅(`useHooks`)이 설정된 경우 빌더에 자동으로 주입됩니다.
184
185
  *
185
186
  * @example
186
187
  * ```ts
187
- * // 기본 조회 (await 직접 사용 가능)
188
188
  * const users = await repo.select({ isActive: true })
189
189
  * .orderBy([{ column: 'createdAt', direction: 'DESC' }])
190
190
  * .limit(20);
191
191
  *
192
- * // OR 조건
193
- * const results = await repo.select()
194
- * .or({ email: { operator: 'LIKE', value: '%@gmail.com' } });
195
- *
196
- * // 페이지네이션
197
192
  * const page = await repo.select()
198
193
  * .paginate({ page: 1, pageSize: 20 });
199
- *
200
- * // 집계
201
- * const agg = await repo.select({ isActive: true })
202
- * .calculate([{ fn: 'COUNT', alias: 'count' }]);
203
194
  * ```
204
195
  */
205
196
  select(where) {
@@ -210,15 +201,17 @@ class BaseRepo {
210
201
  }
211
202
  /**
212
203
  * 단건 조회 (없으면 null). `select(where).one()` 의 단축형입니다.
204
+ * 전역 훅이 적용됩니다.
213
205
  *
214
206
  * @example
215
207
  * ```ts
216
208
  * const user = await repo.selectOne({ id: 1 });
209
+ * const user = await repo.selectOne({ email: 'foo@bar.com' });
217
210
  * ```
218
211
  */
219
212
  async selectOne(where) {
220
213
  try {
221
- return await new builder_1.QueryBuilder(this.tableName, where).one();
214
+ return await this.select(where).one();
222
215
  }
223
216
  catch (err) {
224
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,7 +1,25 @@
1
1
  import { ColShape } from './Column';
2
2
  export type Cols = Record<string, ColShape<unknown, boolean, boolean>>;
3
3
  export interface TableDef<TName extends string = string, TCols extends Cols = Cols> {
4
+ /** 테이블 이름 (schema 미포함 순수 테이블명) */
4
5
  readonly name: TName;
6
+ /** PostgreSQL 스키마 이름 (예: 'public', 'user', 'audit'). 없으면 undefined. */
7
+ readonly schema: string | undefined;
8
+ /**
9
+ * 쿼리에서 사용되는 완전한 식별자.
10
+ *
11
+ * - schema 있음: `"schema"."table"`
12
+ * - schema 없음: `"table"`
13
+ *
14
+ * @example
15
+ * // schema 없음
16
+ * defineTable('users', cols).qualifiedName // "users"
17
+ * // schema 있음
18
+ * defineTable('users', cols, { schema: 'auth' }).qualifiedName // "auth"."users"
19
+ * // 도트 표기
20
+ * defineTable('auth.users', cols).qualifiedName // "auth"."users"
21
+ */
22
+ readonly qualifiedName: string;
5
23
  readonly cols: TCols;
6
24
  }
7
25
  //# sourceMappingURL=Table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../../../src/features/schema/interfaces/Table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvE,MAAM,WAAW,QAAQ,CACvB,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,KAAK,SAAS,IAAI,GAAG,IAAI;IAEzB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;CACtB"}
1
+ {"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../../../src/features/schema/interfaces/Table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvE,MAAM,WAAW,QAAQ,CACvB,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,KAAK,SAAS,IAAI,GAAG,IAAI;IAEzB,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;CACtB"}
@@ -1,14 +1,44 @@
1
1
  import { TableDef, Cols } from './interfaces/Table';
2
+ export interface TableOpts {
3
+ /**
4
+ * PostgreSQL 스키마 이름.
5
+ * 지정하면 쿼리에서 `"schema"."table"` 형태로 사용됩니다.
6
+ *
7
+ * @example { schema: 'auth' }
8
+ */
9
+ schema?: string;
10
+ }
2
11
  /**
3
- * 테이블 정의를 생성합니다.
12
+ * PostgreSQL 테이블 정의를 생성합니다.
4
13
  *
5
- * @example
14
+ * ## 사용 방법 (세 가지 모두 동일하게 동작합니다)
15
+ *
16
+ * ### 1. 스키마 없이 테이블만 정의
6
17
  * ```ts
7
18
  * const usersTable = defineTable('users', {
8
- * id: col.serial().primaryKey(),
9
- * name: col.varchar(255).notNull(),
19
+ * id: col.serial().primaryKey(),
20
+ * name: col.varchar(255).notNull(),
21
+ * });
22
+ * // qualifiedName → "users"
23
+ * ```
24
+ *
25
+ * ### 2. options 객체로 스키마 지정
26
+ * ```ts
27
+ * const usersTable = defineTable('users', {
28
+ * id: col.serial().primaryKey(),
29
+ * name: col.varchar(255).notNull(),
30
+ * }, { schema: 'auth' });
31
+ * // qualifiedName → "auth"."users"
32
+ * ```
33
+ *
34
+ * ### 3. 도트(.) 표기법으로 스키마.테이블 직접 지정
35
+ * ```ts
36
+ * const usersTable = defineTable('auth.users', {
37
+ * id: col.serial().primaryKey(),
38
+ * name: col.varchar(255).notNull(),
10
39
  * });
40
+ * // qualifiedName → "auth"."users"
11
41
  * ```
12
42
  */
13
- export declare function defineTable<TName extends string, TCols extends Cols>(name: TName, cols: TCols): TableDef<TName, TCols>;
43
+ export declare function defineTable<TName extends string, TCols extends Cols>(nameOrQualified: TName, cols: TCols, opts?: TableOpts): TableDef<string, TCols>;
14
44
  //# sourceMappingURL=table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../src/features/schema/table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,SAAS,IAAI,EAClE,IAAI,EAAE,KAAK,EACX,IAAI,EAAE,KAAK,GACV,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAExB"}
1
+ {"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../src/features/schema/table.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,KAAK,SAAS,IAAI,EAClE,eAAe,EAAE,KAAK,EACtB,IAAI,EAAE,KAAK,EACX,IAAI,CAAC,EAAE,SAAS,GACf,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAyBzB"}
@@ -2,16 +2,58 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defineTable = defineTable;
4
4
  /**
5
- * 테이블 정의를 생성합니다.
5
+ * PostgreSQL 테이블 정의를 생성합니다.
6
6
  *
7
- * @example
7
+ * ## 사용 방법 (세 가지 모두 동일하게 동작합니다)
8
+ *
9
+ * ### 1. 스키마 없이 테이블만 정의
8
10
  * ```ts
9
11
  * const usersTable = defineTable('users', {
10
- * id: col.serial().primaryKey(),
11
- * name: col.varchar(255).notNull(),
12
+ * id: col.serial().primaryKey(),
13
+ * name: col.varchar(255).notNull(),
14
+ * });
15
+ * // qualifiedName → "users"
16
+ * ```
17
+ *
18
+ * ### 2. options 객체로 스키마 지정
19
+ * ```ts
20
+ * const usersTable = defineTable('users', {
21
+ * id: col.serial().primaryKey(),
22
+ * name: col.varchar(255).notNull(),
23
+ * }, { schema: 'auth' });
24
+ * // qualifiedName → "auth"."users"
25
+ * ```
26
+ *
27
+ * ### 3. 도트(.) 표기법으로 스키마.테이블 직접 지정
28
+ * ```ts
29
+ * const usersTable = defineTable('auth.users', {
30
+ * id: col.serial().primaryKey(),
31
+ * name: col.varchar(255).notNull(),
12
32
  * });
33
+ * // qualifiedName → "auth"."users"
13
34
  * ```
14
35
  */
15
- function defineTable(name, cols) {
16
- return { name, cols };
36
+ function defineTable(nameOrQualified, cols, opts) {
37
+ // 도트 표기법 파싱: "schema.table" → { schema, name }
38
+ const dotIdx = nameOrQualified.indexOf('.');
39
+ let resolvedSchema;
40
+ let resolvedName;
41
+ if (dotIdx !== -1) {
42
+ resolvedSchema = nameOrQualified.slice(0, dotIdx);
43
+ resolvedName = nameOrQualified.slice(dotIdx + 1);
44
+ }
45
+ else {
46
+ resolvedName = nameOrQualified;
47
+ resolvedSchema = opts?.schema;
48
+ }
49
+ // 쌍따옴표로 식별자 이스케이프 (예약어·대소문자 안전)
50
+ const qualifiedName = resolvedSchema
51
+ ? `"${resolvedSchema}"."${resolvedName}"`
52
+ : `"${resolvedName}"`;
53
+ return {
54
+ name: resolvedName,
55
+ schema: resolvedSchema,
56
+ qualifiedName,
57
+ cols,
58
+ };
17
59
  }
@@ -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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { col, Col } from './features/schema/column';
2
2
  export { defineTable } from './features/schema/table';
3
+ export type { TableOpts } from './features/schema/table';
3
4
  export type { ColShape } from './features/schema/interfaces/Column';
4
5
  export type { TableDef, Cols } from './features/schema/interfaces/Table';
5
6
  export type { InferRow, InferInsert, InferUpdate } from './features/schema/interfaces/Infer';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAe,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,MAAU,qCAAqC,CAAC;AACxE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AACzE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAG7F,OAAO,EAAE,UAAU,EAAE,MAAa,8BAA8B,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAe,4BAA4B,CAAC;AAC/D,YAAY,EAAE,KAAK,EAAE,MAAa,uCAAuC,CAAC;AAC1E,YAAY,EAAE,QAAQ,EAAE,MAAU,uCAAuC,CAAC;AAG1E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5G,YAAY,EAAE,UAAU,EAAE,MAAQ,4BAA4B,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAgB,0BAA0B,CAAC;AAG7D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAQ,6BAA6B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAa,wBAAwB,CAAC;AAC3D,YAAY,EAAE,UAAU,EAAE,MAAQ,mCAAmC,CAAC;AACtE,YAAY,EAAE,UAAU,EAAE,MAAQ,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAChF,YAAY,EAAE,UAAU,EAAE,MAAQ,yBAAyB,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,MAAW,0BAA0B,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,OAAO,EACP,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,SAAS,GACV,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAQ,6BAA6B,CAAC;AAGhE,OAAO,EAAE,OAAO,EAAE,MAAgB,iBAAiB,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGxE,OAAO,EAAE,MAAM,EAAE,MAAiB,gBAAgB,CAAC;AACnD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAe,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,YAAY,EAAE,SAAS,EAAE,MAAS,yBAAyB,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,MAAU,qCAAqC,CAAC;AACxE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AACzE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAG7F,OAAO,EAAE,UAAU,EAAE,MAAa,8BAA8B,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAe,4BAA4B,CAAC;AAC/D,YAAY,EAAE,KAAK,EAAE,MAAa,uCAAuC,CAAC;AAC1E,YAAY,EAAE,QAAQ,EAAE,MAAU,uCAAuC,CAAC;AAG1E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5G,YAAY,EAAE,UAAU,EAAE,MAAQ,4BAA4B,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAgB,0BAA0B,CAAC;AAG7D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAY,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAQ,6BAA6B,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAa,wBAAwB,CAAC;AAC3D,YAAY,EAAE,UAAU,EAAE,MAAQ,mCAAmC,CAAC;AACtE,YAAY,EAAE,UAAU,EAAE,MAAQ,mCAAmC,CAAC;AACtE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,mCAAmC,CAAC;AAChF,YAAY,EAAE,UAAU,EAAE,MAAQ,yBAAyB,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,MAAW,0BAA0B,CAAC;AAC7D,YAAY,EACV,aAAa,EACb,OAAO,EACP,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,SAAS,GACV,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAQ,6BAA6B,CAAC;AAGhE,OAAO,EAAE,OAAO,EAAE,MAAgB,iBAAiB,CAAC;AACpD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,YAAY,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGxE,OAAO,EAAE,MAAM,EAAE,MAAiB,gBAAgB,CAAC;AACnD,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reltype",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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",