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.
- package/CHANGELOG.md +83 -3
- package/dist/features/connection/pool.d.ts +8 -1
- package/dist/features/connection/pool.d.ts.map +1 -1
- package/dist/features/connection/pool.js +14 -5
- package/dist/features/query/builder.d.ts +10 -3
- package/dist/features/query/builder.d.ts.map +1 -1
- package/dist/features/query/builder.js +27 -28
- package/dist/features/query/bulkInsert.d.ts +6 -3
- package/dist/features/query/bulkInsert.d.ts.map +1 -1
- package/dist/features/query/bulkInsert.js +14 -4
- package/dist/features/query/delete.d.ts +3 -1
- package/dist/features/query/delete.d.ts.map +1 -1
- package/dist/features/query/delete.js +11 -2
- package/dist/features/query/insert.d.ts +2 -0
- package/dist/features/query/insert.d.ts.map +1 -1
- package/dist/features/query/insert.js +8 -0
- package/dist/features/query/update.d.ts +2 -0
- package/dist/features/query/update.d.ts.map +1 -1
- package/dist/features/query/update.js +13 -0
- package/dist/features/query/upsert.d.ts +3 -1
- package/dist/features/query/upsert.d.ts.map +1 -1
- package/dist/features/query/upsert.js +19 -1
- package/dist/features/repository/base.d.ts +9 -13
- package/dist/features/repository/base.d.ts.map +1 -1
- package/dist/features/repository/base.js +29 -37
- package/dist/features/schema/column.d.ts +26 -11
- package/dist/features/schema/column.d.ts.map +1 -1
- package/dist/features/schema/column.js +32 -18
- package/dist/features/schema/interfaces/Infer.d.ts +8 -1
- package/dist/features/schema/interfaces/Infer.d.ts.map +1 -1
- package/dist/features/transform/case.d.ts +12 -2
- package/dist/features/transform/case.d.ts.map +1 -1
- package/dist/features/transform/case.js +19 -4
- 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.
|
|
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 기반 페이지네이션
|
|
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] —
|
|
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
|
-
/**
|
|
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
|
|
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 {
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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":"AAKA,OAAO,EACL,aAAa,EAEb,UAAU,EACV,aAAa,EACb,YAAY,EACZ,UAAU,EACV,aAAa,EAEb,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,SAAS,EACV,MAAM,uBAAuB,CAAC;AA4D/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,qBAAa,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,UAAU,CAAC,CAAe;gBAEtB,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;IASvD,gBAAgB;IAChB,KAAK,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI;IAKzC,eAAe;IACf,EAAE,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI;IAKtC;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI;IAQ1C,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAKtB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAKvB;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI;IAK/C
|
|
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
|
|
195
|
-
|
|
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
|
-
|
|
258
|
-
dataParams
|
|
259
|
-
const limitIdx =
|
|
260
|
-
|
|
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
|
|
276
|
-
|
|
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
|
|
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
|
-
|
|
332
|
+
logger.error(`cursorPaginate [${this._table}]: 유효하지 않은 cursor 토큰입니다. 이전 페이지 응답의 nextCursor 값을 그대로 사용하세요.`);
|
|
333
|
+
return { data: [], nextCursor: null, pageSize, hasNext: false };
|
|
335
334
|
}
|
|
336
335
|
}
|
|
337
|
-
//
|
|
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
|
|
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 - 테이블명
|
|
7
|
-
* @param rows - 삽입할 데이터 배열 (camelCase 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;
|
|
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 - 테이블명
|
|
10
|
-
* @param rows - 삽입할 데이터 배열 (camelCase 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;
|
|
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;
|
|
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;
|
|
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 - 테이블명
|
|
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;
|
|
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 - 테이블명
|
|
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":"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;
|
|
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
|
-
/**
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
133
|
-
return
|
|
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
|
|
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
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* DB 기본값이 있는 컬럼으로 설정합니다. (INSERT optional)
|
|
32
|
+
*
|
|
33
|
+
* @example col.boolean().default() // BOOLEAN DEFAULT false 등
|
|
34
|
+
*/
|
|
32
35
|
default(): Col<T, true, IsPrimary>;
|
|
33
|
-
/**
|
|
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:
|
|
43
|
-
* name:
|
|
44
|
-
* email:
|
|
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;
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
56
|
-
* name:
|
|
57
|
-
* email:
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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-
|
|
16
|
+
return str.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
13
17
|
}
|
|
14
18
|
/**
|
|
15
19
|
* camelCase 문자열을 snake_case로 변환합니다.
|
|
16
|
-
*
|
|
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
|
|
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
|
+
"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",
|