@simplysm/orm-common 14.0.1 → 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 +135 -0
- package/dist/exec/queryable.js +18 -18
- package/dist/exec/queryable.js.map +1 -1
- package/dist/expr/expr.d.ts +102 -102
- package/dist/expr/expr.js +105 -105
- package/dist/expr/expr.js.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mssql/mssql-expr-renderer.js +17 -17
- package/dist/query-builder/mssql/mssql-expr-renderer.js.map +1 -1
- 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 +26 -26
- package/dist/query-builder/mssql/mssql-query-builder.js.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/mysql/mysql-expr-renderer.js +14 -14
- package/dist/query-builder/mysql/mysql-expr-renderer.js.map +1 -1
- 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 +67 -67
- package/dist/query-builder/mysql/mysql-query-builder.js.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.d.ts.map +1 -1
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js +13 -13
- package/dist/query-builder/postgresql/postgresql-expr-renderer.js.map +1 -1
- 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 +47 -47
- package/dist/query-builder/postgresql/postgresql-query-builder.js.map +1 -1
- package/dist/schema/factory/relation-builder.d.ts +8 -8
- package/dist/schema/factory/relation-builder.js +9 -9
- package/dist/schema/factory/relation-builder.js.map +1 -1
- package/dist/schema/view-builder.js +2 -2
- package/dist/schema/view-builder.js.map +1 -1
- package/dist/types/column.d.ts +21 -21
- package/dist/types/column.js +5 -5
- package/dist/utils/result-parser.d.ts +11 -11
- package/dist/utils/result-parser.js +48 -48
- package/dist/utils/result-parser.js.map +1 -1
- package/docs/core.md +206 -0
- package/docs/expression.md +217 -0
- package/docs/query-builder.md +126 -0
- package/docs/queryable.md +236 -0
- package/docs/schema-builders.md +352 -0
- package/docs/types.md +501 -0
- package/package.json +7 -3
- package/src/exec/queryable.ts +18 -18
- package/src/expr/expr.ts +105 -105
- package/src/query-builder/mssql/mssql-expr-renderer.ts +17 -17
- package/src/query-builder/mssql/mssql-query-builder.ts +26 -26
- package/src/query-builder/mysql/mysql-expr-renderer.ts +14 -14
- package/src/query-builder/mysql/mysql-query-builder.ts +67 -67
- package/src/query-builder/postgresql/postgresql-expr-renderer.ts +13 -13
- package/src/query-builder/postgresql/postgresql-query-builder.ts +47 -47
- package/src/schema/factory/relation-builder.ts +9 -9
- package/src/schema/view-builder.ts +2 -2
- package/src/types/column.ts +23 -23
- package/src/utils/result-parser.ts +49 -49
|
@@ -5,7 +5,7 @@ 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
|
/**
|
|
@@ -61,7 +61,7 @@ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// ============================================
|
|
64
|
-
//
|
|
64
|
+
// 그룹핑 유틸리티
|
|
65
65
|
// ============================================
|
|
66
66
|
|
|
67
67
|
/** flatToNested용 사전 계산된 column 메타데이터 */
|
|
@@ -127,7 +127,7 @@ function isEmptyObject(record: Record<string, unknown>): boolean {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// ============================================
|
|
130
|
-
//
|
|
130
|
+
// 메인 함수
|
|
131
131
|
// ============================================
|
|
132
132
|
|
|
133
133
|
/** 양보 간격: N개 레코드마다 이벤트 루프에 양보 */
|
|
@@ -140,28 +140,28 @@ const yieldToEventLoop: () => Promise<void> =
|
|
|
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,19 +178,19 @@ 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
|
|
|
@@ -241,7 +241,7 @@ 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,13 +251,13 @@ 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;
|
|
@@ -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__")) {
|
|
@@ -464,7 +464,7 @@ function mergeJoinData(
|
|
|
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
470
|
throw new Error(`isSingle 관계 '${localKey}'에 여러 개의 다른 결과가 있습니다.`);
|
|
@@ -473,7 +473,7 @@ function mergeJoinData(
|
|
|
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];
|