sonamu 0.7.4 → 0.7.5

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 (133) hide show
  1. package/dist/api/config.d.ts +1 -4
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +2 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +19 -47
  7. package/dist/bin/cli.js +6 -6
  8. package/dist/database/base-model.d.ts +1 -1
  9. package/dist/database/base-model.d.ts.map +1 -1
  10. package/dist/database/base-model.js +15 -4
  11. package/dist/database/code-generator.d.ts.map +1 -1
  12. package/dist/database/code-generator.js +3 -3
  13. package/dist/database/db.d.ts.map +1 -1
  14. package/dist/database/db.js +1 -1
  15. package/dist/database/puri-wrapper.d.ts +11 -11
  16. package/dist/database/puri-wrapper.d.ts.map +1 -1
  17. package/dist/database/puri-wrapper.js +7 -11
  18. package/dist/database/puri.d.ts +36 -17
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +54 -7
  21. package/dist/database/puri.types.d.ts +54 -17
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +2 -4
  24. package/dist/database/puri.types.test-d.js +129 -0
  25. package/dist/database/upsert-builder.d.ts +16 -10
  26. package/dist/database/upsert-builder.d.ts.map +1 -1
  27. package/dist/database/upsert-builder.js +10 -19
  28. package/dist/entity/entity-manager.d.ts +113 -22
  29. package/dist/entity/entity-manager.d.ts.map +1 -1
  30. package/dist/entity/entity-manager.js +1 -1
  31. package/dist/entity/entity.d.ts +34 -0
  32. package/dist/entity/entity.d.ts.map +1 -1
  33. package/dist/entity/entity.js +110 -37
  34. package/dist/index.d.ts +5 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -2
  37. package/dist/migration/code-generation.d.ts.map +1 -1
  38. package/dist/migration/code-generation.js +341 -149
  39. package/dist/migration/migration-set.d.ts.map +1 -1
  40. package/dist/migration/migration-set.js +21 -5
  41. package/dist/migration/migrator.d.ts.map +1 -1
  42. package/dist/migration/migrator.js +7 -1
  43. package/dist/migration/postgresql-schema-reader.d.ts +11 -1
  44. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  45. package/dist/migration/postgresql-schema-reader.js +111 -10
  46. package/dist/syncer/syncer.d.ts.map +1 -1
  47. package/dist/syncer/syncer.js +4 -3
  48. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  49. package/dist/template/implementations/generated.template.js +12 -2
  50. package/dist/template/implementations/generated_sso.template.d.ts +3 -3
  51. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  52. package/dist/template/implementations/generated_sso.template.js +50 -2
  53. package/dist/template/implementations/model.template.js +6 -6
  54. package/dist/template/implementations/model_test.template.js +4 -4
  55. package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
  56. package/dist/template/implementations/view_enums_select.template.js +2 -2
  57. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  58. package/dist/template/implementations/view_form.template.js +12 -9
  59. package/dist/template/implementations/view_id_async_select.template.js +4 -4
  60. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  61. package/dist/template/implementations/view_list.template.js +12 -9
  62. package/dist/template/implementations/view_search_input.template.js +2 -2
  63. package/dist/template/template.js +2 -2
  64. package/dist/template/zod-converter.d.ts.map +1 -1
  65. package/dist/template/zod-converter.js +17 -2
  66. package/dist/testing/fixture-manager.d.ts +2 -1
  67. package/dist/testing/fixture-manager.d.ts.map +1 -1
  68. package/dist/testing/fixture-manager.js +29 -29
  69. package/dist/types/types.d.ts +593 -68
  70. package/dist/types/types.d.ts.map +1 -1
  71. package/dist/types/types.js +113 -9
  72. package/dist/vector/chunking.d.ts +25 -0
  73. package/dist/vector/chunking.d.ts.map +1 -0
  74. package/dist/vector/chunking.js +97 -0
  75. package/dist/vector/config.d.ts +12 -0
  76. package/dist/vector/config.d.ts.map +1 -0
  77. package/dist/vector/config.js +83 -0
  78. package/dist/vector/embedding.d.ts +42 -0
  79. package/dist/vector/embedding.d.ts.map +1 -0
  80. package/dist/vector/embedding.js +147 -0
  81. package/dist/vector/types.d.ts +105 -0
  82. package/dist/vector/types.d.ts.map +1 -0
  83. package/dist/vector/types.js +5 -0
  84. package/dist/vector/vector-search.d.ts +47 -0
  85. package/dist/vector/vector-search.d.ts.map +1 -0
  86. package/dist/vector/vector-search.js +176 -0
  87. package/package.json +9 -8
  88. package/src/api/config.ts +0 -4
  89. package/src/api/sonamu.ts +21 -36
  90. package/src/bin/cli.ts +5 -5
  91. package/src/database/base-model.ts +20 -11
  92. package/src/database/code-generator.ts +6 -2
  93. package/src/database/db.ts +1 -0
  94. package/src/database/puri-wrapper.ts +22 -16
  95. package/src/database/puri.ts +150 -27
  96. package/src/database/puri.types.test-d.ts +457 -0
  97. package/src/database/puri.types.ts +231 -33
  98. package/src/database/upsert-builder.ts +43 -34
  99. package/src/entity/entity-manager.ts +2 -2
  100. package/src/entity/entity.ts +134 -44
  101. package/src/index.ts +6 -0
  102. package/src/migration/code-generation.ts +377 -174
  103. package/src/migration/migration-set.ts +22 -3
  104. package/src/migration/migrator.ts +6 -0
  105. package/src/migration/postgresql-schema-reader.ts +121 -21
  106. package/src/syncer/syncer.ts +3 -2
  107. package/src/template/implementations/generated.template.ts +51 -9
  108. package/src/template/implementations/generated_sso.template.ts +71 -2
  109. package/src/template/implementations/model.template.ts +5 -5
  110. package/src/template/implementations/model_test.template.ts +3 -3
  111. package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
  112. package/src/template/implementations/view_enums_select.template.ts +1 -1
  113. package/src/template/implementations/view_form.template.ts +11 -8
  114. package/src/template/implementations/view_id_async_select.template.ts +3 -3
  115. package/src/template/implementations/view_list.template.ts +11 -8
  116. package/src/template/implementations/view_search_input.template.ts +1 -1
  117. package/src/template/template.ts +1 -1
  118. package/src/template/zod-converter.ts +20 -0
  119. package/src/testing/fixture-manager.ts +31 -30
  120. package/src/types/types.ts +226 -48
  121. package/src/vector/chunking.ts +115 -0
  122. package/src/vector/config.ts +68 -0
  123. package/src/vector/embedding.ts +193 -0
  124. package/src/vector/types.ts +122 -0
  125. package/src/vector/vector-search.ts +261 -0
  126. package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
  127. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
  128. package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
  129. package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
  130. package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
  131. package/dist/template/implementations/view_list_columns.template.js +0 -49
  132. package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
  133. package/src/template/implementations/view_list_columns.template.ts +0 -53
