reltype 0.1.6 → 0.1.7
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 +34 -0
- package/dist/features/query/builder.d.ts +13 -3
- package/dist/features/query/builder.d.ts.map +1 -1
- package/dist/features/query/builder.js +39 -13
- package/dist/features/query/bulkInsert.d.ts +1 -0
- package/dist/features/query/bulkInsert.d.ts.map +1 -1
- package/dist/features/query/bulkInsert.js +3 -1
- package/dist/features/query/insert.d.ts +1 -0
- package/dist/features/query/insert.d.ts.map +1 -1
- package/dist/features/query/insert.js +3 -1
- package/dist/features/query/select.d.ts +1 -1
- package/dist/features/query/select.d.ts.map +1 -1
- package/dist/features/query/select.js +7 -2
- package/dist/features/query/update.d.ts +1 -0
- package/dist/features/query/update.d.ts.map +1 -1
- package/dist/features/query/update.js +3 -1
- package/dist/features/query/upsert.d.ts +1 -0
- package/dist/features/query/upsert.d.ts.map +1 -1
- package/dist/features/query/upsert.js +12 -6
- package/dist/features/query/where.d.ts +1 -0
- package/dist/features/query/where.d.ts.map +1 -1
- package/dist/features/query/where.js +3 -1
- package/dist/features/schema/table.d.ts.map +1 -1
- package/dist/features/schema/table.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/sqlGuard.d.ts +42 -0
- package/dist/utils/sqlGuard.d.ts.map +1 -0
- package/dist/utils/sqlGuard.js +94 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.6] — 2026-03-19
|
|
10
|
+
|
|
11
|
+
### Security — SQL Injection 전면 차단
|
|
12
|
+
|
|
13
|
+
이번 릴리즈에서는 SQL Injection 취약점을 방어하기 위한 `sqlGuard` 유틸리티를 도입하고,
|
|
14
|
+
모든 SQL 식별자(컬럼명, 테이블명, 집계 함수 등)에 대한 검증·이스케이프를 적용했습니다.
|
|
15
|
+
|
|
16
|
+
#### 신규 파일
|
|
17
|
+
|
|
18
|
+
- **`src/utils/sqlGuard.ts`** — SQL 보안 유틸리티 신규 추가
|
|
19
|
+
- `quoteIdentifier(raw)` — 식별자 패턴 검증 후 PostgreSQL 표준 이중 따옴표 이스케이프 (`"` → `""`)
|
|
20
|
+
- `escapeSchemaIdentifier(name)` — 스키마·테이블 정의 시 `"` 이스케이프 (패턴 검증 없음, 개발자 제어 값)
|
|
21
|
+
- `validateOrderDir(dir)` — ORDER BY 방향 화이트리스트 (ASC/DESC)
|
|
22
|
+
- `validateAggregateFn(fn)` — 집계 함수 화이트리스트 (COUNT/SUM/AVG/MIN/MAX)
|
|
23
|
+
- `validateJoinType(type)` — JOIN 타입 화이트리스트 (INNER/LEFT/RIGHT/FULL)
|
|
24
|
+
- 유효하지 않은 값 감지 시 `logger.error` 로 기록 후 안전한 기본값 반환 (no-throw 정책 준수)
|
|
25
|
+
|
|
26
|
+
#### 수정 파일
|
|
27
|
+
|
|
28
|
+
| 파일 | 수정 내용 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `src/features/schema/table.ts` | `escapeSchemaIdentifier` 적용 — 식별자 내 `"` 이중 이스케이프 |
|
|
31
|
+
| `src/features/query/where.ts` | 컬럼명에 `quoteIdentifier` 적용 |
|
|
32
|
+
| `src/features/query/insert.ts` | INSERT 컬럼명에 `quoteIdentifier` 적용 |
|
|
33
|
+
| `src/features/query/update.ts` | SET 절 컬럼명에 `quoteIdentifier` 적용 |
|
|
34
|
+
| `src/features/query/upsert.ts` | INSERT 컬럼명·`conflictCol`에 `quoteIdentifier` 적용, `EXCLUDED."col"` 이스케이프 |
|
|
35
|
+
| `src/features/query/bulkInsert.ts` | INSERT 컬럼명에 `quoteIdentifier` 적용 |
|
|
36
|
+
| `src/features/query/select.ts` | ORDER BY 컬럼 `quoteIdentifier`, 방향 `validateOrderDir` 적용 |
|
|
37
|
+
| `src/features/query/builder.ts` | `renderCond`·`buildJoinSQL`·`buildGroupBySQL`·`buildOrderBySQL`·`calculate` 전체에 가드 적용, `cursorColumn` 사전 검증 추가 |
|
|
38
|
+
| `src/index.ts` | `sqlGuard` 유틸리티 공개 export 추가 |
|
|
39
|
+
| `src/utils/index.ts` | `sqlGuard` re-export 추가 |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
9
43
|
## [0.1.5] — 2026-03-19
|
|
10
44
|
|
|
11
45
|
### Changed
|
|
@@ -71,18 +71,28 @@ export declare class QueryBuilder<T extends Record<string, unknown>> {
|
|
|
71
71
|
*/
|
|
72
72
|
groupBy(columns: Array<keyof T | string>): this;
|
|
73
73
|
/**
|
|
74
|
-
* JOIN 추가 (여러 번 호출 가능)
|
|
74
|
+
* JOIN 추가 (여러 번 호출 가능).
|
|
75
|
+
* `table`은 `quoteIdentifier`로 자동 검증됩니다.
|
|
76
|
+
* `type`(INNER/LEFT/RIGHT/FULL)은 화이트리스트로 검증됩니다.
|
|
75
77
|
*
|
|
76
78
|
* @example
|
|
77
79
|
* .join({ table: 'orders', on: 'users.id = orders.user_id', type: 'LEFT' })
|
|
78
80
|
*
|
|
79
|
-
* @security `on`
|
|
80
|
-
* 사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.
|
|
81
|
+
* @security `on` 절(JOIN 조건)은 반드시 애플리케이션 코드에서 정적으로 구성해야 합니다.
|
|
82
|
+
* 사용자 입력을 `on` 에 직접 전달하면 SQL Injection 위험이 있습니다.
|
|
83
|
+
* `table`, `type` 은 내부적으로 검증됩니다.
|
|
81
84
|
*/
|
|
82
85
|
join(j: JoinClause): this;
|
|
83
86
|
/**
|
|
84
87
|
* SELECT 할 컬럼 지정 (기본값: *)
|
|
88
|
+
*
|
|
89
|
+
* 단순 컬럼명 및 `table.column` 표기법을 사용하세요.
|
|
90
|
+
* 집계 표현식(`COUNT(orders.id) AS cnt`)도 허용하지만,
|
|
91
|
+
* **사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.**
|
|
92
|
+
* 복잡한 표현식은 정적으로 구성된 문자열만 전달하세요.
|
|
93
|
+
*
|
|
85
94
|
* @example .columns(['id', 'email', 'firstName'])
|
|
95
|
+
* @example .columns(['users.id', 'orders.total'])
|
|
86
96
|
*/
|
|
87
97
|
columns(cols: Array<keyof T | string>): this;
|
|
88
98
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/features/query/builder.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../../src/features/query/builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ const mapper_1 = require("../transform/mapper");
|
|
|
6
6
|
const case_1 = require("../transform/case");
|
|
7
7
|
const logger_1 = require("../../utils/logger");
|
|
8
8
|
const dbError_1 = require("../../utils/dbError");
|
|
9
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
9
10
|
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
|
|
10
11
|
// ── 내부 유틸 ──────────────────────────────────────────────────────────────
|
|
11
12
|
function isWhereVal(val) {
|
|
@@ -33,19 +34,20 @@ function parseWhere(where) {
|
|
|
33
34
|
return conds;
|
|
34
35
|
}
|
|
35
36
|
function renderCond(c, params) {
|
|
37
|
+
const col = (0, sqlGuard_1.quoteIdentifier)(c.col);
|
|
36
38
|
switch (c.op) {
|
|
37
39
|
case 'IS NULL':
|
|
38
40
|
case 'IS NOT NULL':
|
|
39
|
-
return `${
|
|
41
|
+
return `${col} ${c.op}`;
|
|
40
42
|
case 'IN':
|
|
41
43
|
case 'NOT IN': {
|
|
42
44
|
const arr = Array.isArray(c.val) ? c.val : [c.val];
|
|
43
45
|
const phs = arr.map((v) => { params.push(v); return `$${params.length}`; });
|
|
44
|
-
return `${
|
|
46
|
+
return `${col} ${c.op} (${phs.join(', ')})`;
|
|
45
47
|
}
|
|
46
48
|
default:
|
|
47
49
|
params.push(c.val);
|
|
48
|
-
return `${
|
|
50
|
+
return `${col} ${c.op} $${params.length}`;
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
// ── QueryBuilder ────────────────────────────────────────────────────────────
|
|
@@ -123,7 +125,7 @@ class QueryBuilder {
|
|
|
123
125
|
orderBy(clauses) {
|
|
124
126
|
this._orderByClauses = clauses.map(({ column, direction }) => ({
|
|
125
127
|
col: (0, case_1.toSnake)(String(column)),
|
|
126
|
-
dir: direction ?? 'ASC',
|
|
128
|
+
dir: (0, sqlGuard_1.validateOrderDir)(direction ?? 'ASC'),
|
|
127
129
|
}));
|
|
128
130
|
return this;
|
|
129
131
|
}
|
|
@@ -144,13 +146,16 @@ class QueryBuilder {
|
|
|
144
146
|
return this;
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
|
-
* JOIN 추가 (여러 번 호출 가능)
|
|
149
|
+
* JOIN 추가 (여러 번 호출 가능).
|
|
150
|
+
* `table`은 `quoteIdentifier`로 자동 검증됩니다.
|
|
151
|
+
* `type`(INNER/LEFT/RIGHT/FULL)은 화이트리스트로 검증됩니다.
|
|
148
152
|
*
|
|
149
153
|
* @example
|
|
150
154
|
* .join({ table: 'orders', on: 'users.id = orders.user_id', type: 'LEFT' })
|
|
151
155
|
*
|
|
152
|
-
* @security `on`
|
|
153
|
-
* 사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.
|
|
156
|
+
* @security `on` 절(JOIN 조건)은 반드시 애플리케이션 코드에서 정적으로 구성해야 합니다.
|
|
157
|
+
* 사용자 입력을 `on` 에 직접 전달하면 SQL Injection 위험이 있습니다.
|
|
158
|
+
* `table`, `type` 은 내부적으로 검증됩니다.
|
|
154
159
|
*/
|
|
155
160
|
join(j) {
|
|
156
161
|
this._joins.push(j);
|
|
@@ -158,7 +163,14 @@ class QueryBuilder {
|
|
|
158
163
|
}
|
|
159
164
|
/**
|
|
160
165
|
* SELECT 할 컬럼 지정 (기본값: *)
|
|
166
|
+
*
|
|
167
|
+
* 단순 컬럼명 및 `table.column` 표기법을 사용하세요.
|
|
168
|
+
* 집계 표현식(`COUNT(orders.id) AS cnt`)도 허용하지만,
|
|
169
|
+
* **사용자 입력을 직접 전달하면 SQL Injection 위험이 있습니다.**
|
|
170
|
+
* 복잡한 표현식은 정적으로 구성된 문자열만 전달하세요.
|
|
171
|
+
*
|
|
161
172
|
* @example .columns(['id', 'email', 'firstName'])
|
|
173
|
+
* @example .columns(['users.id', 'orders.total'])
|
|
162
174
|
*/
|
|
163
175
|
columns(cols) {
|
|
164
176
|
this._cols = cols.map((c) => (0, case_1.toSnake)(String(c))).join(', ');
|
|
@@ -216,14 +228,19 @@ class QueryBuilder {
|
|
|
216
228
|
async calculate(fns) {
|
|
217
229
|
const { whereSQL, params } = this.buildWhereParts();
|
|
218
230
|
const selects = fns
|
|
219
|
-
.map(({ fn, column, alias }) =>
|
|
231
|
+
.map(({ fn, column, alias }) => {
|
|
232
|
+
const safeFn = (0, sqlGuard_1.validateAggregateFn)(fn);
|
|
233
|
+
const safeCol = column ? (0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(column)) : '*';
|
|
234
|
+
const safeAlias = (0, sqlGuard_1.quoteIdentifier)(alias);
|
|
235
|
+
return `${safeFn}(${safeCol}) AS ${safeAlias}`;
|
|
236
|
+
})
|
|
220
237
|
.join(', ');
|
|
221
238
|
const sql = [
|
|
222
239
|
`SELECT ${selects}`,
|
|
223
240
|
`FROM ${this._table}`,
|
|
224
|
-
|
|
241
|
+
this.buildJoinSQL(),
|
|
225
242
|
whereSQL,
|
|
226
|
-
this._groupByCols.length > 0 ? `GROUP BY ${this._groupByCols.join(', ')}` : '',
|
|
243
|
+
this._groupByCols.length > 0 ? `GROUP BY ${this._groupByCols.map(sqlGuard_1.quoteIdentifier).join(', ')}` : '',
|
|
227
244
|
]
|
|
228
245
|
.filter(Boolean)
|
|
229
246
|
.join(' ');
|
|
@@ -323,6 +340,11 @@ class QueryBuilder {
|
|
|
323
340
|
async cursorPaginate(opts) {
|
|
324
341
|
const { pageSize, cursor, cursorColumn, direction = 'asc' } = opts;
|
|
325
342
|
const colSnake = (0, case_1.toSnake)(cursorColumn);
|
|
343
|
+
// cursorColumn 유효성 검증 (내부에서 quoteIdentifier가 렌더 시 재검증함)
|
|
344
|
+
if (!colSnake || (0, sqlGuard_1.quoteIdentifier)(colSnake) === '""') {
|
|
345
|
+
logger.error(`cursorPaginate [${this._table}]: 유효하지 않은 cursorColumn "${cursorColumn}".`);
|
|
346
|
+
return { data: [], nextCursor: null, pageSize, hasNext: false };
|
|
347
|
+
}
|
|
326
348
|
let cursorValue;
|
|
327
349
|
if (cursor) {
|
|
328
350
|
try {
|
|
@@ -538,17 +560,21 @@ class QueryBuilder {
|
|
|
538
560
|
}
|
|
539
561
|
buildJoinSQL() {
|
|
540
562
|
return this._joins
|
|
541
|
-
.map((j) =>
|
|
563
|
+
.map((j) => {
|
|
564
|
+
const type = (0, sqlGuard_1.validateJoinType)(j.type ?? 'INNER');
|
|
565
|
+
const table = (0, sqlGuard_1.quoteIdentifier)(j.table);
|
|
566
|
+
return `${type} JOIN ${table} ON ${j.on}`;
|
|
567
|
+
})
|
|
542
568
|
.join(' ');
|
|
543
569
|
}
|
|
544
570
|
buildGroupBySQL() {
|
|
545
571
|
return this._groupByCols.length > 0
|
|
546
|
-
? `GROUP BY ${this._groupByCols.join(', ')}`
|
|
572
|
+
? `GROUP BY ${this._groupByCols.map(sqlGuard_1.quoteIdentifier).join(', ')}`
|
|
547
573
|
: '';
|
|
548
574
|
}
|
|
549
575
|
buildOrderBySQL() {
|
|
550
576
|
return this._orderByClauses.length > 0
|
|
551
|
-
? `ORDER BY ${this._orderByClauses.map((o) => `${o.col} ${o.dir}`).join(', ')}`
|
|
577
|
+
? `ORDER BY ${this._orderByClauses.map((o) => `${(0, sqlGuard_1.quoteIdentifier)(o.col)} ${o.dir}`).join(', ')}`
|
|
552
578
|
: '';
|
|
553
579
|
}
|
|
554
580
|
buildSelectSQL() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulkInsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/bulkInsert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bulkInsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/bulkInsert.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,UAAU,CA4BZ"}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildBulkInsert = buildBulkInsert;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
const logger_1 = require("../../utils/logger");
|
|
6
7
|
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
|
|
7
8
|
/**
|
|
8
9
|
* 여러 row를 한 번의 INSERT 쿼리로 삽입합니다.
|
|
10
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
9
11
|
*
|
|
10
12
|
* @param table - 테이블명
|
|
11
13
|
* @param rows - 삽입할 데이터 배열 (camelCase key)
|
|
@@ -24,7 +26,7 @@ function buildBulkInsert(table, rows) {
|
|
|
24
26
|
logger.error(`buildBulkInsert [${table}]: 첫 번째 row에 삽입할 데이터가 없습니다.`);
|
|
25
27
|
return { sql: '', params: [] };
|
|
26
28
|
}
|
|
27
|
-
const cols = keys.map(case_1.toSnake).join(', ');
|
|
29
|
+
const cols = keys.map((k) => (0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(k))).join(', ');
|
|
28
30
|
const params = [];
|
|
29
31
|
const valueSets = rows.map((row) => {
|
|
30
32
|
const placeholders = keys.map((k) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/features/query/insert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../src/features/query/insert.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,UAAU,CAgBZ"}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildInsert = buildInsert;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
const logger_1 = require("../../utils/logger");
|
|
6
7
|
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
|
|
7
8
|
/**
|
|
8
9
|
* INSERT 쿼리를 생성합니다. RETURNING * 으로 삽입된 row를 반환합니다.
|
|
9
10
|
* camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
|
|
11
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
10
12
|
*
|
|
11
13
|
* 삽입할 데이터가 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
|
|
12
14
|
*/
|
|
@@ -16,7 +18,7 @@ function buildInsert(table, data) {
|
|
|
16
18
|
logger.error(`buildInsert [${table}]: 삽입할 데이터가 없습니다.`);
|
|
17
19
|
return { sql: '', params: [] };
|
|
18
20
|
}
|
|
19
|
-
const cols = entries.map(([k]) => (0, case_1.toSnake)(k)).join(', ');
|
|
21
|
+
const cols = entries.map(([k]) => (0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(k))).join(', ');
|
|
20
22
|
const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
|
|
21
23
|
const params = entries.map(([, v]) => v);
|
|
22
24
|
return {
|
|
@@ -9,7 +9,7 @@ export interface SelectOpts<T extends Record<string, unknown>> {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* SELECT 쿼리를 생성합니다.
|
|
12
|
-
*
|
|
12
|
+
* camelCase → snake_case 자동 변환 및 `quoteIdentifier` 검증이 적용됩니다.
|
|
13
13
|
*/
|
|
14
14
|
export declare function buildSelect<T extends Record<string, unknown>>(table: string, opts?: SelectOpts<T>): BuiltQuery;
|
|
15
15
|
//# sourceMappingURL=select.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/features/query/select.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/features/query/select.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;IACtB,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,UAAU,CAAC,CAAC,CAAM,GACvB,UAAU,CAgCZ"}
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildSelect = buildSelect;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
const where_1 = require("./where");
|
|
6
7
|
/**
|
|
7
8
|
* SELECT 쿼리를 생성합니다.
|
|
8
|
-
*
|
|
9
|
+
* camelCase → snake_case 자동 변환 및 `quoteIdentifier` 검증이 적용됩니다.
|
|
9
10
|
*/
|
|
10
11
|
function buildSelect(table, opts = {}) {
|
|
11
12
|
const parts = [`SELECT * FROM ${table}`];
|
|
@@ -18,7 +19,11 @@ function buildSelect(table, opts = {}) {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
if (opts.orderBy && opts.orderBy.length > 0) {
|
|
21
|
-
const clauses = opts.orderBy.map(({ col, dir }) =>
|
|
22
|
+
const clauses = opts.orderBy.map(({ col, dir }) => {
|
|
23
|
+
const safeCol = (0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(String(col)));
|
|
24
|
+
const safeDir = (0, sqlGuard_1.validateOrderDir)(dir ?? 'ASC');
|
|
25
|
+
return `${safeCol} ${safeDir}`;
|
|
26
|
+
});
|
|
22
27
|
parts.push(`ORDER BY ${clauses.join(', ')}`);
|
|
23
28
|
}
|
|
24
29
|
if (opts.limit !== undefined) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/features/query/update.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/features/query/update.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;GAMG;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"}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildUpdate = buildUpdate;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
const where_1 = require("./where");
|
|
6
7
|
const logger_1 = require("../../utils/logger");
|
|
7
8
|
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
|
|
8
9
|
/**
|
|
9
10
|
* UPDATE 쿼리를 생성합니다. RETURNING * 으로 수정된 row를 반환합니다.
|
|
10
11
|
* camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
|
|
12
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
11
13
|
*
|
|
12
14
|
* 수정할 데이터가 없거나 WHERE 조건이 없으면 빈 쿼리를 반환합니다 (실행 시 no-op).
|
|
13
15
|
*/
|
|
@@ -25,7 +27,7 @@ function buildUpdate(table, data, where) {
|
|
|
25
27
|
const params = [];
|
|
26
28
|
const setClauses = entries.map(([k, v]) => {
|
|
27
29
|
params.push(v);
|
|
28
|
-
return `${(0, case_1.toSnake)(k)} = $${params.length}`;
|
|
30
|
+
return `${(0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(k))} = $${params.length}`;
|
|
29
31
|
});
|
|
30
32
|
const { sql: whereSql, params: whereParams } = (0, where_1.buildWhere)(where, params.length + 1);
|
|
31
33
|
params.push(...whereParams);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/upsert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"upsert.d.ts","sourceRoot":"","sources":["../../../src/features/query/upsert.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAQhD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE,MAAM,GAClB,UAAU,CAyCZ"}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildUpsert = buildUpsert;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
const logger_1 = require("../../utils/logger");
|
|
6
7
|
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[Query]' });
|
|
7
8
|
/**
|
|
8
9
|
* INSERT ... ON CONFLICT DO UPDATE 쿼리를 생성합니다.
|
|
9
10
|
* 충돌 컬럼을 제외한 나머지 컬럼을 EXCLUDED 값으로 업데이트합니다.
|
|
11
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
10
12
|
*
|
|
11
13
|
* @param table - 테이블명
|
|
12
14
|
* @param data - 삽입할 데이터 (camelCase key)
|
|
@@ -20,19 +22,23 @@ function buildUpsert(table, data, conflictCol) {
|
|
|
20
22
|
logger.error(`buildUpsert [${table}]: 삽입할 데이터가 없습니다.`);
|
|
21
23
|
return { sql: '', params: [] };
|
|
22
24
|
}
|
|
23
|
-
const
|
|
25
|
+
const quotedConflictCol = (0, sqlGuard_1.quoteIdentifier)(conflictCol);
|
|
26
|
+
const snakeCols = entries.map(([k]) => (0, case_1.toSnake)(k));
|
|
27
|
+
const cols = snakeCols.map(sqlGuard_1.quoteIdentifier).join(', ');
|
|
24
28
|
const placeholders = entries.map((_, i) => `$${i + 1}`).join(', ');
|
|
25
29
|
const params = entries.map(([, v]) => v);
|
|
26
|
-
const updateSet =
|
|
27
|
-
.map(([k]) => (0, case_1.toSnake)(k))
|
|
30
|
+
const updateSet = snakeCols
|
|
28
31
|
.filter((col) => col !== conflictCol)
|
|
29
|
-
.map((col) =>
|
|
32
|
+
.map((col) => {
|
|
33
|
+
const q = (0, sqlGuard_1.quoteIdentifier)(col);
|
|
34
|
+
return `${q} = EXCLUDED.${q}`;
|
|
35
|
+
})
|
|
30
36
|
.join(', ');
|
|
31
37
|
if (!updateSet) {
|
|
32
38
|
return {
|
|
33
39
|
sql: [
|
|
34
40
|
`INSERT INTO ${table} (${cols}) VALUES (${placeholders})`,
|
|
35
|
-
`ON CONFLICT (${
|
|
41
|
+
`ON CONFLICT (${quotedConflictCol}) DO NOTHING`,
|
|
36
42
|
'RETURNING *',
|
|
37
43
|
].join(' '),
|
|
38
44
|
params,
|
|
@@ -41,7 +47,7 @@ function buildUpsert(table, data, conflictCol) {
|
|
|
41
47
|
return {
|
|
42
48
|
sql: [
|
|
43
49
|
`INSERT INTO ${table} (${cols}) VALUES (${placeholders})`,
|
|
44
|
-
`ON CONFLICT (${
|
|
50
|
+
`ON CONFLICT (${quotedConflictCol}) DO UPDATE SET ${updateSet}`,
|
|
45
51
|
'RETURNING *',
|
|
46
52
|
].join(' '),
|
|
47
53
|
params,
|
|
@@ -3,6 +3,7 @@ import { WhereInput } from './interfaces/Where';
|
|
|
3
3
|
/**
|
|
4
4
|
* WhereInput 객체로부터 WHERE 절 SQL과 params를 생성합니다.
|
|
5
5
|
* camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
|
|
6
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
6
7
|
*
|
|
7
8
|
* @param where - camelCase key 기반 조건 객체
|
|
8
9
|
* @param startIdx - param placeholder 시작 번호 ($1, $2 ...)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../../src/features/query/where.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../../src/features/query/where.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1D,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,EACpB,QAAQ,GAAE,MAAU,GACnB,UAAU,CAqBZ"}
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildWhere = buildWhere;
|
|
4
4
|
const case_1 = require("../transform/case");
|
|
5
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
5
6
|
/**
|
|
6
7
|
* WhereInput 객체로부터 WHERE 절 SQL과 params를 생성합니다.
|
|
7
8
|
* camelCase key → snake_case 컬럼명으로 자동 변환됩니다.
|
|
9
|
+
* 모든 컬럼명은 `quoteIdentifier`로 검증 및 이스케이프됩니다.
|
|
8
10
|
*
|
|
9
11
|
* @param where - camelCase key 기반 조건 객체
|
|
10
12
|
* @param startIdx - param placeholder 시작 번호 ($1, $2 ...)
|
|
@@ -17,7 +19,7 @@ function buildWhere(where, startIdx = 1) {
|
|
|
17
19
|
const params = [];
|
|
18
20
|
const conditions = [];
|
|
19
21
|
for (const [key, val] of entries) {
|
|
20
|
-
const col = (0, case_1.toSnake)(key);
|
|
22
|
+
const col = (0, sqlGuard_1.quoteIdentifier)((0, case_1.toSnake)(key));
|
|
21
23
|
if (val === null) {
|
|
22
24
|
conditions.push(`${col} IS NULL`);
|
|
23
25
|
}
|
|
@@ -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;
|
|
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;AAGpD,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,CA0BzB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.defineTable = defineTable;
|
|
4
|
+
const sqlGuard_1 = require("../../utils/sqlGuard");
|
|
4
5
|
/**
|
|
5
6
|
* PostgreSQL 테이블 정의를 생성합니다.
|
|
6
7
|
*
|
|
@@ -47,9 +48,10 @@ function defineTable(nameOrQualified, cols, opts) {
|
|
|
47
48
|
resolvedSchema = opts?.schema;
|
|
48
49
|
}
|
|
49
50
|
// 쌍따옴표로 식별자 이스케이프 (예약어·대소문자 안전)
|
|
51
|
+
// 내부의 " 문자는 "" 로 이중 이스케이프합니다 (PostgreSQL 표준).
|
|
50
52
|
const qualifiedName = resolvedSchema
|
|
51
|
-
?
|
|
52
|
-
:
|
|
53
|
+
? `${(0, sqlGuard_1.escapeSchemaIdentifier)(resolvedSchema)}.${(0, sqlGuard_1.escapeSchemaIdentifier)(resolvedName)}`
|
|
54
|
+
: (0, sqlGuard_1.escapeSchemaIdentifier)(resolvedName);
|
|
53
55
|
return {
|
|
54
56
|
name: resolvedName,
|
|
55
57
|
schema: resolvedSchema,
|
package/dist/index.d.ts
CHANGED
|
@@ -33,4 +33,5 @@ export type { DatabaseConfig } from './interfaces/DatabaseConfig';
|
|
|
33
33
|
export { PostgresConfig, NodeEnvSource, readEnv } from './utils/reader';
|
|
34
34
|
export { Logger } from './utils/logger';
|
|
35
35
|
export type { LogLevel, LogFormat, LoggerConfig } from './utils/logger';
|
|
36
|
+
export { quoteIdentifier, escapeSchemaIdentifier, validateOrderDir, validateAggregateFn, validateJoinType, } from './utils/sqlGuard';
|
|
36
37
|
//# sourceMappingURL=index.d.ts.map
|
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,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"}
|
|
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;AAGxE,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Logger = exports.readEnv = exports.NodeEnvSource = exports.PostgresConfig = exports.getDatabaseConfig = exports.DbError = exports.mapRows = exports.mapRow = exports.keysToSnake = exports.keysToCamel = exports.toSnake = exports.toCamel = exports.QueryBuilder = exports.buildWhere = exports.buildBulkInsert = exports.buildUpsert = exports.buildDelete = exports.buildUpdate = exports.buildInsert = exports.buildSelect = exports.runInTx = exports.checkPoolHealth = exports.getPoolStatus = exports.closePool = exports.withClient = exports.getPool = exports.BaseRepo = exports.createRepo = exports.defineTable = exports.Col = exports.col = void 0;
|
|
3
|
+
exports.validateJoinType = exports.validateAggregateFn = exports.validateOrderDir = exports.escapeSchemaIdentifier = exports.quoteIdentifier = exports.Logger = exports.readEnv = exports.NodeEnvSource = exports.PostgresConfig = exports.getDatabaseConfig = exports.DbError = exports.mapRows = exports.mapRow = exports.keysToSnake = exports.keysToCamel = exports.toSnake = exports.toCamel = exports.QueryBuilder = exports.buildWhere = exports.buildBulkInsert = exports.buildUpsert = exports.buildDelete = exports.buildUpdate = exports.buildInsert = exports.buildSelect = exports.runInTx = exports.checkPoolHealth = exports.getPoolStatus = exports.closePool = exports.withClient = exports.getPool = exports.BaseRepo = exports.createRepo = exports.defineTable = exports.Col = exports.col = void 0;
|
|
4
4
|
// ── Schema ──────────────────────────────────────────────────────────────────
|
|
5
5
|
var column_1 = require("./features/schema/column");
|
|
6
6
|
Object.defineProperty(exports, "col", { enumerable: true, get: function () { return column_1.col; } });
|
|
@@ -61,3 +61,10 @@ Object.defineProperty(exports, "readEnv", { enumerable: true, get: function () {
|
|
|
61
61
|
// ── Logger ───────────────────────────────────────────────────────────────────
|
|
62
62
|
var logger_1 = require("./utils/logger");
|
|
63
63
|
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logger_1.Logger; } });
|
|
64
|
+
// ── SQL Guard (SQL Injection 방어 유틸리티) ───────────────────────────────────
|
|
65
|
+
var sqlGuard_1 = require("./utils/sqlGuard");
|
|
66
|
+
Object.defineProperty(exports, "quoteIdentifier", { enumerable: true, get: function () { return sqlGuard_1.quoteIdentifier; } });
|
|
67
|
+
Object.defineProperty(exports, "escapeSchemaIdentifier", { enumerable: true, get: function () { return sqlGuard_1.escapeSchemaIdentifier; } });
|
|
68
|
+
Object.defineProperty(exports, "validateOrderDir", { enumerable: true, get: function () { return sqlGuard_1.validateOrderDir; } });
|
|
69
|
+
Object.defineProperty(exports, "validateAggregateFn", { enumerable: true, get: function () { return sqlGuard_1.validateAggregateFn; } });
|
|
70
|
+
Object.defineProperty(exports, "validateJoinType", { enumerable: true, get: function () { return sqlGuard_1.validateJoinType; } });
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AggregateFn, JoinType } from '../features/query/interfaces/Advanced';
|
|
2
|
+
/**
|
|
3
|
+
* SQL 식별자(컬럼명, 테이블명 등)를 검증하고 PostgreSQL 표준으로 이스케이프합니다.
|
|
4
|
+
*
|
|
5
|
+
* - 유효한 식별자 패턴 `[a-zA-Z_][a-zA-Z0-9_.]*` 에 맞지 않으면
|
|
6
|
+
* `logger.error` 로 기록하고 `""` (빈 식별자) 를 반환합니다.
|
|
7
|
+
* - `"` 문자는 `""` 로 이중 이스케이프합니다 (PostgreSQL 표준).
|
|
8
|
+
* - `schema.table` / `table.column` 도트 표기법을 지원합니다.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* quoteIdentifier('first_name') // → '"first_name"'
|
|
12
|
+
* quoteIdentifier('auth.users') // → '"auth"."users"'
|
|
13
|
+
* quoteIdentifier('1; DROP TABLE')// → '""' + logger.error
|
|
14
|
+
*/
|
|
15
|
+
export declare function quoteIdentifier(raw: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* 개발자가 정의하는 테이블/스키마 식별자를 이스케이프합니다.
|
|
18
|
+
* 패턴 검증 없이 `"` → `""` 이중 이스케이프만 수행합니다.
|
|
19
|
+
*
|
|
20
|
+
* `defineTable` 내부에서만 사용합니다 (개발자가 제어하는 정적 값).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* escapeSchemaIdentifier('auth') // → '"auth"'
|
|
24
|
+
* escapeSchemaIdentifier('my"table') // → '"my""table"'
|
|
25
|
+
*/
|
|
26
|
+
export declare function escapeSchemaIdentifier(name: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* ORDER BY 방향(ASC / DESC)을 검증합니다.
|
|
29
|
+
* 유효하지 않으면 `logger.error` 후 `'ASC'` 를 반환합니다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateOrderDir(dir: string): 'ASC' | 'DESC';
|
|
32
|
+
/**
|
|
33
|
+
* 집계 함수명(COUNT / SUM / AVG / MIN / MAX)을 검증합니다.
|
|
34
|
+
* 유효하지 않으면 `logger.error` 후 `'COUNT'` 를 반환합니다.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateAggregateFn(fn: string): AggregateFn;
|
|
37
|
+
/**
|
|
38
|
+
* JOIN 타입(INNER / LEFT / RIGHT / FULL)을 검증합니다.
|
|
39
|
+
* 유효하지 않으면 `logger.error` 후 `'INNER'` 를 반환합니다.
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateJoinType(type: string): JoinType;
|
|
42
|
+
//# sourceMappingURL=sqlGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlGuard.d.ts","sourceRoot":"","sources":["../../src/utils/sqlGuard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,uCAAuC,CAAC;AAoB9E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAanD;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAS5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,CAS3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CASvD"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.quoteIdentifier = quoteIdentifier;
|
|
4
|
+
exports.escapeSchemaIdentifier = escapeSchemaIdentifier;
|
|
5
|
+
exports.validateOrderDir = validateOrderDir;
|
|
6
|
+
exports.validateAggregateFn = validateAggregateFn;
|
|
7
|
+
exports.validateJoinType = validateJoinType;
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
|
+
const logger = logger_1.Logger.fromEnv(process.env, { prefix: '[SqlGuard]' });
|
|
10
|
+
/**
|
|
11
|
+
* 유효한 SQL 식별자 패턴.
|
|
12
|
+
* - 단순 식별자: `[a-zA-Z_][a-zA-Z0-9_]*` 예: `first_name`, `userId`
|
|
13
|
+
* - 점 표기법: `schema.table`, `table.column`
|
|
14
|
+
*
|
|
15
|
+
* 공백, 세미콜론, 따옴표, 괄호 등 SQL 특수문자를 포함하는 경우 거부합니다.
|
|
16
|
+
*/
|
|
17
|
+
const IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
18
|
+
const VALID_ORDER_DIRS = new Set(['ASC', 'DESC']);
|
|
19
|
+
const VALID_AGG_FNS = new Set(['COUNT', 'SUM', 'AVG', 'MIN', 'MAX']);
|
|
20
|
+
const VALID_JOIN_TYPES = new Set(['INNER', 'LEFT', 'RIGHT', 'FULL']);
|
|
21
|
+
/**
|
|
22
|
+
* SQL 식별자(컬럼명, 테이블명 등)를 검증하고 PostgreSQL 표준으로 이스케이프합니다.
|
|
23
|
+
*
|
|
24
|
+
* - 유효한 식별자 패턴 `[a-zA-Z_][a-zA-Z0-9_.]*` 에 맞지 않으면
|
|
25
|
+
* `logger.error` 로 기록하고 `""` (빈 식별자) 를 반환합니다.
|
|
26
|
+
* - `"` 문자는 `""` 로 이중 이스케이프합니다 (PostgreSQL 표준).
|
|
27
|
+
* - `schema.table` / `table.column` 도트 표기법을 지원합니다.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* quoteIdentifier('first_name') // → '"first_name"'
|
|
31
|
+
* quoteIdentifier('auth.users') // → '"auth"."users"'
|
|
32
|
+
* quoteIdentifier('1; DROP TABLE')// → '""' + logger.error
|
|
33
|
+
*/
|
|
34
|
+
function quoteIdentifier(raw) {
|
|
35
|
+
if (!IDENTIFIER_RE.test(raw)) {
|
|
36
|
+
logger.error(`유효하지 않은 SQL 식별자 "${raw}". ` +
|
|
37
|
+
`허용 패턴: [a-zA-Z_][a-zA-Z0-9_]*(.[a-zA-Z_][a-zA-Z0-9_]*)*. ` +
|
|
38
|
+
`빈 식별자("")로 대체합니다.`);
|
|
39
|
+
return '""';
|
|
40
|
+
}
|
|
41
|
+
return raw
|
|
42
|
+
.split('.')
|
|
43
|
+
.map((part) => `"${part.replace(/"/g, '""')}"`)
|
|
44
|
+
.join('.');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 개발자가 정의하는 테이블/스키마 식별자를 이스케이프합니다.
|
|
48
|
+
* 패턴 검증 없이 `"` → `""` 이중 이스케이프만 수행합니다.
|
|
49
|
+
*
|
|
50
|
+
* `defineTable` 내부에서만 사용합니다 (개발자가 제어하는 정적 값).
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* escapeSchemaIdentifier('auth') // → '"auth"'
|
|
54
|
+
* escapeSchemaIdentifier('my"table') // → '"my""table"'
|
|
55
|
+
*/
|
|
56
|
+
function escapeSchemaIdentifier(name) {
|
|
57
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* ORDER BY 방향(ASC / DESC)을 검증합니다.
|
|
61
|
+
* 유효하지 않으면 `logger.error` 후 `'ASC'` 를 반환합니다.
|
|
62
|
+
*/
|
|
63
|
+
function validateOrderDir(dir) {
|
|
64
|
+
const upper = dir.toUpperCase();
|
|
65
|
+
if (!VALID_ORDER_DIRS.has(upper)) {
|
|
66
|
+
logger.error(`유효하지 않은 ORDER BY 방향 "${dir}". 허용 값: ASC, DESC. 'ASC'로 대체합니다.`);
|
|
67
|
+
return 'ASC';
|
|
68
|
+
}
|
|
69
|
+
return upper;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 집계 함수명(COUNT / SUM / AVG / MIN / MAX)을 검증합니다.
|
|
73
|
+
* 유효하지 않으면 `logger.error` 후 `'COUNT'` 를 반환합니다.
|
|
74
|
+
*/
|
|
75
|
+
function validateAggregateFn(fn) {
|
|
76
|
+
const upper = fn.toUpperCase();
|
|
77
|
+
if (!VALID_AGG_FNS.has(upper)) {
|
|
78
|
+
logger.error(`유효하지 않은 집계 함수 "${fn}". 허용 값: COUNT, SUM, AVG, MIN, MAX. 'COUNT'로 대체합니다.`);
|
|
79
|
+
return 'COUNT';
|
|
80
|
+
}
|
|
81
|
+
return upper;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* JOIN 타입(INNER / LEFT / RIGHT / FULL)을 검증합니다.
|
|
85
|
+
* 유효하지 않으면 `logger.error` 후 `'INNER'` 를 반환합니다.
|
|
86
|
+
*/
|
|
87
|
+
function validateJoinType(type) {
|
|
88
|
+
const upper = type.toUpperCase();
|
|
89
|
+
if (!VALID_JOIN_TYPES.has(upper)) {
|
|
90
|
+
logger.error(`유효하지 않은 JOIN 타입 "${type}". 허용 값: INNER, LEFT, RIGHT, FULL. 'INNER'로 대체합니다.`);
|
|
91
|
+
return 'INNER';
|
|
92
|
+
}
|
|
93
|
+
return upper;
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reltype",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|