@simplysm/orm-common 14.0.48 → 14.0.50

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.
Files changed (58) hide show
  1. package/README.md +39 -172
  2. package/dist/db-context.d.ts +6 -6
  3. package/dist/db-context.d.ts.map +1 -1
  4. package/dist/db-context.js.map +1 -1
  5. package/dist/ddl/initialize.js.map +1 -1
  6. package/dist/ddl/table-ddl.d.ts +3 -3
  7. package/dist/ddl/table-ddl.d.ts.map +1 -1
  8. package/dist/ddl/table-ddl.js.map +1 -1
  9. package/dist/exec/queryable.d.ts +10 -10
  10. package/dist/exec/queryable.d.ts.map +1 -1
  11. package/dist/exec/queryable.js.map +1 -1
  12. package/dist/models/system-migration.d.ts +1 -1
  13. package/dist/schema/factory/relation-builder.d.ts +17 -17
  14. package/dist/schema/factory/relation-builder.d.ts.map +1 -1
  15. package/dist/schema/factory/relation-builder.js.map +1 -1
  16. package/dist/schema/table-builder.d.ts +12 -12
  17. package/dist/schema/table-builder.d.ts.map +1 -1
  18. package/dist/schema/table-builder.js +1 -1
  19. package/dist/schema/table-builder.js.map +1 -1
  20. package/dist/types/db-context-def.d.ts +4 -4
  21. package/dist/types/db-context-def.d.ts.map +1 -1
  22. package/dist/utils/result-parser.js.map +1 -1
  23. package/docs/core/db-context.md +208 -0
  24. package/docs/core/db-transaction-error.md +64 -0
  25. package/docs/expression/expr-unit.md +62 -0
  26. package/docs/expression/expr.md +198 -0
  27. package/docs/models/migration.md +37 -0
  28. package/docs/query-builder/create-query-builder.md +80 -0
  29. package/docs/queryable-executable/executable.md +54 -0
  30. package/docs/queryable-executable/parse-search-query.md +75 -0
  31. package/docs/queryable-executable/queryable.md +238 -0
  32. package/docs/schema-builders/column-builder.md +63 -0
  33. package/docs/schema-builders/foreign-key-builder.md +137 -0
  34. package/docs/schema-builders/index-builder.md +54 -0
  35. package/docs/schema-builders/procedure.md +67 -0
  36. package/docs/schema-builders/table.md +94 -0
  37. package/docs/schema-builders/view.md +71 -0
  38. package/docs/types/data-type.md +146 -0
  39. package/docs/types/dialect.md +151 -0
  40. package/docs/types/expr.md +175 -0
  41. package/docs/types/parse-query-result.md +58 -0
  42. package/docs/types/query-def.md +224 -0
  43. package/package.json +2 -2
  44. package/src/db-context.ts +8 -8
  45. package/src/ddl/initialize.ts +4 -4
  46. package/src/ddl/table-ddl.ts +3 -3
  47. package/src/exec/queryable.ts +21 -21
  48. package/src/schema/factory/relation-builder.ts +45 -38
  49. package/src/schema/table-builder.ts +13 -12
  50. package/src/types/db-context-def.ts +4 -4
  51. package/src/utils/result-parser.ts +57 -57
  52. package/docs/core.md +0 -188
  53. package/docs/expression.md +0 -190
  54. package/docs/models.md +0 -17
  55. package/docs/query-builder.md +0 -97
  56. package/docs/queryable-executable.md +0 -311
  57. package/docs/schema-builders.md +0 -364
  58. package/docs/types.md +0 -537
@@ -36,8 +36,8 @@ import type { ViewBuilder } from "../view-builder";
36
36
  * @see {@link RelationKeyBuilder} DB FK 없는 관계
37
37
  */
38
38
  export class ForeignKeyBuilder<