@@ -1,27 +1,50 @@
1
1
  /** biome-ignore-all lint/suspicious/noExplicitAny: Puri.types.ts는 다양한 타입을 사용하고 있습니다. */
2
2
 
3
3
  import type { QueryResult } from "pg";
4
- import type { DatabaseSchemaExtend } from "../types/types";
4
+ import type { DatabaseForeignKeys, DatabaseSchemaExtend } from "../types/types";
5
5
  import type { Puri } from "./puri";
6
6
  import type { PuriWrapper } from "./puri-wrapper";
7
7
 
8
- // 메타데이터 컬럼 유틸
9
- type MetadataColumns = "__fulltext__" | "__virtual__";
8
+ // ============================================
9
+ // 내부 타입 (메타데이터)
10
+ // ============================================
11
+ type FulltextKey = "__fulltext__";
12
+ type VirtualKey = "__virtual__";
13
+ type LeftJoinedKey = "__leftJoined__";
14
+ type HasDefault = "__hasDefault__";
15
+ type GeneratedKey = "__generated__";
16
+
17
+ type InternalTypeKeys = FulltextKey | VirtualKey | LeftJoinedKey | HasDefault | GeneratedKey;
18
+
19
+ // ============================================
20
+ // 타입 유틸리티
21
+ // ============================================
22
+
23
+ // 테이블명 타입
24
+ export type TableName<TSchema> = keyof TSchema & string;
10
25
 
11
26
  // virtual 컬럼 타입 추출
12
- type VirtualKeys<T> = T extends { __virtual__: readonly (infer V)[] } ? V & string : never;
27
+ type VirtualKeys<T> = T extends { [K in VirtualKey]: readonly (infer V)[] } ? V & string : never;
13
28
 
14
29
  // virtual 컬럼 제거
15
30
  type StripVirtual<T> = Omit<T, VirtualKeys<T>>;
16
31
 
