@simplysm/orm-common 13.0.100 → 14.0.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/README.md +90 -147
- package/dist/create-db-context.d.ts +10 -10
- package/dist/create-db-context.js +312 -276
- package/dist/create-db-context.js.map +1 -6
- package/dist/ddl/column-ddl.d.ts +4 -4
- package/dist/ddl/column-ddl.js +41 -35
- package/dist/ddl/column-ddl.js.map +1 -6
- package/dist/ddl/initialize.d.ts +17 -17
- package/dist/ddl/initialize.js +200 -142
- package/dist/ddl/initialize.js.map +1 -6
- package/dist/ddl/relation-ddl.d.ts +6 -6
- package/dist/ddl/relation-ddl.js +55 -48
- package/dist/ddl/relation-ddl.js.map +1 -6
- package/dist/ddl/schema-ddl.d.ts +4 -4
- package/dist/ddl/schema-ddl.js +21 -15
- package/dist/ddl/schema-ddl.js.map +1 -6
- package/dist/ddl/table-ddl.d.ts +20 -20
- package/dist/ddl/table-ddl.js +139 -93
- package/dist/ddl/table-ddl.js.map +1 -6
- package/dist/define-db-context.js +10 -13
- package/dist/define-db-context.js.map +1 -6
- package/dist/errors/db-transaction-error.d.ts +15 -15
- package/dist/errors/db-transaction-error.d.ts.map +1 -1
- package/dist/errors/db-transaction-error.js +53 -19
- package/dist/errors/db-transaction-error.js.map +1 -6
- package/dist/exec/executable.d.ts +23 -23
- package/dist/exec/executable.js +94 -40
- package/dist/exec/executable.js.map +1 -6
- package/dist/exec/queryable.d.ts +97 -97
- package/dist/exec/queryable.js +1310 -1204
- package/dist/exec/queryable.js.map +1 -6
- package/dist/exec/search-parser.d.ts +31 -31
- package/dist/exec/search-parser.d.ts.map +1 -1
- package/dist/exec/search-parser.js +158 -59
- package/dist/exec/search-parser.js.map +1 -6
- package/dist/expr/expr-unit.d.ts +4 -4
- package/dist/expr/expr-unit.js +24 -18
- package/dist/expr/expr-unit.js.map +1 -6
- package/dist/expr/expr.d.ts +108 -108
- package/dist/expr/expr.js +1872 -1844
- package/dist/expr/expr.js.map +1 -6
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -6
- package/dist/models/system-migration.js +7 -7
- package/dist/models/system-migration.js.map +1 -6
- package/dist/query-builder/base/expr-renderer-base.d.ts +10 -10
- package/dist/query-builder/base/expr-renderer-base.js +27 -21
- package/dist/query-builder/base/expr-renderer-base.js.map +1 -6
- package/dist/query-builder/base/query-builder-base.d.ts +21 -21
- package/dist/query-builder/base/query-builder-base.d.ts.map +1 -1
- package/dist/query-builder/base/query-builder-base.js +90 -80
- package/dist/query-builder/base/query-builder-base.js.map +1 -6
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +447 -420
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mssql/mssql-query-builder.d.ts +2 -2
- package/dist/query-builder/mssql/mssql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-query-builder.js +483 -443
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -6
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +451 -419
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -6
- package/dist/query-builder/mysql/mysql-query-builder.d.ts +10 -10
- package/dist/query-builder/mysql/mysql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-query-builder.js +570 -479
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +5 -5
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +449 -422
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -6
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts +8 -8
- package/dist/query-builder/postgresql/postgresql-query-builder.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-query-builder.js +511 -460
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -6
- package/dist/query-builder/query-builder.d.ts +1 -1
- package/dist/query-builder/query-builder.js +13 -13
- package/dist/query-builder/query-builder.js.map +1 -6
- package/dist/schema/factory/column-builder.d.ts +84 -84
- package/dist/schema/factory/column-builder.js +248 -185
- package/dist/schema/factory/column-builder.js.map +1 -6
- package/dist/schema/factory/index-builder.d.ts +38 -38
- package/dist/schema/factory/index-builder.js +144 -85
- package/dist/schema/factory/index-builder.js.map +1 -6
- package/dist/schema/factory/relation-builder.d.ts +99 -99
- package/dist/schema/factory/relation-builder.d.ts.map +1 -1
- package/dist/schema/factory/relation-builder.js +274 -136
- package/dist/schema/factory/relation-builder.js.map +1 -6
- package/dist/schema/procedure-builder.d.ts +51 -51
- package/dist/schema/procedure-builder.d.ts.map +1 -1
- package/dist/schema/procedure-builder.js +205 -131
- package/dist/schema/procedure-builder.js.map +1 -6
- package/dist/schema/table-builder.d.ts +55 -55
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +274 -205
- package/dist/schema/table-builder.js.map +1 -6
- package/dist/schema/view-builder.d.ts +44 -44
- package/dist/schema/view-builder.d.ts.map +1 -1
- package/dist/schema/view-builder.js +189 -116
- package/dist/schema/view-builder.js.map +1 -6
- package/dist/types/column.d.ts +21 -21
- package/dist/types/column.js +60 -30
- package/dist/types/column.js.map +1 -6
- package/dist/types/db-context-def.d.ts +9 -9
- package/dist/types/db-context-def.js +2 -1
- package/dist/types/db-context-def.js.map +1 -6
- package/dist/types/db.d.ts +47 -47
- package/dist/types/db.js +15 -5
- package/dist/types/db.js.map +1 -6
- package/dist/types/expr.d.ts +81 -81
- package/dist/types/expr.d.ts.map +1 -1
- package/dist/types/expr.js +3 -1
- package/dist/types/expr.js.map +1 -6
- package/dist/types/query-def.d.ts +46 -46
- package/dist/types/query-def.d.ts.map +1 -1
- package/dist/types/query-def.js +31 -24
- package/dist/types/query-def.js.map +1 -6
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +362 -221
- package/dist/utils/result-parser.js.map +1 -6
- package/docs/core.md +117 -145
- package/docs/expression.md +186 -203
- package/docs/query-builder.md +75 -42
- package/docs/queryable.md +189 -151
- package/docs/schema-builders.md +172 -283
- package/docs/types.md +229 -173
- package/package.json +7 -5
- package/src/create-db-context.ts +31 -31
- package/src/ddl/column-ddl.ts +4 -4
- package/src/ddl/initialize.ts +38 -38
- package/src/ddl/relation-ddl.ts +6 -6
- package/src/ddl/schema-ddl.ts +4 -4
- package/src/ddl/table-ddl.ts +24 -24
- package/src/errors/db-transaction-error.ts +13 -13
- package/src/exec/executable.ts +25 -25
- package/src/exec/queryable.ts +152 -152
- package/src/exec/search-parser.ts +50 -50
- package/src/expr/expr-unit.ts +4 -4
- package/src/expr/expr.ts +118 -118
- package/src/index.ts +8 -8
- package/src/models/system-migration.ts +1 -1
- package/src/query-builder/base/expr-renderer-base.ts +21 -21
- package/src/query-builder/base/query-builder-base.ts +33 -33
- package/src/query-builder/mssql/mssql-expr-renderer.ts +28 -28
- package/src/query-builder/mssql/mssql-query-builder.ts +37 -37
- package/src/query-builder/mysql/mysql-expr-renderer.ts +29 -29
- package/src/query-builder/mysql/mysql-query-builder.ts +70 -70
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +22 -22
- package/src/query-builder/postgresql/postgresql-query-builder.ts +54 -54
- package/src/query-builder/query-builder.ts +1 -1
- package/src/schema/factory/column-builder.ts +86 -86
- package/src/schema/factory/index-builder.ts +38 -38
- package/src/schema/factory/relation-builder.ts +102 -102
- package/src/schema/procedure-builder.ts +52 -52
- package/src/schema/table-builder.ts +56 -56
- package/src/schema/view-builder.ts +47 -47
- package/src/types/column.ts +24 -24
- package/src/types/db-context-def.ts +15 -15
- package/src/types/db.ts +50 -50
- package/src/types/expr.ts +103 -103
- package/src/types/query-def.ts +50 -50
- package/src/utils/result-parser.ts +88 -88
- package/docs/utilities.md +0 -27
- package/tests/db-context/create-db-context.spec.ts +0 -193
- package/tests/db-context/define-db-context.spec.ts +0 -17
- package/tests/ddl/basic.expected.ts +0 -341
- package/tests/ddl/basic.spec.ts +0 -557
- package/tests/ddl/column-builder.expected.ts +0 -310
- package/tests/ddl/column-builder.spec.ts +0 -525
- package/tests/ddl/index-builder.expected.ts +0 -38
- package/tests/ddl/index-builder.spec.ts +0 -148
- package/tests/ddl/procedure-builder.expected.ts +0 -52
- package/tests/ddl/procedure-builder.spec.ts +0 -128
- package/tests/ddl/relation-builder.expected.ts +0 -36
- package/tests/ddl/relation-builder.spec.ts +0 -171
- package/tests/ddl/table-builder.expected.ts +0 -113
- package/tests/ddl/table-builder.spec.ts +0 -399
- package/tests/ddl/view-builder.expected.ts +0 -38
- package/tests/ddl/view-builder.spec.ts +0 -116
- package/tests/dml/delete.expected.ts +0 -96
- package/tests/dml/delete.spec.ts +0 -127
- package/tests/dml/insert.expected.ts +0 -192
- package/tests/dml/insert.spec.ts +0 -210
- package/tests/dml/update.expected.ts +0 -176
- package/tests/dml/update.spec.ts +0 -222
- package/tests/dml/upsert.expected.ts +0 -215
- package/tests/dml/upsert.spec.ts +0 -190
- package/tests/errors/queryable-errors.spec.ts +0 -126
- package/tests/escape.spec.ts +0 -59
- package/tests/examples/pivot.expected.ts +0 -211
- package/tests/examples/pivot.spec.ts +0 -200
- package/tests/examples/sampling.expected.ts +0 -69
- package/tests/examples/sampling.spec.ts +0 -42
- package/tests/examples/unpivot.expected.ts +0 -120
- package/tests/examples/unpivot.spec.ts +0 -161
- package/tests/exec/search-parser.spec.ts +0 -267
- package/tests/executable/basic.expected.ts +0 -18
- package/tests/executable/basic.spec.ts +0 -54
- package/tests/expr/comparison.expected.ts +0 -282
- package/tests/expr/comparison.spec.ts +0 -334
- package/tests/expr/conditional.expected.ts +0 -134
- package/tests/expr/conditional.spec.ts +0 -249
- package/tests/expr/date.expected.ts +0 -332
- package/tests/expr/date.spec.ts +0 -459
- package/tests/expr/math.expected.ts +0 -62
- package/tests/expr/math.spec.ts +0 -59
- package/tests/expr/string.expected.ts +0 -218
- package/tests/expr/string.spec.ts +0 -300
- package/tests/expr/utility.expected.ts +0 -147
- package/tests/expr/utility.spec.ts +0 -155
- package/tests/select/basic.expected.ts +0 -322
- package/tests/select/basic.spec.ts +0 -433
- package/tests/select/filter.expected.ts +0 -357
- package/tests/select/filter.spec.ts +0 -954
- package/tests/select/group.expected.ts +0 -169
- package/tests/select/group.spec.ts +0 -159
- package/tests/select/join.expected.ts +0 -582
- package/tests/select/join.spec.ts +0 -692
- package/tests/select/order.expected.ts +0 -150
- package/tests/select/order.spec.ts +0 -140
- package/tests/select/recursive-cte.expected.ts +0 -244
- package/tests/select/recursive-cte.spec.ts +0 -514
- package/tests/select/result-meta.spec.ts +0 -270
- package/tests/select/subquery.expected.ts +0 -363
- package/tests/select/subquery.spec.ts +0 -441
- package/tests/select/view.expected.ts +0 -155
- package/tests/select/view.spec.ts +0 -235
- package/tests/select/window.expected.ts +0 -345
- package/tests/select/window.spec.ts +0 -433
- package/tests/setup/MockExecutor.ts +0 -18
- package/tests/setup/TestDbContext.ts +0 -59
- package/tests/setup/models/Company.ts +0 -13
- package/tests/setup/models/Employee.ts +0 -10
- package/tests/setup/models/MonthlySales.ts +0 -11
- package/tests/setup/models/Post.ts +0 -16
- package/tests/setup/models/Sales.ts +0 -10
- package/tests/setup/models/User.ts +0 -19
- package/tests/setup/procedure/GetAllUsers.ts +0 -9
- package/tests/setup/procedure/GetUserById.ts +0 -12
- package/tests/setup/test-utils.ts +0 -72
- package/tests/setup/views/ActiveUsers.ts +0 -8
- package/tests/setup/views/UserSummary.ts +0 -11
- package/tests/types/nullable-queryable-record.spec.ts +0 -97
- package/tests/utils/result-parser-perf.spec.ts +0 -143
- package/tests/utils/result-parser.spec.ts +0 -667
|
@@ -5,19 +5,19 @@ import type { ResultMeta } from "../types/db";
|
|
|
5
5
|
declare function setImmediate(callback: () => void): void;
|
|
6
6
|
|
|
7
7
|
// ============================================
|
|
8
|
-
//
|
|
8
|
+
// 타입 파서
|
|
9
9
|
// ============================================
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 값을 지정된 타입으로 파싱
|
|
13
13
|
*
|
|
14
|
-
* @param value -
|
|
15
|
-
* @param type -
|
|
16
|
-
* @returns
|
|
17
|
-
* @throws
|
|
14
|
+
* @param value - 파싱할 값
|
|
15
|
+
* @param type - 대상 타입 (ColumnPrimitiveStr)
|
|
16
|
+
* @returns 파싱된 값
|
|
17
|
+
* @throws 파싱 실패 시 Error
|
|
18
18
|
*/
|
|
19
19
|
function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
|
|
20
|
-
// null/undefined
|
|
20
|
+
// null/undefined는 그대로 반환 (key 제거는 호출자가 처리)
|
|
21
21
|
if (value == null) {
|
|
22
22
|
return undefined;
|
|
23
23
|
}
|
|
@@ -26,7 +26,7 @@ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
|
|
|
26
26
|
case "number": {
|
|
27
27
|
const num = Number(value);
|
|
28
28
|
if (Number.isNaN(num)) {
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error(`숫자 파싱 실패: ${String(value)}`);
|
|
30
30
|
}
|
|
31
31
|
return num;
|
|
32
32
|
}
|
|
@@ -35,7 +35,7 @@ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
|
|
|
35
35
|
return String(value);
|
|
36
36
|
|
|
37
37
|
case "boolean":
|
|
38
|
-
//
|
|
38
|
+
// 0, 1, "0", "1", true, false 등 처리
|
|
39
39
|
if (value === 0 || value === "0" || value === false) return false;
|
|
40
40
|
if (value === 1 || value === "1" || value === true) return true;
|
|
41
41
|
return Boolean(value);
|
|
@@ -56,22 +56,22 @@ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
|
|
|
56
56
|
case "Bytes":
|
|
57
57
|
if (value instanceof Uint8Array) return value;
|
|
58
58
|
if (typeof value === "string") return bytes.fromHex(value);
|
|
59
|
-
throw new Error(`
|
|
59
|
+
throw new Error(`Bytes 파싱 실패: ${typeof value}`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// ============================================
|
|
64
|
-
//
|
|
64
|
+
// 그룹핑 유틸리티
|
|
65
65
|
// ============================================
|
|
66
66
|
|
|
67
|
-
/**
|
|
67
|
+
/** flatToNested용 사전 계산된 column 메타데이터 */
|
|
68
68
|
interface ColumnInfo {
|
|
69
69
|
key: string;
|
|
70
70
|
type: ColumnPrimitiveStr;
|
|
71
|
-
parts: string[] | undefined; // undefined
|
|
71
|
+
parts: string[] | undefined; // 단순 key는 undefined, 중첩 key는 string[]
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/** 고유한 columns 객체마다 column 정보를 한 번만 사전 계산 */
|
|
75
75
|
function buildColumnInfos(columns: Record<string, ColumnPrimitiveStr>): ColumnInfo[] {
|
|
76
76
|
return Object.entries(columns).map(([key, type]) => ({
|
|
77
77
|
key,
|
|
@@ -81,7 +81,7 @@ function buildColumnInfos(columns: Record<string, ColumnPrimitiveStr>): ColumnIn
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
84
|
+
* 플랫 레코드를 중첩 객체로 변환
|
|
85
85
|
*
|
|
86
86
|
* @example
|
|
87
87
|
* { "posts.id": 1, "posts.title": "Hi" } → { posts: { id: 1, title: "Hi" } }
|
|
@@ -96,11 +96,11 @@ function flatToNested(
|
|
|
96
96
|
const rawValue = record[key];
|
|
97
97
|
const parsedValue = parseValue(rawValue, type);
|
|
98
98
|
|
|
99
|
-
// undefined
|
|
99
|
+
// undefined 값은 key로 추가하지 않음
|
|
100
100
|
if (parsedValue === undefined) continue;
|
|
101
101
|
|
|
102
102
|
if (parts != null) {
|
|
103
|
-
//
|
|
103
|
+
// 중첩 key: "posts.id" → { posts: { id: ... } }
|
|
104
104
|
let current = result;
|
|
105
105
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
106
106
|
const part = parts[i];
|
|
@@ -111,7 +111,7 @@ function flatToNested(
|
|
|
111
111
|
}
|
|
112
112
|
current[parts[parts.length - 1]] = parsedValue;
|
|
113
113
|
} else {
|
|
114
|
-
//
|
|
114
|
+
// 단순 key
|
|
115
115
|
result[key] = parsedValue;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -120,48 +120,48 @@ function flatToNested(
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
123
|
+
* 객체가 비어있는지 확인 (모든 값이 undefined)
|
|
124
124
|
*/
|
|
125
125
|
function isEmptyObject(record: Record<string, unknown>): boolean {
|
|
126
126
|
return Object.keys(record).length === 0;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// ============================================
|
|
130
|
-
//
|
|
130
|
+
// 메인 함수
|
|
131
131
|
// ============================================
|
|
132
132
|
|
|
133
|
-
/**
|
|
133
|
+
/** 양보 간격: N개 레코드마다 이벤트 루프에 양보 */
|
|
134
134
|
const YIELD_INTERVAL = 100;
|
|
135
135
|
|
|
136
|
-
/**
|
|
136
|
+
/** 이벤트 루프 양보: Node.js는 setImmediate, 브라우저는 setTimeout 폴백 */
|
|
137
137
|
const yieldToEventLoop: () => Promise<void> =
|
|
138
138
|
typeof setImmediate !== "undefined"
|
|
139
139
|
? () => new Promise<void>((resolve) => setImmediate(resolve))
|
|
140
140
|
: () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* ResultMeta를 통해 DB 쿼리 결과를 TypeScript 객체로 변환
|
|
144
144
|
*
|
|
145
|
-
* @param rawResults -
|
|
146
|
-
* @param meta -
|
|
147
|
-
* @returns
|
|
148
|
-
* @throws
|
|
145
|
+
* @param rawResults - 데이터베이스에서 반환된 원시 결과 배열
|
|
146
|
+
* @param meta - 타입 변환 및 JOIN 구조 정보 (필수)
|
|
147
|
+
* @returns 타입 변환 및 중첩된 결과 배열. 입력이 비어있거나 유효한 결과가 없으면 undefined 반환
|
|
148
|
+
* @throws 타입 파싱 실패 시 Error
|
|
149
149
|
*
|
|
150
150
|
* @remarks
|
|
151
|
-
* - meta
|
|
152
|
-
* - async
|
|
153
|
-
* -
|
|
154
|
-
* -
|
|
151
|
+
* - meta 필수: meta 없이는 이 함수를 호출할 필요 없음 (입력 = 출력)
|
|
152
|
+
* - async 전용: 대규모 처리 시 외부 인터럽트 허용을 위해 동기 버전 미제공
|
|
153
|
+
* - 브라우저/Node 호환: setTimeout(resolve, 0)으로 양보
|
|
154
|
+
* - 빈 결과 처리: 입력 배열이 비어있거나 파싱 후 모든 레코드가 빈 객체이면 undefined 반환
|
|
155
155
|
*
|
|
156
156
|
* @example
|
|
157
157
|
* ```typescript
|
|
158
|
-
* // 1.
|
|
158
|
+
* // 1. 단순 타입 파싱
|
|
159
159
|
* const raw = [{ id: "1", createdAt: "2026-01-07T10:00:00.000Z" }];
|
|
160
160
|
* const meta = { columns: { id: "number", createdAt: "DateTime" }, joins: {} };
|
|
161
161
|
* const result = await parseQueryResult(raw, meta);
|
|
162
162
|
* // [{ id: 1, createdAt: DateTime(...) }]
|
|
163
163
|
*
|
|
164
|
-
* // 2. JOIN
|
|
164
|
+
* // 2. JOIN 결과 중첩
|
|
165
165
|
* const raw = [
|
|
166
166
|
* { id: 1, name: "User1", "posts.id": 10, "posts.title": "Post1" },
|
|
167
167
|
* { id: 1, name: "User1", "posts.id": 11, "posts.title": "Post2" },
|
|
@@ -178,24 +178,24 @@ export async function parseQueryResult<TRecord>(
|
|
|
178
178
|
rawResults: Record<string, unknown>[],
|
|
179
179
|
meta: ResultMeta,
|
|
180
180
|
): Promise<TRecord[] | undefined> {
|
|
181
|
-
//
|
|
181
|
+
// 빈 입력 처리
|
|
182
182
|
if (rawResults.length === 0) {
|
|
183
183
|
return undefined;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const joinKeys = Object.keys(meta.joins);
|
|
187
187
|
|
|
188
|
-
//
|
|
188
|
+
// JOIN 없음: 단순 타입 파싱만 수행
|
|
189
189
|
if (joinKeys.length === 0) {
|
|
190
190
|
return parseSimpleRecords<TRecord>(rawResults, meta.columns);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
//
|
|
193
|
+
// JOIN 있음: 그룹핑 + 중첩
|
|
194
194
|
return parseJoinedRecords<TRecord>(rawResults, meta);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
*
|
|
198
|
+
* JOIN이 없는 단순 레코드 파싱
|
|
199
199
|
*/
|
|
200
200
|
async function parseSimpleRecords<TRecord>(
|
|
201
201
|
rawResults: Record<string, unknown>[],
|
|
@@ -205,43 +205,43 @@ async function parseSimpleRecords<TRecord>(
|
|
|
205
205
|
const results: Record<string, unknown>[] = [];
|
|
206
206
|
|
|
207
207
|
for (let i = 0; i < rawResults.length; i++) {
|
|
208
|
-
//
|
|
208
|
+
// 이벤트 루프에 양보
|
|
209
209
|
if (i > 0 && i % YIELD_INTERVAL === 0) {
|
|
210
210
|
await yieldToEventLoop();
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
const parsed = flatToNested(rawResults[i], columnInfos);
|
|
214
214
|
|
|
215
|
-
//
|
|
215
|
+
// 빈 객체 제외
|
|
216
216
|
if (!isEmptyObject(parsed)) {
|
|
217
217
|
results.push(parsed);
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
//
|
|
221
|
+
// 빈 배열은 undefined 반환
|
|
222
222
|
return results.length > 0 ? (results as TRecord[]) : undefined;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
*
|
|
226
|
+
* JOIN key를 깊이순으로 정렬 (얕은 것 우선)
|
|
227
227
|
* "posts" (1) < "posts.comments" (2)
|
|
228
228
|
*/
|
|
229
229
|
function sortJoinKeysByDepth(joinKeys: string[]): string[] {
|
|
230
230
|
return [...joinKeys].sort((a, b) => {
|
|
231
231
|
const depthA = a.split(".").length;
|
|
232
232
|
const depthB = b.split(".").length;
|
|
233
|
-
return depthA - depthB; //
|
|
233
|
+
return depthA - depthB; // 얕은 것 우선
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
|
-
*
|
|
238
|
+
* JOIN이 있는 레코드 파싱 (재귀 그룹핑)
|
|
239
239
|
*/
|
|
240
240
|
async function parseJoinedRecords<TRecord>(
|
|
241
241
|
rawResults: Record<string, unknown>[],
|
|
242
242
|
meta: ResultMeta,
|
|
243
243
|
): Promise<TRecord[] | undefined> {
|
|
244
|
-
// 1.
|
|
244
|
+
// 1. 모든 레코드를 중첩 구조로 변환
|
|
245
245
|
const columnInfos = buildColumnInfos(meta.columns);
|
|
246
246
|
const nestedRecords: Record<string, unknown>[] = [];
|
|
247
247
|
for (let i = 0; i < rawResults.length; i++) {
|
|
@@ -251,22 +251,22 @@ async function parseJoinedRecords<TRecord>(
|
|
|
251
251
|
nestedRecords.push(flatToNested(rawResults[i], columnInfos));
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
// 2.
|
|
254
|
+
// 2. JOIN key를 깊이순으로 정렬 (얕은 것 우선)
|
|
255
255
|
const sortedJoinKeys = sortJoinKeysByDepth(Object.keys(meta.joins));
|
|
256
256
|
|
|
257
|
-
// 3.
|
|
257
|
+
// 3. 루트 레벨부터 재귀적으로 그룹핑
|
|
258
258
|
const results = groupRecordsRecursively(nestedRecords, sortedJoinKeys, meta.joins, "");
|
|
259
259
|
|
|
260
|
-
// 4.
|
|
260
|
+
// 4. 빈 결과 필터링
|
|
261
261
|
const filteredResults = results.filter((r) => !isEmptyObject(r));
|
|
262
262
|
|
|
263
263
|
return filteredResults.length > 0 ? (filteredResults as TRecord[]) : undefined;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
267
|
+
* 그룹 key를 문자열로 직렬화 (Map key로 사용)
|
|
268
268
|
*
|
|
269
|
-
*
|
|
269
|
+
* JSON.stringify보다 빠른 커스텀 직렬화
|
|
270
270
|
*/
|
|
271
271
|
function serializeGroupKey(groupKey: Record<string, unknown>, cachedKeyOrder?: string[]): string {
|
|
272
272
|
const keys = cachedKeyOrder ?? Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
|
|
@@ -282,14 +282,14 @@ function serializeGroupKey(groupKey: Record<string, unknown>, cachedKeyOrder?: s
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
|
-
*
|
|
285
|
+
* 현재 경로에 대해 레코드를 재귀적으로 그룹핑
|
|
286
286
|
*
|
|
287
|
-
*
|
|
287
|
+
* Map 기반 그룹핑으로 O(n) 복잡도 달성
|
|
288
288
|
*
|
|
289
|
-
* @param records -
|
|
290
|
-
* @param allJoinKeys -
|
|
291
|
-
* @param joinsConfig - JOIN
|
|
292
|
-
* @param currentPath -
|
|
289
|
+
* @param records - 그룹핑할 레코드 배열
|
|
290
|
+
* @param allJoinKeys - 모든 JOIN key (깊이순 정렬)
|
|
291
|
+
* @param joinsConfig - JOIN 설정
|
|
292
|
+
* @param currentPath - 현재 경로 (예: "", "posts", "posts.comments")
|
|
293
293
|
*/
|
|
294
294
|
function groupRecordsRecursively(
|
|
295
295
|
records: Record<string, unknown>[],
|
|
@@ -297,15 +297,15 @@ function groupRecordsRecursively(
|
|
|
297
297
|
joinsConfig: Record<string, { isSingle: boolean }>,
|
|
298
298
|
currentPath: string,
|
|
299
299
|
): Record<string, unknown>[] {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
300
|
+
// 현재 경로에 직접 대응하는 JOIN key 찾기
|
|
301
|
+
// 예: currentPath="" → ["posts", "company"]
|
|
302
|
+
// 예: currentPath="posts" → ["posts.comments"]
|
|
303
303
|
const childJoinKeys = allJoinKeys.filter((key) => {
|
|
304
304
|
if (currentPath === "") {
|
|
305
|
-
//
|
|
305
|
+
// 루트 레벨: 점이 없는 key
|
|
306
306
|
return !key.includes(".");
|
|
307
307
|
} else {
|
|
308
|
-
//
|
|
308
|
+
// 하위 레벨: 현재 경로 + "." + key
|
|
309
309
|
return (
|
|
310
310
|
key.startsWith(currentPath + ".") && key.slice(currentPath.length + 1).indexOf(".") === -1
|
|
311
311
|
);
|
|
@@ -313,21 +313,21 @@ function groupRecordsRecursively(
|
|
|
313
313
|
});
|
|
314
314
|
|
|
315
315
|
if (childJoinKeys.length === 0) {
|
|
316
|
-
//
|
|
316
|
+
// 더 이상 그룹핑할 JOIN 없음
|
|
317
317
|
return records;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// Map
|
|
320
|
+
// Map 기반 그룹핑 (O(n) 복잡도)
|
|
321
321
|
const groupMap = new Map<string, Record<string, unknown>>();
|
|
322
322
|
|
|
323
|
-
//
|
|
323
|
+
// O(1) 조회를 위한 JOIN key 제외 집합 사전 계산
|
|
324
324
|
const joinKeyExclusions = buildJoinKeyExclusionSet(childJoinKeys);
|
|
325
325
|
|
|
326
|
-
// Key
|
|
326
|
+
// Key 순서 캐싱 (첫 번째 레코드에서 결정 후 재사용)
|
|
327
327
|
let groupKeyOrder: string[] | undefined;
|
|
328
328
|
|
|
329
329
|
for (const record of records) {
|
|
330
|
-
//
|
|
330
|
+
// 그룹 key 추출 및 직렬화 (JOIN key 제외)
|
|
331
331
|
const groupKey = extractGroupKey(record, joinKeyExclusions);
|
|
332
332
|
if (groupKeyOrder == null) {
|
|
333
333
|
groupKeyOrder = Object.keys(groupKey).sort((a, b) => a.localeCompare(b));
|
|
@@ -337,27 +337,27 @@ function groupRecordsRecursively(
|
|
|
337
337
|
const existingGroup = groupMap.get(keyStr);
|
|
338
338
|
|
|
339
339
|
if (existingGroup != null) {
|
|
340
|
-
//
|
|
340
|
+
// 기존 그룹에 JOIN 데이터 병합
|
|
341
341
|
for (const joinKey of childJoinKeys) {
|
|
342
342
|
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
343
343
|
mergeJoinData(existingGroup, record, localKey, joinsConfig[joinKey].isSingle);
|
|
344
344
|
}
|
|
345
345
|
} else {
|
|
346
|
-
//
|
|
346
|
+
// 새 그룹 생성
|
|
347
347
|
const newGroup = { ...record };
|
|
348
348
|
|
|
349
|
-
//
|
|
349
|
+
// 각 JOIN key를 배열 또는 단일 객체로 초기화
|
|
350
350
|
for (const joinKey of childJoinKeys) {
|
|
351
351
|
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
352
352
|
const joinData = newGroup[localKey] as Record<string, unknown> | undefined;
|
|
353
353
|
|
|
354
354
|
if (joinData != null && !isEmptyObject(joinData)) {
|
|
355
355
|
if (!joinsConfig[joinKey].isSingle) {
|
|
356
|
-
//
|
|
356
|
+
// 배열로 변환
|
|
357
357
|
newGroup[localKey] = [joinData];
|
|
358
358
|
}
|
|
359
359
|
} else {
|
|
360
|
-
//
|
|
360
|
+
// 데이터가 비어있으면 key 삭제
|
|
361
361
|
delete newGroup[localKey];
|
|
362
362
|
}
|
|
363
363
|
}
|
|
@@ -366,17 +366,17 @@ function groupRecordsRecursively(
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
//
|
|
369
|
+
// Map을 배열로 변환
|
|
370
370
|
const grouped = Array.from(groupMap.values());
|
|
371
371
|
|
|
372
|
-
//
|
|
372
|
+
// 각 JOIN의 하위 레벨을 재귀적으로 처리
|
|
373
373
|
for (const group of grouped) {
|
|
374
374
|
for (const joinKey of childJoinKeys) {
|
|
375
375
|
const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
|
|
376
376
|
const joinData = group[localKey];
|
|
377
377
|
|
|
378
378
|
if (Array.isArray(joinData) && joinData.length > 0) {
|
|
379
|
-
//
|
|
379
|
+
// 배열인 경우: 하위 레벨을 재귀적으로 처리
|
|
380
380
|
group[localKey] = groupRecordsRecursively(
|
|
381
381
|
joinData as Record<string, unknown>[],
|
|
382
382
|
allJoinKeys,
|
|
@@ -384,7 +384,7 @@ function groupRecordsRecursively(
|
|
|
384
384
|
joinKey,
|
|
385
385
|
);
|
|
386
386
|
} else if (joinData != null && typeof joinData === "object" && !Array.isArray(joinData)) {
|
|
387
|
-
//
|
|
387
|
+
// 단일 객체인 경우 (isSingle: true)
|
|
388
388
|
const processed = groupRecordsRecursively(
|
|
389
389
|
[joinData as Record<string, unknown>],
|
|
390
390
|
allJoinKeys,
|
|
@@ -398,7 +398,7 @@ function groupRecordsRecursively(
|
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
//
|
|
401
|
+
// __hashSet__ 내부 속성 제거 (중복 검사용 임시 속성)
|
|
402
402
|
for (const group of grouped) {
|
|
403
403
|
for (const key of Object.keys(group)) {
|
|
404
404
|
if (key.startsWith("__hashSet__")) {
|
|
@@ -411,13 +411,13 @@ function groupRecordsRecursively(
|
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
|
-
*
|
|
414
|
+
* 그룹 key에서 제외할 key의 Set 구성 (join key와 그 접두사)
|
|
415
415
|
*/
|
|
416
416
|
function buildJoinKeyExclusionSet(joinKeys: string[]): Set<string> {
|
|
417
417
|
const exclusions = new Set<string>();
|
|
418
418
|
for (const jk of joinKeys) {
|
|
419
419
|
exclusions.add(jk);
|
|
420
|
-
//
|
|
420
|
+
// 상위 경로도 제외 (예: join key "posts.comments"에 대해 "posts")
|
|
421
421
|
const parts = jk.split(".");
|
|
422
422
|
for (let i = 1; i < parts.length; i++) {
|
|
423
423
|
exclusions.add(parts.slice(0, i).join("."));
|
|
@@ -427,7 +427,7 @@ function buildJoinKeyExclusionSet(joinKeys: string[]): Set<string> {
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
/**
|
|
430
|
-
*
|
|
430
|
+
* JOIN key를 제외하고 레코드에서 그룹 key 추출
|
|
431
431
|
*/
|
|
432
432
|
function extractGroupKey(
|
|
433
433
|
record: Record<string, unknown>,
|
|
@@ -435,9 +435,9 @@ function extractGroupKey(
|
|
|
435
435
|
): Record<string, unknown> {
|
|
436
436
|
const result: Record<string, unknown> = {};
|
|
437
437
|
for (const [key, value] of Object.entries(record)) {
|
|
438
|
-
//
|
|
438
|
+
// JOIN이 아닌 key만 포함
|
|
439
439
|
if (!joinKeyExclusions.has(key)) {
|
|
440
|
-
//
|
|
440
|
+
// 프리미티브 값만 그룹 key로 사용 (객체/배열 제외)
|
|
441
441
|
if (value == null || typeof value !== "object") {
|
|
442
442
|
result[key] = value;
|
|
443
443
|
}
|
|
@@ -447,7 +447,7 @@ function extractGroupKey(
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
/**
|
|
450
|
-
*
|
|
450
|
+
* 기존 그룹에 JOIN 데이터 병합
|
|
451
451
|
*/
|
|
452
452
|
function mergeJoinData(
|
|
453
453
|
existingGroup: Record<string, unknown>,
|
|
@@ -458,29 +458,29 @@ function mergeJoinData(
|
|
|
458
458
|
const newJoinData = newRecord[localKey] as Record<string, unknown> | undefined;
|
|
459
459
|
|
|
460
460
|
if (newJoinData == null || isEmptyObject(newJoinData)) {
|
|
461
|
-
return; //
|
|
461
|
+
return; // 병합할 데이터 없음
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
const existingJoinData = existingGroup[localKey];
|
|
465
465
|
|
|
466
466
|
if (isSingle) {
|
|
467
|
-
// isSingle: true -
|
|
467
|
+
// isSingle: true - 데이터가 존재하고 값이 다르면 에러
|
|
468
468
|
if (existingJoinData != null) {
|
|
469
469
|
if (!obj.equal(existingJoinData as Record<string, unknown>, newJoinData)) {
|
|
470
|
-
throw new Error(`isSingle
|
|
470
|
+
throw new Error(`isSingle 관계 '${localKey}'에 여러 개의 다른 결과가 있습니다.`);
|
|
471
471
|
}
|
|
472
472
|
} else {
|
|
473
473
|
existingGroup[localKey] = newJoinData;
|
|
474
474
|
}
|
|
475
475
|
} else {
|
|
476
|
-
// isSingle: false →
|
|
476
|
+
// isSingle: false → 배열에 추가
|
|
477
477
|
const hashSetKey = `__hashSet__${localKey}`;
|
|
478
478
|
if (!Array.isArray(existingJoinData)) {
|
|
479
479
|
existingGroup[localKey] = [newJoinData];
|
|
480
|
-
//
|
|
480
|
+
// Set 기반 중복 검사용 내부 속성 초기화
|
|
481
481
|
existingGroup[hashSetKey] = new Set([serializeGroupKey(newJoinData)]);
|
|
482
482
|
} else {
|
|
483
|
-
// Set
|
|
483
|
+
// Set 기반 중복 검사 (O(1))
|
|
484
484
|
const hashSet = existingGroup[hashSetKey] as Set<string> | undefined;
|
|
485
485
|
const newHash = serializeGroupKey(newJoinData);
|
|
486
486
|
if (hashSet != null) {
|
|
@@ -489,7 +489,7 @@ function mergeJoinData(
|
|
|
489
489
|
existingJoinData.push(newJoinData);
|
|
490
490
|
}
|
|
491
491
|
} else {
|
|
492
|
-
//
|
|
492
|
+
// hashSet 없는 폴백 (레거시 방식)
|
|
493
493
|
const isDuplicate = existingJoinData.some((item) =>
|
|
494
494
|
obj.equal(item as Record<string, unknown>, newJoinData),
|
|
495
495
|
);
|
package/docs/utilities.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Utilities
|
|
2
|
-
|
|
3
|
-
## `parseQueryResult`
|
|
4
|
-
|
|
5
|
-
Transform raw DB query results to typed TypeScript objects via `ResultMeta`. Handles type parsing, nested object construction from flat JOIN results, and deduplication.
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
async function parseQueryResult<TRecord>(
|
|
9
|
-
rawResults: Record<string, unknown>[],
|
|
10
|
-
meta: ResultMeta,
|
|
11
|
-
): Promise<TRecord[] | undefined>;
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
| Parameter | Type | Description |
|
|
15
|
-
|-----------|------|-------------|
|
|
16
|
-
| `rawResults` | `Record<string, unknown>[]` | Raw result array from database |
|
|
17
|
-
| `meta` | `ResultMeta` | Type transformation and JOIN structure information |
|
|
18
|
-
|
|
19
|
-
**Returns:** Type-transformed and nested result array. Returns `undefined` if input is empty or no valid results.
|
|
20
|
-
|
|
21
|
-
### Behavior
|
|
22
|
-
|
|
23
|
-
- **Type parsing**: Converts raw values to TypeScript types based on `meta.columns` mapping (e.g., string to `DateTime`, number to boolean).
|
|
24
|
-
- **JOIN nesting**: Converts flat `"posts.id"` keys into nested `{ posts: { id: ... } }` structures.
|
|
25
|
-
- **Deduplication**: Groups records by non-JOIN columns and collects JOIN data into arrays (or single objects for `isSingle: true`).
|
|
26
|
-
- **Async**: Yields to the event loop every 100 records to prevent blocking.
|
|
27
|
-
- **Empty handling**: Returns `undefined` for empty input or all-empty parsed records.
|