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.
- 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 +30 -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/schema/interfaces/Table.d.ts +18 -0
- package/dist/features/schema/interfaces/Table.d.ts.map +1 -1
- package/dist/features/schema/table.d.ts +35 -5
- package/dist/features/schema/table.d.ts.map +1 -1
- package/dist/features/schema/table.js +48 -6
- 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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- 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
|
-
/**
|
|
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,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
|
|
132
|
-
return
|
|
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
|
|
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
|
-
/**
|
|
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,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
|
-
*
|
|
14
|
+
* ## 사용 방법 (세 가지 모두 동일하게 동작합니다)
|
|
15
|
+
*
|
|
16
|
+
* ### 1. 스키마 없이 테이블만 정의
|
|
6
17
|
* ```ts
|
|
7
18
|
* const usersTable = defineTable('users', {
|
|
8
|
-
* id:
|
|
9
|
-
* name:
|
|
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>(
|
|
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
|
|
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
|
-
*
|
|
7
|
+
* ## 사용 방법 (세 가지 모두 동일하게 동작합니다)
|
|
8
|
+
*
|
|
9
|
+
* ### 1. 스키마 없이 테이블만 정의
|
|
8
10
|
* ```ts
|
|
9
11
|
* const usersTable = defineTable('users', {
|
|
10
|
-
* id:
|
|
11
|
-
* name:
|
|
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(
|
|
16
|
-
|
|
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
|
-
*
|
|
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/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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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",
|