32
+ // LEFT JOIN 마커 - nullable FK로 조인된 테이블
33
+ // 이 마커는 nullable FK + leftJoin 조합에서만 붙습니다.
34
+ // join + FK nullable -> 안 붙음
35
+ // join + FK non-nullable -> 안 붙음
36
+ // leftJoin + FK non-nullable -> 안 붙음
37
+ // leftJoin + FK nullable -> 붙음!
38
+ export type LeftJoinedMarker = { [K in LeftJoinedKey]: true };
39
+
17
40
  // 메타데이터 필드 제외한 실제 엔티티 컬럼
18
- export type ColumnKeys<T> = Exclude<keyof StripVirtual<T>, MetadataColumns> & string;
41
+ export type ColumnKeys<T> = Exclude<keyof StripVirtual<T>, InternalTypeKeys> & string;
19
42
 
20
- // virtual 컬럼 제거 후 __fulltext__ 메타데이터 유지
21
- export type PuriTable<T> = Omit<StripVirtual<T>, "__virtual__">;
43
+ // virtual 컬럼 제거 후 __fulltext__ 유지
44
+ export type PuriTable<T> = Omit<StripVirtual<T>, VirtualKey>;
22
45
 
23
- // 메타데이터 컬럼 제외 타입 정의
24
- export type OmitMetadataColumns<T> = Omit<T, MetadataColumns>;
46
+ // 내부 타입 제외 (실제 컬럼만 남김)
47
+ export type OmitInternalTypeKeys<T> = Omit<T, InternalTypeKeys>;
25
48
 
26
49
  // TTables의 모든 테이블에서 사용 가능한 컬럼 경로
27
50
  export type AvailableColumns<TTables extends Record<string, any>> =
@@ -32,27 +55,120 @@ export type AvailableColumns<TTables extends Record<string, any>> =
32
55
  ? ColumnKeys<TTables[keyof TTables]> // 단일 테이블이면 컬럼명만도 허용
33
56
  : never);
34
57
 
58
+ // 숫자 타입 컬럼만 추출하는 유틸리티 타입
59
+ type NumericColumnKeys<T> = {
60
+ [K in keyof T]: T[K] extends number | bigint | null | undefined ? K : never;
61
+ }[keyof T] &
62
+ string;
63
+
64
+ // TTables의 모든 테이블에서 숫자 타입 컬럼만 추출
65
+ export type NumericColumns<TTables extends Record<string, any>> =
66
+ | {
67
+ [TAlias in keyof TTables]: `${TAlias & string}.${NumericColumnKeys<TTables[TAlias]>}`;
68
+ }[keyof TTables]
69
+ | (IsSingleKey<TTables> extends true
70
+ ? NumericColumnKeys<TTables[keyof TTables]> // 단일 테이블이면 컬럼명만도 허용
71
+ : never);
72
+
35
73
  // Group By, Order By, Having 등에서 선택 가능한 컬럼
36
74
  export type ResultAvailableColumns<TTables extends Record<string, any>, TResult = any> =
37
75
  | AvailableColumns<TTables>
38
76
  | `${keyof TResult & string}`;
39
77
 
40
- // Select 값 타입 확장
78
+ // Select 값 타입 확장 (단일 컬럼 또는 SQL 표현식)
41
79
  export type SelectValue<TTables extends Record<string, any>> =
42
80
  | AvailableColumns<TTables>
43
81
  | SqlExpression<"string" | "number" | "boolean" | "date">;
44
82
 