39
- TOwner extends TableBuilder<any, any>,
40
- TTargetFn extends () => TableBuilder<any, any>,
39
+ TOwner extends TableBuilder<any, any, any>,
40
+ TTargetFn extends () => TableBuilder<any, any, any>,
41
41
  > {
42
42
  /**
43
43
  * @param meta - FK 메타데이터
@@ -91,7 +91,7 @@ export class ForeignKeyBuilder<
91
91
  * @see {@link ForeignKeyBuilder} FK builder
92
92
  */
93
93
  export class ForeignKeyTargetBuilder<
94
- TTargetTableFn extends () => TableBuilder<any, any>,
94
+ TTargetTableFn extends () => TableBuilder<any, any, any>,
95
95
  TIsSingle extends boolean,
96
96
  > {
97
97
  /**
@@ -140,8 +140,8 @@ export class ForeignKeyTargetBuilder<
140
140
  * @see {@link ForeignKeyBuilder} DB FK 생성 버전
141
141
  */
142
142
  export class RelationKeyBuilder<
143
- TOwner extends TableBuilder<any, any> | ViewBuilder<any, any, any>,
144
- TTargetFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
143
+ TOwner extends TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
144
+ TTargetFn extends () => TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
145
145
  > {
146
146
  /**
147
147
  * @param meta - 관계 메타데이터
@@ -186,7 +186,7 @@ export class RelationKeyBuilder<
186
186
  * @see {@link ForeignKeyTargetBuilder} DB FK 생성 버전
187
187
  */
188
188
  export class RelationKeyTargetBuilder<
189
- TTargetTableFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
189
+ TTargetTableFn extends () => TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
190
190
  TIsSingle extends boolean,
191
191
  > {
192
192
  /**
@@ -212,20 +212,20 @@ export class RelationKeyTargetBuilder<
212
212
  * @template TOwner - 소유 Table builder 타입
213
213
  * @template TColumnKey - Column key 타입
214
214
  */
215
- type RelationFkFactory<TOwner extends TableBuilder<any, any>, TColumnKey extends string> = {
215
+ type RelationFkFactory<TOwner extends TableBuilder<any, any, any>, TColumnKey extends string> = {
216
216
  /** N:1 FK 관계 정의 (DB FK 생성) */
217
- foreignKey<TTargetFn extends () => TableBuilder<any, any>>(
217
+ foreignKey<TTargetFn extends () => TableBuilder<any, any, any>>(
218
218
  columns: TColumnKey[],
219
219
  targetFn: TTargetFn,
220
220
  opts?: { description?: string },
221
221
  ): ForeignKeyBuilder<TOwner, TTargetFn>;
222
222
  /** 1:N FK 역참조 정의 (single: true → 단일 객체) */
223
- foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any>>(
223
+ foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any, any>>(
224
224
  targetTableFn: TTargetTableFn,
225
225
  relationName: string,
226
226
  opts: { single: true; description?: string },
227
227
  ): ForeignKeyTargetBuilder<TTargetTableFn, true>;
228
- foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any>>(
228
+ foreignKeyTarget<TTargetTableFn extends () => TableBuilder<any, any, any>>(
229
229
  targetTableFn: TTargetTableFn,
230
230
  relationName: string,
231
231
  opts?: { single?: false; description?: string },
@@ -239,25 +239,25 @@ type RelationFkFactory<TOwner extends TableBuilder<any, any>, TColumnKey extends
239
239
  * @template TColumnKey - Column key 타입
240
240
  */
241
241
  type RelationRkFactory<
242
- TOwner extends TableBuilder<any, any> | ViewBuilder<any, any, any>,
242
+ TOwner extends TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
243
243
  TColumnKey extends string,
244
244
  > = {
245
245
  /** N:1 논리적 관계 정의 (DB FK 미생성) */
246
- relationKey<TTargetFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>>(
246
+ relationKey<TTargetFn extends () => TableBuilder<any, any, any> | ViewBuilder<any, any, any>>(
247
247
  columns: TColumnKey[],
248
248
  targetFn: TTargetFn,
249
249
  opts?: { description?: string },
250
250
  ): RelationKeyBuilder<TOwner, TTargetFn>;
251
251
  /** 1:N 논리적 역참조 정의 (single: true → 단일 객체) */
252
252
  relationKeyTarget<
253
- TTargetTableFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
253
+ TTargetTableFn extends () => TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
254
254
  >(
255
255
  targetTableFn: TTargetTableFn,
256
256
  relationName: string,
257
257
  opts: { single: true; description?: string },
258
258
  ): RelationKeyTargetBuilder<TTargetTableFn, true>;
259
259
  relationKeyTarget<
260
- TTargetTableFn extends () => TableBuilder<any, any> | ViewBuilder<any, any, any>,
260
+ TTargetTableFn extends () => TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
261
261
  >(
262
262
  targetTableFn: TTargetTableFn,
263
263
  relationName: string,
@@ -297,17 +297,17 @@ type RelationRkFactory<
297
297
  * ```
298
298
  */
299
299
  export function createRelationFactory<
300
- TOwner extends TableBuilder<any, any> | ViewBuilder<any, any, any>,
300
+ TOwner extends TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
301
301
  TColumnKey extends string,
302
302
  >(
303
303
  ownerFn: () => TOwner,
304
- ): TOwner extends TableBuilder<any, any>
304
+ ): TOwner extends TableBuilder<any, any, any>
305
305
  ? RelationFkFactory<TOwner, TColumnKey> & RelationRkFactory<TOwner, TColumnKey>
306
306
  : RelationRkFactory<TOwner, TColumnKey> {
307
307
  return {
308
308
  foreignKey(columns, targetFn, opts?) {
309
309
  return new ForeignKeyBuilder({
310
- ownerFn: ownerFn as () => TableBuilder<any, any>,
310
+ ownerFn: ownerFn as () => TableBuilder<any, any, any>,
311
311
  columns,
312
312
  targetFn,
313
313
  description: opts?.description,
@@ -337,7 +337,7 @@ export function createRelationFactory<
337
337
  isSingle: opts?.single,
338
338
  });
339
339
  },
340
- } as TOwner extends TableBuilder<any, any>
340
+ } as TOwner extends TableBuilder<any, any, any>
341
341
  ? RelationFkFactory<TOwner, TColumnKey> & RelationRkFactory<TOwner, TColumnKey>
342
342
  : RelationRkFactory<TOwner, TColumnKey>;
343
343
  }
@@ -370,13 +370,15 @@ export type RelationBuilderRecord = Record<
370
370
  *
371
371
  * @template T - FK 또는 RelationKey builder 타입
372
372
  */
373
- export type ExtractRelationTarget<TRelation> = TRelation extends
373
+ export type ExtractRelationTarget<TRelation, TVisited extends string = never> = TRelation extends
374
374
  | ForeignKeyBuilder<any, infer TTargetFn>
375
375
  | RelationKeyBuilder<any, infer TTargetFn>
376
- ? ReturnType<TTargetFn> extends TableBuilder<infer TCols, infer TRels>
377
- ? InferColumns<TCols> & InferDeepRelations<TRels>
376
+ ? ReturnType<TTargetFn> extends TableBuilder<infer TName, infer TCols, infer TRels>
377
+ ? TName extends TVisited
378
+ ? InferColumns<TCols>
379
+ : InferColumns<TCols> & InferDeepRelations<TRels, TVisited | TName>
378
380
  : ReturnType<TTargetFn> extends ViewBuilder<any, infer TData, infer TRels>
379
- ? TData & InferDeepRelations<TRels>
381
+ ? TData & InferDeepRelations<TRels, TVisited>
380
382
  : never
381
383
  : never;
382
384
 
@@ -388,19 +390,24 @@ export type ExtractRelationTarget<TRelation> = TRelation extends
388
390
  *
389
391
  * @template T - FKTarget 또는 RelationKeyTarget builder 타입
390
392
  */
391
- export type ExtractRelationTargetResult<TRelation> = TRelation extends
392
- | ForeignKeyTargetBuilder<infer TTargetTableFn, infer TIsSingle>
393
- | RelationKeyTargetBuilder<infer TTargetTableFn, infer TIsSingle>
394
- ? ReturnType<TTargetTableFn> extends TableBuilder<infer TCols, infer TRels>
395
- ? TIsSingle extends true
396
- ? InferColumns<TCols> & InferDeepRelations<TRels>
397
- : (InferColumns<TCols> & InferDeepRelations<TRels>)[]
398
- : ReturnType<TTargetTableFn> extends ViewBuilder<any, infer TData, infer TRels>
399
- ? TIsSingle extends true
400
- ? TData & InferDeepRelations<TRels>
401
- : (TData & InferDeepRelations<TRels>)[]
402
- : never
403
- : never;
393
+ export type ExtractRelationTargetResult<TRelation, TVisited extends string = never> =
394
+ TRelation extends
395
+ | ForeignKeyTargetBuilder<infer TTargetTableFn, infer TIsSingle>
396
+ | RelationKeyTargetBuilder<infer TTargetTableFn, infer TIsSingle>
397
+ ? ReturnType<TTargetTableFn> extends TableBuilder<infer TName, infer TCols, infer TRels>
398
+ ? TName extends TVisited
399
+ ? TIsSingle extends true
400
+ ? InferColumns<TCols>
401
+ : InferColumns<TCols>[]
402
+ : TIsSingle extends true
403
+ ? InferColumns<TCols> & InferDeepRelations<TRels, TVisited | TName>
404
+ : (InferColumns<TCols> & InferDeepRelations<TRels, TVisited | TName>)[]
405
+ : ReturnType<TTargetTableFn> extends ViewBuilder<any, infer TData, infer TRels>
406
+ ? TIsSingle extends true
407
+ ? TData & InferDeepRelations<TRels, TVisited>
408
+ : (TData & InferDeepRelations<TRels, TVisited>)[]
409
+ : never
410
+ : never;
404
411
 
405
412
  /**
406
413
  * 관계 정의에서 심층 관계 타입 추론
@@ -415,8 +422,8 @@ export type ExtractRelationTargetResult<TRelation> = TRelation extends
415
422
  * // { posts?: Post[]; profile?: Profile; }
416
423
  * ```
417
424
  */
418
- export type InferDeepRelations<TRelations extends RelationBuilderRecord> = {
425
+ export type InferDeepRelations<TRelations extends RelationBuilderRecord, TVisited extends string = never> = {
419
426
  [K in keyof TRelations]?:
420
- | ExtractRelationTarget<TRelations[K]>
421
- | ExtractRelationTargetResult<TRelations[K]>;
427
+ | ExtractRelationTarget<TRelations[K], TVisited>
428
+ | ExtractRelationTargetResult<TRelations[K], TVisited>;
422
429
  };
@@ -49,6 +49,7 @@ import {
49
49
  * @see {@link queryable} Queryable 생성
50
50
  */
51
51
  export class TableBuilder<
52
+ TName extends string,
52
53
  TColumns extends ColumnBuilderRecord,
53
54
  TRelations extends RelationBuilderRecord,
54
55
  > {
@@ -57,7 +58,7 @@ export class TableBuilder<
57
58
  /** 관계 정의 (타입 추론용) */
58
59
  readonly $relations!: TRelations;
59
60
 
60
- /** 전체 타입 추론 (column + 관계) */
61
+ /** 전체 타입 추론 (column + 관계) — 순환 감지: 같은 테이블 재방문 시 끊김 */
61
62
  readonly $inferSelect!: InferColumns<TColumns> & InferDeepRelations<TRelations>;
62
63
  /** Column 전용 타입 추론 */
63
64
  readonly $inferColumns!: InferColumns<TColumns>;
@@ -79,7 +80,7 @@ export class TableBuilder<
79
80
  */
80
81
  constructor(
81
82
  readonly meta: {
82
- name: string;
83
+ name: TName;
83
84
  description?: string;
84
85
  database?: string;
85
86
  schema?: string;
@@ -97,7 +98,7 @@ export class TableBuilder<
97
98
  * @param desc - Table 설명 (DDL Comment로 사용됨)
98
99
  * @returns 새 TableBuilder 인스턴스
99
100
  */
100
- description(desc: string) {
101
+ description(desc: string): TableBuilder<TName, TColumns, TRelations> {
101
102
  return new TableBuilder({ ...this.meta, description: desc });
102
103
  }
103
104
 
@@ -112,7 +113,7 @@ export class TableBuilder<
112
113
  * const User = Table("User").database("mydb");
113
114
  * ```
114
115
  */
115
- database(db: string) {
116
+ database(db: string): TableBuilder<TName, TColumns, TRelations> {
116
117
  return new TableBuilder({ ...this.meta, database: db });
117
118
  }
118
119
 
@@ -131,7 +132,7 @@ export class TableBuilder<
131
132
  * .schema("custom_schema");
132
133
  * ```
133
134
  */
134
- schema(schema: string) {
135
+ schema(schema: string): TableBuilder<TName, TColumns, TRelations> {
135
136
  return new TableBuilder({ ...this.meta, schema });
136
137
  }
137
138
 
@@ -157,8 +158,8 @@ export class TableBuilder<
157
158
  */
158
159
  columns<TNewColumnDefs extends ColumnBuilderRecord>(
159
160
  fn: (c: ReturnType<typeof createColumnFactory>) => TNewColumnDefs,
160
- ) {
161
- return new TableBuilder<TNewColumnDefs, TRelations>({
161
+ ): TableBuilder<TName, TNewColumnDefs, TRelations> {
162
+ return new TableBuilder<TName, TNewColumnDefs, TRelations>({
162
163
  ...this.meta,
163
164
  columns: fn(createColumnFactory()),
164
165
  });
@@ -186,7 +187,7 @@ export class TableBuilder<
186
187
  * .primaryKey("userId", "roleId");
187
188
  * ```
188
189
  */
189
- primaryKey(...columns: (keyof TColumns & string)[]) {
190
+ primaryKey(...columns: (keyof TColumns & string)[]): TableBuilder<TName, TColumns, TRelations> {
190
191
  return new TableBuilder({
191
192
  ...this.meta,
192
193
  primaryKey: columns,
@@ -217,7 +218,7 @@ export class TableBuilder<
217
218
  fn: (
218
219
  i: ReturnType<typeof createIndexFactory<keyof TColumns & string>>,
219
220
  ) => IndexBuilder<string[]>[],
220
- ) {
221
+ ): TableBuilder<TName, TColumns, TRelations> {
221
222
  return new TableBuilder({
222
223
  ...this.meta,
223
224
  indexes: fn(createIndexFactory<keyof TColumns & string>()),
@@ -264,7 +265,7 @@ export class TableBuilder<
264
265
  */
265
266
  relations<T extends RelationBuilderRecord>(
266
267
  fn: (r: ReturnType<typeof createRelationFactory<this, keyof TColumns & string>>) => T,
267
- ): TableBuilder<TColumns, T> {
268
+ ): TableBuilder<TName, TColumns, T> {
268
269
  return new TableBuilder({
269
270
  ...this.meta,
270
271
  relations: fn(createRelationFactory<this, keyof TColumns & string>(() => this)),
@@ -314,6 +315,6 @@ export class TableBuilder<
314
315
  *
315
316
  * @see {@link TableBuilder} builder 클래스
316
317
  */
317
- export function Table(name: string) {
318
- return new TableBuilder({ name });
318
+ export function Table<TName extends string>(name: TName) {
319
+ return new TableBuilder<TName, ColumnBuilderRecord, RelationBuilderRecord>({ name });
319
320
  }
@@ -24,7 +24,7 @@ export interface DbContextBase {
24
24
  resultMetas?: (ResultMeta | undefined)[],
25
25
  ): Promise<T[][]>;
26
26
  getQueryDefObjectName(
27
- tableOrView: TableBuilder<any, any> | ViewBuilder<any, any, any>,
27
+ tableOrView: TableBuilder<any, any, any> | ViewBuilder<any, any, any>,
28
28
  ): QueryDefObjectName;
29
29
  switchFk(table: QueryDefObjectName, enabled: boolean): Promise<void>;
30
30
  }
@@ -32,7 +32,7 @@ export interface DbContextBase {
32
32
  export type DbContextStatus = "ready" | "connect" | "transact";
33
33
 
34
34
  export interface DbContextDdlMethods {
35
- createTable(table: TableBuilder<any, any>): Promise<void>;
35
+ createTable(table: TableBuilder<any, any, any>): Promise<void>;
36
36
  dropTable(table: QueryDefObjectName): Promise<void>;
37
37
  renameTable(table: QueryDefObjectName, newName: string): Promise<void>;
38
38
  createView(view: ViewBuilder<any, any, any>): Promise<void>;
@@ -66,11 +66,11 @@ export interface DbContextDdlMethods {
66
66
  truncate(table: QueryDefObjectName): Promise<void>;
67
67
  switchFk(table: QueryDefObjectName, enabled: boolean): Promise<void>;
68
68
  // QueryDef 생성기
69
- getCreateTableQueryDef(table: TableBuilder<any, any>): QueryDef;
69
+ getCreateTableQueryDef(table: TableBuilder<any, any, any>): QueryDef;
70
70
  getCreateViewQueryDef(view: ViewBuilder<any, any, any>): QueryDef;
71
71
  getCreateProcQueryDef(procedure: ProcedureBuilder<any, any>): QueryDef;
72
72
  getCreateObjectQueryDef(
73
- builder: TableBuilder<any, any> | ViewBuilder<any, any, any> | ProcedureBuilder<any, any>,
73
+ builder: TableBuilder<any, any, any> | ViewBuilder<any, any, any> | ProcedureBuilder<any, any>,
74
74
  ): QueryDef;
75
75
  getDropTableQueryDef(table: QueryDefObjectName): QueryDef;
76
76
  getRenameTableQueryDef(table: QueryDefObjectName, newName: string): QueryDef;
@@ -16,11 +16,11 @@ declare function setImmediate(callback: () => void): void;
16
16
  * @returns 파싱된 값
17
17
  * @throws 파싱 실패 시 Error
18
18
  */
19
- function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
20
- // undefined는 key 제거 대상으로 남기고, null은 SQL 결과값으로 보존한다.
21
- if (value == null) {
22
- return value;
23
- }
19
+ function parseValue(value: unknown, type: ColumnPrimitiveStr): unknown {
20
+ // undefined는 key 제거 대상으로 남기고, null은 SQL 결과값으로 보존한다.
21
+ if (value == null) {
22
+ return value;
23
+ }
24
24
 
25
25
  switch (type) {
26
26
  case "number": {
@@ -93,11 +93,11 @@ function flatToNested(
93
93
  const result: Record<string, unknown> = {};
94
94
 
95
95
  for (const { key, type, parts } of columnInfos) {
96
- const rawValue = record[key];
97
- if (typeof rawValue === "undefined") continue;
98
- const parsedValue = parseValue(rawValue, type);
99
-
100
- if (parts != null) {
96
+ const rawValue = record[key];
97
+ if (typeof rawValue === "undefined") continue;
98
+ const parsedValue = parseValue(rawValue, type);
99
+
100
+ if (parts != null) {
101
101
  // 중첩 key: "posts.id" → { posts: { id: ... } }
102
102
  let current = result;
103
103
  for (let i = 0; i < parts.length - 1; i++) {
@@ -120,45 +120,45 @@ function flatToNested(
120
120
  /**
121
121
  * 객체가 비어있는지 확인 (모든 값이 undefined)
122
122
  */
123
- function isEmptyObject(record: Record<string, unknown>): boolean {
124
- return Object.keys(record).length === 0;
125
- }
126
-
127
- /**
128
- * JOIN 결과 객체가 실질적으로 비어있는지 확인
129
- *
130
- * 모든 값이 null/undefined 이거나, 중첩 객체도 재귀적으로 비어있으면 비어있는 JOIN으로 간주한다.
131
- */
132
- function isEmptyJoinObject(record: Record<string, unknown>): boolean {
133
- const entries = Object.entries(record);
134
- if (entries.length === 0) {
135
- return true;
136
- }
137
-
138
- for (const [, value] of entries) {
139
- if (value == null) {
140
- continue;
141
- }
142
-
143
- if (Array.isArray(value)) {
144
- if (value.length > 0) {
145
- return false;
146
- }
147
- continue;
148
- }
149
-
150
- if (typeof value === "object") {
151
- if (!isEmptyJoinObject(value as Record<string, unknown>)) {
152
- return false;
153
- }
154
- continue;
155
- }
156
-
157
- return false;
158
- }
159
-
160
- return true;
161
- }
123
+ function isEmptyObject(record: Record<string, unknown>): boolean {
124
+ return Object.keys(record).length === 0;
125
+ }
126
+
127
+ /**
128
+ * JOIN 결과 객체가 실질적으로 비어있는지 확인
129
+ *
130
+ * 모든 값이 null/undefined 이거나, 중첩 객체도 재귀적으로 비어있으면 비어있는 JOIN으로 간주한다.
131
+ */
132
+ function isEmptyJoinObject(record: Record<string, unknown>): boolean {
133
+ const entries = Object.entries(record);
134
+ if (entries.length === 0) {
135
+ return true;
136
+ }
137
+
138
+ for (const [, value] of entries) {
139
+ if (value == null) {
140
+ continue;
141
+ }
142
+
143
+ if (Array.isArray(value)) {
144
+ if (value.length > 0) {
145
+ return false;
146
+ }
147
+ continue;
148
+ }
149
+
150
+ if (typeof value === "object") {
151
+ if (!isEmptyJoinObject(value as Record<string, unknown>)) {
152
+ return false;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ return false;
158
+ }
159
+
160
+ return true;
161
+ }
162
162
 
163
163
  // ============================================
164
164
  // 메인 함수
@@ -401,10 +401,10 @@ function groupRecordsRecursively(
401
401
  const localKey = currentPath === "" ? joinKey : joinKey.slice(currentPath.length + 1);
402
402
  const joinData = newGroup[localKey] as Record<string, unknown> | undefined;
403
403
 
404
- if (joinData != null && !isEmptyJoinObject(joinData)) {
405
- if (!joinsConfig[joinKey].isSingle) {
406
- // 배열로 변환 (hashSet은 첫 merge 시 초기화)
407
- newGroup[localKey] = [joinData];
404
+ if (joinData != null && !isEmptyJoinObject(joinData)) {
405
+ if (!joinsConfig[joinKey].isSingle) {
406
+ // 배열로 변환 (hashSet은 첫 merge 시 초기화)
407
+ newGroup[localKey] = [joinData];
408
408
  }
409
409
  } else {
410
410
  // 데이터가 비어있으면 key 삭제
@@ -500,16 +500,16 @@ function mergeJoinData(
500
500
  ): void {
501
501
  const newJoinData = newRecord[localKey] as Record<string, unknown> | undefined;
502
502
 
503
- if (newJoinData == null || isEmptyJoinObject(newJoinData)) {
504
- return; // 병합할 데이터 없음
505
- }
503
+ if (newJoinData == null || isEmptyJoinObject(newJoinData)) {
504
+ return; // 병합할 데이터 없음
505
+ }
506
506
 
507
507
  const existingJoinData = existingGroup[localKey];
508
508
 
509
509
  if (isSingle) {
510
510
  // isSingle: true - 데이터가 존재하고 값이 다르면 에러
511
511
  if (existingJoinData != null) {
512
- if (!obj.equal(existingJoinData as Record<string, unknown>, newJoinData)) {
512
+ if (!obj.equal(existingJoinData, newJoinData)) {
513
513
  throw new Error(`isSingle 관계 '${localKey}'에 여러 개의 다른 결과가 있습니다.`);
514
514
  }
515
515
  } else {
package/docs/core.md DELETED
@@ -1,188 +0,0 @@
1
- # Core
2
-
3
- ## `DbContext`
4
-
5
- DB 연결/트랜잭션/DDL/초기화를 제공하는 추상 클래스. `queryable()`/`executable()`로 테이블/프로시저를 등록한다. 각 프로퍼티가 독립 직렬화되어 40+ 테이블에서도 TS7056이 발생하지 않는다.
6
-
7
- ```typescript
8
- export abstract class DbContext implements DbContextBase {
9
- status: DbContextStatus;
10
- migrations: Migration[];
11
-
12
- constructor(
13
- executor: DbContextExecutor,
14
- opt: { database: string; schema?: string },
15
- );
16
-
17
- // 등록 메서드
18
- protected queryable<T extends TableBuilder | ViewBuilder>(builder: T): () => Queryable;
19
- protected executable<T extends ProcedureBuilder>(builder: T): () => Executable;
20
-
21
- // 연결 관리
22
- connect<TResult>(fn: () => Promise<TResult>, isolationLevel?: IsolationLevel): Promise<TResult>;
23
- connectWithoutTransaction<TResult>(callback: () => Promise<TResult>): Promise<TResult>;
24
- transaction<TResult>(fn: () => Promise<TResult>, isolationLevel?: IsolationLevel): Promise<TResult>;
25
-
26
- // DDL 실행 메서드
27
- createTable(table: TableBuilder): Promise<void>;
28
- dropTable(table: QueryDefObjectName): Promise<void>;
29
- renameTable(table: QueryDefObjectName, newName: string): Promise<void>;
30
- createView(view: ViewBuilder): Promise<void>;
31
- dropView(view: QueryDefObjectName): Promise<void>;
32
- createProc(procedure: ProcedureBuilder): Promise<void>;
33
- dropProc(procedure: QueryDefObjectName): Promise<void>;
34
- addColumn(table: QueryDefObjectName, columnName: string, column: ColumnBuilder): Promise<void>;
35
- dropColumn(table: QueryDefObjectName, column: string): Promise<void>;
36
- modifyColumn(table: QueryDefObjectName, columnName: string, column: ColumnBuilder): Promise<void>;
37
- renameColumn(table: QueryDefObjectName, column: string, newName: string): Promise<void>;
38
- addPrimaryKey(table: QueryDefObjectName, columns: string[]): Promise<void>;
39
- dropPrimaryKey(table: QueryDefObjectName): Promise<void>;
40
- addForeignKey(table: QueryDefObjectName, relationName: string, relationDef: ForeignKeyBuilder): Promise<void>;
41
- addIndex(table: QueryDefObjectName, indexBuilder: IndexBuilder): Promise<void>;
42
- dropForeignKey(table: QueryDefObjectName, relationName: string): Promise<void>;
43
- dropIndex(table: QueryDefObjectName, columns: string[]): Promise<void>;
44
- clearSchema(params: { database: string; schema?: string }): Promise<void>;
45
- schemaExists(database: string, schema?: string): Promise<boolean>;
46
- truncate(table: QueryDefObjectName): Promise<void>;
47
- switchFk(table: QueryDefObjectName, enabled: boolean): Promise<void>;
48
-
49
- // DDL QueryDef 생성기 (getCreateTableQueryDef, getDropTableQueryDef, ... 등 동일 시그니처)
50
- getCreateTableQueryDef(table: TableBuilder): QueryDef;
51
- getCreateViewQueryDef(view: ViewBuilder): QueryDef;
52
- getCreateProcQueryDef(procedure: ProcedureBuilder): QueryDef;
53
- getCreateObjectQueryDef(builder: TableBuilder | ViewBuilder | ProcedureBuilder): QueryDef;
54
- getDropTableQueryDef(table: QueryDefObjectName): QueryDef;
55
- getRenameTableQueryDef(table: QueryDefObjectName, newName: string): QueryDef;
56
- getDropViewQueryDef(view: QueryDefObjectName): QueryDef;
57
- getDropProcQueryDef(procedure: QueryDefObjectName): QueryDef;
58
- getAddColumnQueryDef(table: QueryDefObjectName, columnName: string, column: ColumnBuilder): QueryDef;
59
- getDropColumnQueryDef(table: QueryDefObjectName, column: string): QueryDef;
60
- getModifyColumnQueryDef(table: QueryDefObjectName, columnName: string, column: ColumnBuilder): QueryDef;
61
- getRenameColumnQueryDef(table: QueryDefObjectName, column: string, newName: string): QueryDef;
62
- getAddPrimaryKeyQueryDef(table: QueryDefObjectName, columns: string[]): QueryDef;
63
- getDropPrimaryKeyQueryDef(table: QueryDefObjectName): QueryDef;
64
- getAddForeignKeyQueryDef(table: QueryDefObjectName, relationName: string, relationDef: ForeignKeyBuilder): QueryDef;
65
- getAddIndexQueryDef(table: QueryDefObjectName, indexBuilder: IndexBuilder): QueryDef;
66
- getDropForeignKeyQueryDef(table: QueryDefObjectName, relationName: string): QueryDef;
67
- getDropIndexQueryDef(table: QueryDefObjectName, columns: string[]): QueryDef;
68
- getClearSchemaQueryDef(params: { database: string; schema?: string }): QueryDef;
69
- getSchemaExistsQueryDef(database: string, schema?: string): QueryDef;
70
- getTruncateQueryDef(table: QueryDefObjectName): QueryDef;
71
- getSwitchFkQueryDef(table: QueryDefObjectName, enabled: boolean): QueryDef;
72
-
73
- // 초기화
74
- initialize(options?: { dbs?: string[]; force?: boolean }): Promise<boolean>;
75
-
76
- // DbContextBase 구현
77
- get database(): string | undefined;
78
- get schema(): string | undefined;
79
- getNextAlias(): string;
80
- resetAliasCounter(): void;
81
- executeDefs<T>(defs: QueryDef[], resultMetas?: (ResultMeta | undefined)[]): Promise<T[][]>;
82
- getQueryDefObjectName(tableOrView: TableBuilder | ViewBuilder): QueryDefObjectName;
83
- }
84
- ```
85
-
86
- ### 연결/트랜잭션 패턴
87
-
88
- | 메서드 | 용도 |
89
- |--------|------|
90
- | `connect(fn)` | 연결 -> 트랜잭션 시작 -> fn -> 커밋 -> 종료. 일반 DML 작업에 사용 |
91
- | `connectWithoutTransaction(fn)` | 연결 -> fn -> 종료. 트랜잭션 없는 DDL/읽기 전용 작업에 사용 |
92
- | `transaction(fn)` | 이미 연결된 상태에서 부분 트랜잭션 시작. `connectWithoutTransaction` 내부에서 사용 |
93
-
94
- - `connect()`/`connectWithoutTransaction()`은 status가 `"ready"`가 아니면 에러를 던진다 (재진입 방지).
95
- - 트랜잭션 중 DDL 실행은 런타임 에러를 발생시킨다.
96
- - 롤백 실패 시 원래 에러의 `cause`에 롤백 에러를 첨부하여 전파한다.
97
-
98
- ## `SD_BUILDER`
99
-
100
- DbContext의 queryable/executable 프로퍼티에 부착된 builder 참조용 Symbol.
101
-
102
- ```typescript
103
- export const SD_BUILDER: unique symbol;
104
- ```
105
-
106
- ## `DbContextBase`
107
-
108
- Queryable, Executable, ViewBuilder에서 사용하는 DbContext 내부 인터페이스. DbContext class가 이 인터페이스를 구현한다.
109
-
110
- ```typescript
111
- export interface DbContextBase {
112
- status: DbContextStatus;
113
- readonly database: string | undefined;
114
- readonly schema: string | undefined;
115
- getNextAlias(): string;
116
- resetAliasCounter(): void;
117
- executeDefs<T = DataRecord>(defs: QueryDef[], resultMetas?: (ResultMeta | undefined)[]): Promise<T[][]>;
118
- getQueryDefObjectName(tableOrView: TableBuilder | ViewBuilder): QueryDefObjectName;
119
- switchFk(table: QueryDefObjectName, enabled: boolean): Promise<void>;
120
- }
121
- ```
122
-
123
- | Field | Type | Description |
124
- |-------|------|-------------|
125
- | `status` | `DbContextStatus` | 현재 연결 상태 |
126
- | `database` | `string \| undefined` | 데이터베이스 이름 |
127
- | `schema` | `string \| undefined` | 스키마 이름 |
128
-
129
- ## `DbContextStatus`
130
-
131
- DbContext 상태를 나타내는 문자열 리터럴 유니온.
132
-
133
- ```typescript
134
- export type DbContextStatus = "ready" | "connect" | "transact";
135
- ```
136
-
137
- ## `DbContextDdlMethods`
138
-
139
- DDL 실행 메서드 인터페이스. DbContext에서 사용하는 모든 DDL 메서드 시그니처를 정의한다. DDL 실행 메서드와 DDL QueryDef 생성 메서드를 모두 포함한다.
140
-
141
- ```typescript
142
- export interface DbContextDdlMethods {
143
- createTable(table: TableBuilder): Promise<void>;
144
- dropTable(table: QueryDefObjectName): Promise<void>;
145
- // ... (DbContext의 DDL 메서드와 동일한 시그니처)
146
- }
147
- ```
148
-
149
- ## `DbErrorCode`
150
-
151
- 트랜잭션 관련 에러 코드. DBMS별 네이티브 에러 코드를 추상화한다.
152
-
153
- ```typescript
154
- export enum DbErrorCode {
155
- NO_ACTIVE_TRANSACTION = "NO_ACTIVE_TRANSACTION",
156
- TRANSACTION_ALREADY_STARTED = "TRANSACTION_ALREADY_STARTED",
157
- DEADLOCK = "DEADLOCK",
158
- LOCK_TIMEOUT = "LOCK_TIMEOUT",
159
- }
160
- ```
161
-
162
- | Value | Description |
163
- |-------|-------------|
164
- | `NO_ACTIVE_TRANSACTION` | 활성 트랜잭션 없음 (ROLLBACK 시) |
165
- | `TRANSACTION_ALREADY_STARTED` | 트랜잭션 이미 시작됨 |
166
- | `DEADLOCK` | 데드락 발생 |
167
- | `LOCK_TIMEOUT` | 잠금 타임아웃 |
168
-
169
- ## `DbTransactionError`
170
-
171
- DBMS별 네이티브 에러를 표준화된 에러 코드로 래핑하는 에러 클래스.
172
-
173
- ```typescript
174
- export class DbTransactionError extends Error {
175
- override readonly name = "DbTransactionError";
176
-
177
- constructor(
178
- public readonly code: DbErrorCode,
179
- message: string,
180
- public readonly originalError?: unknown,
181
- );
182
- }
183
- ```
184
-
185
- | Field | Type | Description |
186
- |-------|------|-------------|
187
- | `code` | `DbErrorCode` | 표준화된 에러 코드 |
188
- | `originalError` | `unknown \| undefined` | 원본 DBMS 에러 (디버깅용) |