45
- // Select 객체 타입 (현재는 컬럼 경로만 지원)
46
- export type SelectObject<TTables extends Record<string, any>> = Record<
47
- string,
48
- SelectValue<TTables> // AvailableColumns 대신
49
- >;
83
+ // 중첩 Select 객체 타입 (재귀적)
84
+ // 예: { parent: { id: "parent.id", name: "parent.name" } }
85
+ export type NestedSelectObject<TTables extends Record<string, any>> = {
86
+ [key: string]: SelectValue<TTables> | NestedSelectObject<TTables>;
87
+ };
88
+
89
+ // Select 객체 타입 (flat 또는 중첩 허용)
90
+ export type SelectObject<TTables extends Record<string, any>> = NestedSelectObject<TTables>;
91
+
92
+ // 값이 중첩 객체인지 판별하는 헬퍼 타입
93
+ type IsNestedObject<T> = T extends string
94
+ ? false
95
+ : T extends SqlExpression<any>
96
+ ? false
97
+ : T extends Record<string, any>
98
+ ? true
99
+ : false;
100
+
101
+ // 컬럼이 nullable인지 확인 (스키마에서 직접 추출)
102
+ // 예: IsNullableColumn<TTables, "employees.department_id"> → department_id가 number | null이면 true
103
+ export type IsNullableColumn<
104
+ TTables,
105
+ Path extends string,
106
+ > = Path extends `${infer TAlias}.${infer TColumn}`
107
+ ? TAlias extends keyof TTables
108
+ ? TColumn extends keyof TTables[TAlias]
109
+ ? null extends TTables[TAlias][TColumn]
110
+ ? true
111
+ : false
112
+ : false
113
+ : false
114
+ : false;
115
+
116
+ // FK nullable 여부에 따른 마커 타입 결정
117
+ // nullable FK로 leftJoin → LeftJoinedMarker (객체 자체가 null일 수 있음)
118
+ // non-null FK로 leftJoin → 마커 없음 (부모가 있으면 자식도 반드시 있음)
119
+ export type LeftJoinMarkerFor<TTables, Path extends string> = IsNullableColumn<
120
+ TTables,
121
+ Path
122
+ > extends true
123
+ ? LeftJoinedMarker
124
+ : {};
125
+
126
+ // 주어진 테이블이 FK nullable로 leftJoin 된 테이블인지 확인합니다.
127
+ // 사실 LeftJoinMarker가 붙었는지 확인하는게 다입니다.
128
+ // 이 마커는 FK nullable + leftJoin 조합에서만 붙습니다.
129
+ type IsNullableJoinedTable<TTables, TableKey> = TableKey extends keyof TTables
130
+ ? TTables[TableKey] extends LeftJoinedMarker
131
+ ? true // LeftJoinedMarker가 있으면 nullable
132
+ : false
133
+ : false;
50
134
 
51
- // Select 결과 타입 추론
135
+ // 경로 조합 헬퍼 (prefix가 없으면 key만, 있으면 prefix__key)
136
+ type JoinPath<Prefix extends string, Key extends string> = Prefix extends ""
137
+ ? Key
138
+ : `${Prefix}__${Key}`;
139
+
140
+ // Select 결과 타입을 추론해주는 친구입니다.
141
+ // 이 타입은 Puri의 select, appendSelect에서 TResult로 사용됩니다.
142
+ //
143
+ // Schema를 읽어서 FK의 nullability에 따라 join된 객체의 타입을 추론해주는 기능이 있습니다.
144
+ // 이게 무슨 소리냐? FK가 nullable인데 leftJoin되었다면, 해당 객체는 nullable 해야 함을 타입 추론으로 반영해준다는 것입니다.
145
+ // 반면 FK가 non-nullable이거나 그냥 join으로 이어졌다면 해당 객체는 non-nullable할 겁니다.
146
+ // 물론 객체 내부의 nullability는 또 별개로 추론됩니다.
147
+ //
148
+ // 아래에도 ParseSelectObjectWithPath를 비롯해 ExtractColumnType, ExtractColumnTypeRaw 등의 타입이 있습니다.
149
+ // 이들의 역할은 다음과 같습니다:
150
+ // - Parse*: 객체 레벨에서 중첩 구조를 순회하며 객체에 | null을 붙일지 결정합니다.
151
+ // - Extract*: 필드 레벨에서 "table.column" 경로로부터 실제 타입을 추출합니다.
152
+ //
153
+ // 예시:
154
+ // .select({
155
+ // id: "users.id", // ← ExtractColumnType의 결과는 number입니다.
156
+ // department: { // ← ParseSelectObjectInner에 의해 nullable 객체로 추론됩니다.
157
+ // id: "department.id", // ← ExtractColumnTypeRaw의 결과는 number입니다.
158
+ // name: "department.name" // ← ExtractColumnTypeRaw의 결과는 string입니다.
159
+ // }
160
+ // })
52
161
  export type ParseSelectObject<
53
162
  TTables extends Record<string, any>,
54
163
  TSelect extends SelectObject<TTables>,
55
- > = {
164
+ > = ParseSelectObjectWithPath<TTables, TSelect, "">;
165
+
166
+ // 경로를 추적하면서 Select 결과 타입을 추론합니다.
167
+ type ParseSelectObjectWithPath<
168
+ TTables extends Record<string, any>,
169
+ TSelect extends SelectObject<TTables>,
170
+ Prefix extends string,
171
+ > = Expand<{
56
172
  [K in keyof TSelect]: TSelect[K] extends SqlExpression<infer R>
57
173
  ? R extends "string"
58
174
  ? string
@@ -63,24 +179,78 @@ export type ParseSelectObject<
63
179
  : R extends "date"
64
180
  ? Date
65
181
  : never
66
- : ExtractColumnType<TTables, TSelect[K] & string>;
67
- };
182
+ : IsNestedObject<TSelect[K]> extends true
183
+ ? TSelect[K] extends NestedSelectObject<TTables>
184
+ ? IsNullableJoinedTable<TTables, JoinPath<Prefix, K & string>> extends true // 주어진 테이블이 FK nullable에 leftJoin되었는지 여부에 따라 select 결과 객체의 타입이 달라집니다.
185
+ ? Expand<ParseSelectObjectInner<TTables, TSelect[K], JoinPath<Prefix, K & string>>> | null // 만약 해당한다면 해당 객체 자체는 nullable 하며,
186
+ : Expand<ParseSelectObjectInner<TTables, TSelect[K], JoinPath<Prefix, K & string>>> // 그렇지 않다면 non-nullable 합니다.
187
+ : never
188
+ : ExtractColumnType<TTables, TSelect[K] & string>;
189
+ }>;
190
+
191
+ // 중첩 객체 내부용 - leftJoin nullable을 객체 레벨에서 이미 처리했으므로 필드는 원본 타입을 사용합니다.
192
+ // ParseSelectObjectWithPath와 거의 동일하나, 마지막에 ExtractColumnType 대신 ExtractColumnTypeRaw를 사용하여
193
+ // 필드 레벨에서 중복으로 | null이 추가되는 것을 방지합니다.
194
+ type ParseSelectObjectInner<
195
+ TTables extends Record<string, any>,
196
+ TSelect extends SelectObject<TTables>,
197
+ Prefix extends string,
198
+ > = Expand<{
199
+ [K in keyof TSelect]: TSelect[K] extends SqlExpression<infer R>
200
+ ? R extends "string"
201
+ ? string
202
+ : R extends "number"
203
+ ? number
204
+ : R extends "boolean"
205
+ ? boolean
206
+ : R extends "date"
207
+ ? Date
208
+ : never
209
+ : IsNestedObject<TSelect[K]> extends true
210
+ ? TSelect[K] extends NestedSelectObject<TTables>
211
+ ? IsNullableJoinedTable<TTables, JoinPath<Prefix, K & string>> extends true
212
+ ? Expand<ParseSelectObjectInner<TTables, TSelect[K], JoinPath<Prefix, K & string>>> | null
213
+ : Expand<ParseSelectObjectInner<TTables, TSelect[K], JoinPath<Prefix, K & string>>>
214
+ : never
215
+ : ExtractColumnTypeRaw<TTables, TSelect[K] & string>; // leftJoin nullable 무시
216
+ }>;
68
217
 
69
- // 컬럼 경로에서 타입 추출
218
+ // 컬럼 경로에서 타입을 추출합니다. LeftJoinedMarker가 있으면 | null을 추가합니다.
219
+ // 최상위 select 필드에서 사용됩니다.
70
220
  export type ExtractColumnType<
71
221
  TTables extends Record<string, any>,
72
222
  Path extends string,
73
223
  > = Path extends `${infer TAlias}.${infer TColumn}`
74
224
  ? TAlias extends keyof TTables
75
225
  ? TColumn extends keyof TTables[TAlias]
76
- ? TTables[TAlias][TColumn]
226
+ ? TTables[TAlias] extends LeftJoinedMarker
227
+ ? TTables[TAlias][TColumn] | null // LEFT JOIN (nullable FK) → nullable
228
+ : TTables[TAlias][TColumn] // INNER JOIN 또는 non-null FK leftJoin → non-nullable
229
+ : never
230
+ : never
231
+ : IsSingleKey<TTables> extends true
232
+ ? Path extends keyof TTables[keyof TTables]
233
+ ? TTables[keyof TTables][Path]
234
+ : never
235
+ : never;
236
+
237
+ // 컬럼 경로에서 타입을 추출합니다. leftJoin 여부와 관계없이 원본 타입을 반환합니다.
238
+ // 중첩 객체 내부 필드에서 사용됩니다. (객체 레벨에서 이미 | null 처리가 완료되었으므로)
239
+ type ExtractColumnTypeRaw<
240
+ TTables extends Record<string, any>,
241
+ Path extends string,
242
+ > = Path extends `${infer TAlias}.${infer TColumn}`
243
+ ? TAlias extends keyof TTables
244
+ ? TColumn extends keyof TTables[TAlias]
245
+ ? TTables[TAlias][TColumn] // leftJoin 여부와 관계없이 원본 타입
77
246
  : never
78
247
  : never
79
- : IsSingleKey<TTables> extends true // 추가
248
+ : IsSingleKey<TTables> extends true
80
249
  ? Path extends keyof TTables[keyof TTables]
81
250
  ? TTables[keyof TTables][Path]
82
251
  : never
83
252
  : never;
253
+
84
254
  // Where 조건 객체 타입
85
255
  // 예: { "u.id": 1, "u.status": "active" }
86
256
  export type WhereCondition<TTables extends Record<string, any>> = {
@@ -90,7 +260,7 @@ export type WhereCondition<TTables extends Record<string, any>> = {
90
260
  // Fulltext index 컬럼 추출 타입
91
261
  export type FulltextColumns<TTables extends Record<string, any>> = {
92
262
  [TAlias in keyof TTables]: TTables[TAlias] extends {
93
- __fulltext__: readonly (infer Col)[];
263
+ [K in FulltextKey]: readonly (infer Col)[];
94
264
  }
95
265
  ? Col extends string
96
266
  ? `${TAlias & string}.${Col}`
@@ -100,6 +270,8 @@ export type FulltextColumns<TTables extends Record<string, any>> = {
100
270
 
101
271
  // 비교 연산자
102
272
  export type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "<>" | "!=";
273
+ // 조건 연산자: 비교 연산자 + 패턴 매칭 연산자
274
+ export type WhereOperator = ComparisonOperator | "like" | "not like";
103
275
 
104
276
  // SQL Expression 타입 정의
105
277
  export type SqlExpression<T extends "string" | "number" | "boolean" | "date"> = {
@@ -126,17 +298,23 @@ type IsSingleKey<TTables extends Record<string, any>> = keyof TTables extends in
126
298
  export type SingleTableValue<TTables extends Record<string, any>> =
127
299
  IsSingleKey<TTables> extends true ? TTables[keyof TTables] : never;
128
300
 
129
- // Nullable을 Optional로 변환
130
- type NullableToOptional<T> = {
131
- [K in keyof T as T[K] extends null | undefined ? K : never]?: Exclude<T[K], null | undefined>;
132
- } & Partial<{
133
- [K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
134
- }>;
301
+ // __hasDefault__에 포함된 키들을 PuriTable<T>의 키로 제한
302
+ type HasDefaultKeys<T> = T extends { __hasDefault__: readonly (infer K)[] }
303
+ ? Extract<K, keyof PuriTable<T>>
304
+ : never;
135
305
 
136
- // Insert 타입: id, created_at 제외
137
- export type InsertData<T> = NullableToOptional<
138
- Omit<PuriTable<T>, "id" | "created_at" | MetadataColumns>
139
- >;
306
+ // __generated__에 포함된 키들 (INSERT 시 제외해야 함)
307
+ type GeneratedKeys<T> = T extends { __generated__: readonly (infer K)[] }
308
+ ? Extract<K, keyof PuriTable<T>>
309
+ : never;
310
+
311
+ // Insert 타입: 메타데이터 제거 후, __hasDefault__ 컬럼들만 optional로 처리, __generated__ 컬럼은 완전히 제외
312
+ export type InsertData<T> = Omit<
313
+ PuriTable<T>,
314
+ InternalTypeKeys | HasDefaultKeys<T> | GeneratedKeys<T>
315
+ > & {
316
+ [K in HasDefaultKeys<T>]?: PuriTable<T>[K];
317
+ };
140
318
 
141
319
  // Insert Result 타입
142
320
  export type InsertResult = Pick<QueryResult<any>, "command" | "rowCount" | "rows" | "oid">;
@@ -170,3 +348,23 @@ export type OnConflictAction<TTables extends Record<string, unknown>> =
170
348
  | AvailableColumns<TTables>[] // 배열 형태 - ["name", "email"]
171
349
  | WhereCondition<TTables>; // 객체 형태 - { name: "John", count: Puri.rawNumber(...) }
172
350
  };
351
+
352
+ // FK 컬럼명 추출 유틸리티 타입 - DatabaseForeignKeys 활용
353
+ export type ForeignKeyColumns<TTable extends TableName<DatabaseSchemaExtend>> =
354
+ TTable extends keyof DatabaseForeignKeys ? DatabaseForeignKeys[TTable] : never;
355
+
356
+ // Union을 Intersection으로 변환하는 유틸리티
357
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
358
+ ? I
359
+ : never;
360
+
361
+ // SelectAll 시 모든 조인된 테이블의 컬럼 포함
362
+ export type SelectAllResult<TTables extends Record<string, any>> = UnionToIntersection<
363
+ {
364
+ [K in keyof TTables]: TTables[K] extends infer T
365
+ ? T extends LeftJoinedMarker
366
+ ? Partial<OmitInternalTypeKeys<T>> // LEFT JOIN은 nullable, 메타데이터 제거
367
+ : OmitInternalTypeKeys<T> // INNER JOIN은 non-nullable, 메타데이터 제거
368
+ : never;
369
+ }[keyof TTables]
370
+ >;
@@ -3,24 +3,43 @@ import type { Knex } from "knex";
3
3
  import { isArray, unique } from "radashi";
4
4
  import { EntityManager } from "../entity/entity-manager";
5
5
  import { Naite } from "../naite/naite";
6
+ import type { DatabaseForeignKeys, DatabaseSchemaExtend, EntityIndex } from "../types/types";
6
7
  import { assertDefined, chunk, nonNullable } from "../utils/utils";
7
8
  import { batchUpdate, type RowWithId } from "./_batch_update";
9
+ import type { ForeignKeyColumns, TableName } from "./puri.types";
8
10
 
11
+ /**
12
+ * FK 타입 추론을 위해 DatabaseForeignKeys export
13
+ * (module augmentation 자동 로드 보장)
14
+ */
15
+ export type { DatabaseForeignKeys };
16
+
17
+ // 테이블 데이터 타입
9
18
  type TableData = {
10
19
  references: Set<string>;
11
20
  rows: Record<string, unknown>[];
12
- uniqueIndexes: { name?: string; columns: string[] }[];
21
+ uniqueIndexes: EntityIndex[];
13
22
  uniquesMap: Map<string, string>;
14
23
  };
24
+
25
+ // 참조 필드 타입
15
26
  export type UBRef = {
16
27
  uuid: string;
17
28
  of: string;
18
29
  use?: string;
19
30
  };
20
- type UpsertOptions = {
31
+
32
+ // upsert 옵션
33
+ export type UpsertOptions<TTable extends TableName<DatabaseSchemaExtend>> = {
34
+ chunkSize?: number;
35
+ cleanOrphans?: ForeignKeyColumns<TTable> | ForeignKeyColumns<TTable>[];
36
+ };
37
+
38
+ // insertOnly 옵션
39
+ export type InsertOnlyOptions = {
21
40
  chunkSize?: number;
22
- cleanOrphans?: string | string[]; // FK 컬럼명(들)
23
41
  };
42
+
24
43
  export function isRefField(field: unknown): field is UBRef {
25
44
  return (
26
45
  field !== undefined &&
@@ -76,11 +95,11 @@ export class UpsertBuilder {
76
95
  const uniqueKeys = table.uniqueIndexes
77
96
  .map((unqIndex) => {
78
97
  const uniqueKeyArray = unqIndex.columns.map((unqCol) => {
79
- const val = row[unqCol as keyof typeof row];
98
+ const val = row[unqCol.name as keyof typeof row];
80
99
  if (isRefField(val)) {
81
100
  return val.uuid;
82
101
  } else {
83
- return row[unqCol as keyof typeof row] ?? randomUUID(); // nullable인 경우 uuid로 랜덤값 삽입
102
+ return row[unqCol.name as keyof typeof row] ?? randomUUID(); // nullable인 경우 uuid로 랜덤값 삽입
84
103
  }
85
104
  });
86
105
 
@@ -154,37 +173,27 @@ export class UpsertBuilder {
154
173
  return result;
155
174
  }
156
175
 
157
- async upsert(
176
+ async upsert<TTable extends TableName<DatabaseSchemaExtend>>(
158
177
  wdb: Knex,
159
- tableName: string,
160
- optionsOrChunkSize?: UpsertOptions,
178
+ tableName: TTable,
179
+ options?: UpsertOptions<TTable>,
161
180
  ): Promise<number[]> {
162
- // 숫자면 { chunkSize: n } 으로 변환
163
- const options =
164
- typeof optionsOrChunkSize === "number"
165
- ? { chunkSize: optionsOrChunkSize }
166
- : optionsOrChunkSize;
167
-
168
181
  return this.upsertOrInsert(wdb, tableName, "upsert", options);
169
182
  }
170
- async insertOnly(
183
+
184
+ async insertOnly<TTable extends TableName<DatabaseSchemaExtend>>(
171
185
  wdb: Knex,
172
- tableName: string,
173
- optionsOrChunkSize?: UpsertOptions | number,
186
+ tableName: TTable,
187
+ options?: InsertOnlyOptions,
174
188
  ): Promise<number[]> {
175
- const options =
176
- typeof optionsOrChunkSize === "number"
177
- ? { chunkSize: optionsOrChunkSize }
178
- : optionsOrChunkSize;
179
-
180
189
  return this.upsertOrInsert(wdb, tableName, "insert", options);
181
190
  }
182
191
 
183
- async upsertOrInsert(
192
+ async upsertOrInsert<TTable extends TableName<DatabaseSchemaExtend>>(
184
193
  wdb: Knex,
185
- tableName: string,
194
+ tableName: TTable,
186
195
  mode: "upsert" | "insert",
187
- options?: UpsertOptions,
196
+ options?: UpsertOptions<TTable>,
188
197
  ): Promise<number[]> {
189
198
  if (this.hasTable(tableName) === false) {
190
199
  return [];
@@ -234,11 +243,6 @@ export class UpsertBuilder {
234
243
  throw new Error(`${tableName}에 순환 자기 참조가 있습니다.`);
235
244
  }
236
245
 
237
- // upsert 모드일 때 유니크 인덱스가 없으면 에러
238
- if (mode === "upsert" && table.uniqueIndexes.length === 0) {
239
- throw new Error(`${tableName}에 unique index가 정의되지 않아 upsert를 할 수 없습니다.`);
240
- }
241
-
242
246
  const uuidMap = new Map<string, unknown>();
243
247
  const allIds: number[] = [];
244
248
 
@@ -284,8 +288,11 @@ export class UpsertBuilder {
284
288
  // INSERT 모드 - RETURNING 사용
285
289
  resultRows = await wdb.insert(dataForDb).into(tableName).returning(selectFields);
286
290
  } else {
287
- // UPSERT 모드 - onConflict 사용
288
- const conflictColumns = table.uniqueIndexes[0].columns;
291
+ // UPSERT 모드 - onConflict 사용 (unique index 없으면 PK fallback)
292
+ const conflictColumns =
293
+ table.uniqueIndexes.length > 0
294
+ ? table.uniqueIndexes[0].columns.map((c) => c.name)
295
+ : ["id"];
289
296
  const updateColumns = Object.keys(dataForDb[0]).filter(
290
297
  (col) => !conflictColumns.includes(col),
291
298
  );
@@ -340,7 +347,9 @@ export class UpsertBuilder {
340
347
 
341
348
  if (options?.cleanOrphans) {
342
349
  const cleanOrphans = options.cleanOrphans;
343
- const fkColumns = isArray(cleanOrphans) ? cleanOrphans : [cleanOrphans];
350
+ const fkColumns = isArray(cleanOrphans)
351
+ ? (cleanOrphans as ForeignKeyColumns<TTable>[])
352
+ : [cleanOrphans as ForeignKeyColumns<TTable>];
344
353
 
345
354
  // 현재 register된 레코드들의 FK 값들 추출
346
355
  const fkConditions = fkColumns.map((fkCol) => {
@@ -430,7 +439,7 @@ export class UpsertBuilder {
430
439
  }
431
440
 
432
441
  // ============================================================================
433
- // Private Helpers
442
+ // Private Helper Methods
434
443
  // ============================================================================
435
444
 
436
445
  /**
@@ -5,7 +5,7 @@ import inflection from "inflection";
5
5
  import path from "path";
6
6
  import { prettifyError } from "zod";
7
7
  import { Sonamu } from "../api/sonamu";
8
- import { type EntityJson, EntityJsonSchema } from "../types/types";
8
+ import { type EntityIndex, type EntityJson, EntityJsonSchema } from "../types/types";
9
9
  import type { AbsolutePath } from "../utils/path-utils";
10
10
  import { Entity } from "./entity";
11
11
 
@@ -15,7 +15,7 @@ export type EntityNamesRecord = Record<
15
15
  >;
16
16
  type TableSpec = {
17
17
  name: string;
18
- uniqueIndexes: { name?: string; columns: string[] }[];
18
+ uniqueIndexes: EntityIndex[];
19
19
  };
20
20
  class EntityManagerClass {
21
21
  private entities: Map<string, Entity> = new Map();