sonamu 0.7.53 → 0.8.0

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 (271) hide show
  1. package/dist/api/config.d.ts +9 -1
  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 +21 -1
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +159 -65
  7. package/dist/auth/plugins/entity-definitions/anonymous.d.ts +10 -0
  8. package/dist/auth/plugins/entity-definitions/anonymous.d.ts.map +1 -0
  9. package/dist/auth/plugins/entity-definitions/anonymous.js +23 -0
  10. package/dist/auth/plugins/entity-definitions/api-key.d.ts +9 -0
  11. package/dist/auth/plugins/entity-definitions/api-key.d.ts.map +1 -0
  12. package/dist/auth/plugins/entity-definitions/api-key.js +199 -0
  13. package/dist/auth/plugins/entity-definitions/index.d.ts +6 -0
  14. package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
  15. package/dist/auth/plugins/entity-definitions/index.js +20 -2
  16. package/dist/auth/plugins/entity-definitions/jwt.d.ts +9 -0
  17. package/dist/auth/plugins/entity-definitions/jwt.d.ts.map +1 -0
  18. package/dist/auth/plugins/entity-definitions/jwt.js +67 -0
  19. package/dist/auth/plugins/entity-definitions/organization.d.ts +9 -0
  20. package/dist/auth/plugins/entity-definitions/organization.d.ts.map +1 -0
  21. package/dist/auth/plugins/entity-definitions/organization.js +424 -0
  22. package/dist/auth/plugins/entity-definitions/passkey.d.ts +10 -0
  23. package/dist/auth/plugins/entity-definitions/passkey.d.ts.map +1 -0
  24. package/dist/auth/plugins/entity-definitions/passkey.js +129 -0
  25. package/dist/auth/plugins/entity-definitions/sso.d.ts +10 -0
  26. package/dist/auth/plugins/entity-definitions/sso.d.ts.map +1 -0
  27. package/dist/auth/plugins/entity-definitions/sso.js +110 -0
  28. package/dist/auth/plugins/entity-definitions/types.d.ts +1 -1
  29. package/dist/auth/plugins/entity-definitions/types.d.ts.map +1 -1
  30. package/dist/auth/plugins/entity-definitions/types.js +1 -1
  31. package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
  32. package/dist/auth/plugins/wrappers/admin.js +2 -4
  33. package/dist/auth/plugins/wrappers/anonymous.d.ts +18 -0
  34. package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -0
  35. package/dist/auth/plugins/wrappers/anonymous.js +26 -0
  36. package/dist/auth/plugins/wrappers/api-key.d.ts +18 -0
  37. package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -0
  38. package/dist/auth/plugins/wrappers/api-key.js +38 -0
  39. package/dist/auth/plugins/wrappers/index.d.ts +6 -0
  40. package/dist/auth/plugins/wrappers/index.d.ts.map +1 -1
  41. package/dist/auth/plugins/wrappers/index.js +7 -1
  42. package/dist/auth/plugins/wrappers/jwt.d.ts +18 -0
  43. package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -0
  44. package/dist/auth/plugins/wrappers/jwt.js +30 -0
  45. package/dist/auth/plugins/wrappers/organization.d.ts +18 -0
  46. package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -0
  47. package/dist/auth/plugins/wrappers/organization.js +67 -0
  48. package/dist/auth/plugins/wrappers/passkey.d.ts +18 -0
  49. package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -0
  50. package/dist/auth/plugins/wrappers/passkey.js +32 -0
  51. package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
  52. package/dist/auth/plugins/wrappers/phone-number.js +2 -4
  53. package/dist/auth/plugins/wrappers/sso.d.ts +853 -0
  54. package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -0
  55. package/dist/auth/plugins/wrappers/sso.js +36 -0
  56. package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
  57. package/dist/auth/plugins/wrappers/two-factor.js +2 -4
  58. package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
  59. package/dist/auth/plugins/wrappers/username.js +2 -4
  60. package/dist/bin/build-config.d.ts +2 -2
  61. package/dist/bin/build-config.js +6 -7
  62. package/dist/bin/cli.js +417 -32
  63. package/dist/bin/fixture.d.ts +27 -0
  64. package/dist/bin/fixture.d.ts.map +1 -0
  65. package/dist/bin/fixture.js +245 -0
  66. package/dist/cache/decorator.d.ts +4 -3
  67. package/dist/cache/decorator.d.ts.map +1 -1
  68. package/dist/cache/decorator.js +5 -4
  69. package/dist/cone/cone-generator.d.ts +33 -0
  70. package/dist/cone/cone-generator.d.ts.map +1 -0
  71. package/dist/cone/cone-generator.js +286 -0
  72. package/dist/database/_batch_update.d.ts.map +1 -1
  73. package/dist/database/_batch_update.js +16 -2
  74. package/dist/database/puri-subset.test-d.js +1 -1
  75. package/dist/database/puri-subset.types.d.ts +1 -1
  76. package/dist/database/puri-subset.types.d.ts.map +1 -1
  77. package/dist/database/puri-subset.types.js +1 -1
  78. package/dist/database/puri.d.ts +4 -0
  79. package/dist/database/puri.d.ts.map +1 -1
  80. package/dist/database/puri.js +20 -2
  81. package/dist/database/upsert-builder.d.ts.map +1 -1
  82. package/dist/database/upsert-builder.js +19 -3
  83. package/dist/dict/en.d.ts +15 -0
  84. package/dist/dict/en.d.ts.map +1 -1
  85. package/dist/dict/en.js +2 -1
  86. package/dist/dict/ko.d.ts +15 -0
  87. package/dist/dict/ko.d.ts.map +1 -1
  88. package/dist/dict/ko.js +2 -1
  89. package/dist/dict/rc-keys.d.ts +28 -0
  90. package/dist/dict/rc-keys.d.ts.map +1 -1
  91. package/dist/dict/rc-keys.js +31 -1
  92. package/dist/dict/sd.d.ts.map +1 -1
  93. package/dist/dict/sd.js +20 -4
  94. package/dist/entity/entity-manager.d.ts +298 -2
  95. package/dist/entity/entity-manager.d.ts.map +1 -1
  96. package/dist/entity/entity-manager.js +4 -1
  97. package/dist/entity/entity-template-cone.d.ts +14 -0
  98. package/dist/entity/entity-template-cone.d.ts.map +1 -0
  99. package/dist/entity/entity-template-cone.js +222 -0
  100. package/dist/entity/entity.d.ts +47 -2
  101. package/dist/entity/entity.d.ts.map +1 -1
  102. package/dist/entity/entity.js +161 -14
  103. package/dist/ssr/renderer.js +3 -3
  104. package/dist/syncer/api-parser.js +12 -1
  105. package/dist/syncer/checksum.d.ts +0 -14
  106. package/dist/syncer/checksum.d.ts.map +1 -1
  107. package/dist/syncer/checksum.js +1 -23
  108. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  109. package/dist/syncer/syncer-actions.js +8 -2
  110. package/dist/syncer/syncer.d.ts +1 -1
  111. package/dist/syncer/syncer.d.ts.map +1 -1
  112. package/dist/syncer/syncer.js +17 -10
  113. package/dist/tasks/workflow-manager.d.ts +13 -1
  114. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  115. package/dist/tasks/workflow-manager.js +18 -1
  116. package/dist/template/entity-converter.js +4 -4
  117. package/dist/template/helpers.d.ts +10 -0
  118. package/dist/template/helpers.d.ts.map +1 -1
  119. package/dist/template/helpers.js +48 -1
  120. package/dist/template/implementations/entry-server.template.d.ts +1 -1
  121. package/dist/template/implementations/entry-server.template.js +7 -2
  122. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  123. package/dist/template/implementations/generated.template.js +5 -1
  124. package/dist/template/implementations/generated_http.template.d.ts +1 -0
  125. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  126. package/dist/template/implementations/generated_http.template.js +6 -2
  127. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  128. package/dist/template/implementations/generated_sso.template.js +29 -8
  129. package/dist/template/implementations/queries.template.d.ts.map +1 -1
  130. package/dist/template/implementations/queries.template.js +9 -1
  131. package/dist/template/implementations/sd.template.d.ts +1 -1
  132. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  133. package/dist/template/implementations/sd.template.js +28 -4
  134. package/dist/template/implementations/services.template.d.ts.map +1 -1
  135. package/dist/template/implementations/services.template.js +12 -12
  136. package/dist/template/implementations/view_form.template.d.ts +11 -7
  137. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  138. package/dist/template/implementations/view_form.template.js +97 -87
  139. package/dist/template/implementations/view_list.template.d.ts +3 -3
  140. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  141. package/dist/template/implementations/view_list.template.js +115 -109
  142. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  143. package/dist/template/implementations/view_search_input.template.js +18 -14
  144. package/dist/template/zod-converter.d.ts.map +1 -1
  145. package/dist/template/zod-converter.js +95 -7
  146. package/dist/testing/_relation-graph.js +1 -1
  147. package/dist/testing/data-explorer.d.ts +61 -0
  148. package/dist/testing/data-explorer.d.ts.map +1 -0
  149. package/dist/testing/data-explorer.js +274 -0
  150. package/dist/testing/faker-mappings.d.ts +20 -0
  151. package/dist/testing/faker-mappings.d.ts.map +1 -0
  152. package/dist/testing/faker-mappings.js +421 -0
  153. package/dist/testing/fixture-generator.d.ts +161 -0
  154. package/dist/testing/fixture-generator.d.ts.map +1 -0
  155. package/dist/testing/fixture-generator.js +954 -0
  156. package/dist/testing/fixture-manager.d.ts +6 -1
  157. package/dist/testing/fixture-manager.d.ts.map +1 -1
  158. package/dist/testing/fixture-manager.js +72 -4
  159. package/dist/testing/index.d.ts +3 -0
  160. package/dist/testing/index.d.ts.map +1 -1
  161. package/dist/testing/index.js +4 -1
  162. package/dist/types/types.d.ts +1520 -26
  163. package/dist/types/types.d.ts.map +1 -1
  164. package/dist/types/types.js +136 -22
  165. package/dist/ui/ai-client.d.ts.map +1 -1
  166. package/dist/ui/ai-client.js +9 -4
  167. package/dist/ui/api.d.ts.map +1 -1
  168. package/dist/ui/api.js +303 -24
  169. package/dist/ui-web/assets/index-CsUr-_pV.js +254 -0
  170. package/dist/ui-web/assets/index-T42zzs1K.css +1 -0
  171. package/dist/ui-web/index.html +2 -2
  172. package/dist/utils/fs-utils.d.ts +2 -1
  173. package/dist/utils/fs-utils.d.ts.map +1 -1
  174. package/dist/utils/fs-utils.js +14 -3
  175. package/package.json +19 -11
  176. package/src/api/config.ts +12 -1
  177. package/src/api/sonamu.ts +179 -65
  178. package/src/auth/plugins/entity-definitions/anonymous.ts +17 -0
  179. package/src/auth/plugins/entity-definitions/api-key.ts +93 -0
  180. package/src/auth/plugins/entity-definitions/index.ts +18 -0
  181. package/src/auth/plugins/entity-definitions/jwt.ts +35 -0
  182. package/src/auth/plugins/entity-definitions/organization.ts +215 -0
  183. package/src/auth/plugins/entity-definitions/passkey.ts +64 -0
  184. package/src/auth/plugins/entity-definitions/sso.ts +62 -0
  185. package/src/auth/plugins/entity-definitions/types.ts +11 -1
  186. package/src/auth/plugins/wrappers/admin.ts +1 -3
  187. package/src/auth/plugins/wrappers/anonymous.ts +30 -0
  188. package/src/auth/plugins/wrappers/api-key.ts +42 -0
  189. package/src/auth/plugins/wrappers/index.ts +6 -0
  190. package/src/auth/plugins/wrappers/jwt.ts +34 -0
  191. package/src/auth/plugins/wrappers/organization.ts +73 -0
  192. package/src/auth/plugins/wrappers/passkey.ts +36 -0
  193. package/src/auth/plugins/wrappers/phone-number.ts +1 -3
  194. package/src/auth/plugins/wrappers/sso.ts +40 -0
  195. package/src/auth/plugins/wrappers/two-factor.ts +1 -3
  196. package/src/auth/plugins/wrappers/username.ts +1 -3
  197. package/src/bin/build-config.ts +6 -6
  198. package/src/bin/cli.ts +452 -31
  199. package/src/bin/fixture.ts +302 -0
  200. package/src/cache/decorator.ts +4 -3
  201. package/src/cone/cone-generator.ts +363 -0
  202. package/src/database/_batch_update.ts +11 -0
  203. package/src/database/puri-subset.test-d.ts +13 -13
  204. package/src/database/puri-subset.types.ts +1 -1
  205. package/src/database/puri.ts +43 -1
  206. package/src/database/upsert-builder.ts +16 -2
  207. package/src/dict/en.ts +1 -0
  208. package/src/dict/ko.ts +1 -0
  209. package/src/dict/rc-keys.ts +32 -0
  210. package/src/dict/sd.ts +23 -3
  211. package/src/entity/entity-manager.ts +4 -0
  212. package/src/entity/entity-template-cone.ts +298 -0
  213. package/src/entity/entity.ts +189 -13
  214. package/src/shared/app.shared.ts.txt +5 -0
  215. package/src/shared/web.shared.ts.txt +9 -5
  216. package/src/skills/project/README.md +21 -0
  217. package/src/skills/project/architecture.md +373 -0
  218. package/src/skills/project/business-logic.md +270 -0
  219. package/src/skills/project/requirements.md +160 -0
  220. package/src/skills/sonamu/SKILL.md +168 -3
  221. package/src/skills/sonamu/api.md +102 -0
  222. package/src/skills/sonamu/database.md +220 -1
  223. package/src/skills/sonamu/entity-relations.md +89 -1
  224. package/src/skills/sonamu/fixture-cli.md +501 -0
  225. package/src/skills/sonamu/frontend.md +214 -0
  226. package/src/skills/sonamu/i18n.md +95 -0
  227. package/src/skills/sonamu/model.md +153 -0
  228. package/src/skills/sonamu/project-init.md +178 -8
  229. package/src/skills/sonamu/scaffolding.md +112 -0
  230. package/src/skills/sonamu/subset.md +9 -3
  231. package/src/skills/sonamu/testing.md +287 -2
  232. package/src/skills/sonamu/workflow.md +70 -5
  233. package/src/ssr/renderer.ts +2 -2
  234. package/src/syncer/api-parser.ts +12 -0
  235. package/src/syncer/checksum.ts +0 -38
  236. package/src/syncer/syncer-actions.ts +7 -1
  237. package/src/syncer/syncer.ts +16 -5
  238. package/src/tasks/workflow-manager.ts +29 -8
  239. package/src/template/entity-converter.ts +3 -3
  240. package/src/template/helpers.ts +49 -0
  241. package/src/template/implementations/entry-server.template.ts +1 -1
  242. package/src/template/implementations/generated.template.ts +4 -0
  243. package/src/template/implementations/generated_http.template.ts +1 -0
  244. package/src/template/implementations/generated_sso.template.ts +40 -11
  245. package/src/template/implementations/queries.template.ts +8 -0
  246. package/src/template/implementations/sd.template.ts +22 -3
  247. package/src/template/implementations/services.template.ts +11 -10
  248. package/src/template/implementations/view_form.template.ts +111 -101
  249. package/src/template/implementations/view_list.template.ts +120 -119
  250. package/src/template/implementations/view_search_input.template.ts +17 -13
  251. package/src/template/zod-converter.ts +103 -6
  252. package/src/testing/_relation-graph.ts +1 -1
  253. package/src/testing/data-explorer.ts +427 -0
  254. package/src/testing/faker-mappings.ts +434 -0
  255. package/src/testing/fixture-generator.ts +1166 -0
  256. package/src/testing/fixture-manager.ts +91 -6
  257. package/src/testing/index.ts +3 -0
  258. package/src/types/types.ts +222 -26
  259. package/src/ui/ai-client.ts +9 -1
  260. package/src/ui/api.ts +429 -23
  261. package/src/utils/fs-utils.ts +14 -1
  262. package/dist/template/implementations/view_enums_select.template.d.ts +0 -17
  263. package/dist/template/implementations/view_enums_select.template.d.ts.map +0 -1
  264. package/dist/template/implementations/view_enums_select.template.js +0 -62
  265. package/dist/template/implementations/view_id_async_select.template.d.ts +0 -17
  266. package/dist/template/implementations/view_id_async_select.template.d.ts.map +0 -1
  267. package/dist/template/implementations/view_id_async_select.template.js +0 -125
  268. package/dist/ui-web/assets/index-Bd_2AkLb.css +0 -1
  269. package/dist/ui-web/assets/index-BpSbhQWo.js +0 -225
  270. package/src/template/implementations/view_enums_select.template.ts +0 -65
  271. package/src/template/implementations/view_id_async_select.template.ts +0 -139
@@ -0,0 +1,427 @@
1
+ import type { Knex } from "knex";
2
+ import type { CacheManager } from "../cache/types";
3
+ import type { Entity } from "../entity/entity";
4
+ import type { EntityManager } from "../entity/entity-manager";
5
+ import { isBelongsToOneRelationProp, isOneToOneRelationProp, isRelationProp } from "../types/types";
6
+
7
+ export type DataExplorerStrategy = "sample" | "ids" | "query" | "file" | "recent" | "random";
8
+
9
+ /** WHERE 조건 타입 (객체 또는 Knex QueryBuilder 함수) */
10
+ export type WhereCondition = Record<string, unknown> | ((queryBuilder: Knex.QueryBuilder) => void);
11
+
12
+ export type DataExplorerOptions = {
13
+ strategy: DataExplorerStrategy;
14
+ limit?: number;
15
+ where?: WhereCondition;
16
+ orderBy?: string;
17
+ ids?: number[];
18
+ filePath?: string;
19
+ /** 캐싱 사용 여부 (기본값: false) */
20
+ useCache?: boolean;
21
+ /** 캐시 TTL (초 단위, 기본값: 300) */
22
+ cacheTtl?: number;
23
+ };
24
+
25
+ export type ExploreWithRelationsOptions = DataExplorerOptions & {
26
+ /** 관련 데이터 포함 여부 (기본값: true) */
27
+ includeRelations?: boolean;
28
+ /** 재귀 탐색 최대 깊이 (기본값: 2) */
29
+ maxDepth?: number;
30
+ };
31
+
32
+ export type ExploreWithRelationsResult = {
33
+ /** 메인 entity 데이터 */
34
+ main: {
35
+ entityId: string;
36
+ records: Record<string, unknown>[];
37
+ };
38
+ /** 관련 entity 데이터 (entityId -> records) */
39
+ related: Map<string, Record<string, unknown>[]>;
40
+ };
41
+
42
+ // 기존 DB 데이터를 탐색하여 fixture 생성 시 참조할 수 있는 시스템
43
+ export class DataExplorer {
44
+ private cache?: CacheManager;
45
+
46
+ constructor(
47
+ private db: Knex,
48
+ private entityManager: typeof EntityManager,
49
+ cacheManager?: CacheManager,
50
+ ) {
51
+ this.cache = cacheManager;
52
+ }
53
+
54
+ async explore(
55
+ entityName: string,
56
+ options: DataExplorerOptions,
57
+ ): Promise<Record<string, unknown>[]> {
58
+ const entity = this.entityManager.get(entityName);
59
+ if (!entity) {
60
+ throw new Error(`Entity not found: ${entityName}`);
61
+ }
62
+
63
+ // 캐싱 지원
64
+ if (options.useCache && this.cache) {
65
+ const cacheKey = this.generateCacheKey(entityName, options);
66
+ return this.cache.getOrSet({
67
+ key: cacheKey,
68
+ ttl: options.cacheTtl || 300,
69
+ factory: () => this.exploreInternal(entity, options),
70
+ });
71
+ }
72
+
73
+ return this.exploreInternal(entity, options);
74
+ }
75
+
76
+ private async exploreInternal(
77
+ entity: Entity,
78
+ options: DataExplorerOptions,
79
+ ): Promise<Record<string, unknown>[]> {
80
+ const query = this.db(entity.table);
81
+
82
+ switch (options.strategy) {
83
+ case "sample":
84
+ return await this.sampleData(query, options.limit || 10);
85
+
86
+ case "recent": {
87
+ const createdAtCol = this.findTimestampColumn(entity, "created_at");
88
+ if (createdAtCol) {
89
+ query.orderBy(createdAtCol, "desc");
90
+ }
91
+ return await query.limit(options.limit || 10);
92
+ }
93
+
94
+ case "random":
95
+ return await this.randomSample(query, options.limit || 10);
96
+
97
+ case "ids":
98
+ if (options.ids && options.ids.length > 0) {
99
+ query.whereIn("id", options.ids);
100
+ }
101
+ return await query;
102
+
103
+ case "query":
104
+ if (options.where) {
105
+ query.where(options.where);
106
+ }
107
+ if (options.orderBy) {
108
+ const [col, dir = "asc"] = options.orderBy.split(":");
109
+ // id 컬럼은 숫자로 캐스팅하여 정렬합니다 (문자열 정렬 방지)
110
+ if (col === "id") {
111
+ query.orderByRaw(`CAST(?? AS INTEGER) ${dir}`, [col]);
112
+ } else {
113
+ query.orderBy(col, dir as "asc" | "desc");
114
+ }
115
+ }
116
+ return await query.limit(options.limit || 10);
117
+
118
+ case "file":
119
+ if (!options.filePath) {
120
+ throw new Error("filePath is required for file strategy");
121
+ }
122
+ return this.loadFromFile(options.filePath);
123
+
124
+ default:
125
+ throw new Error(`Unknown strategy: ${options.strategy}`);
126
+ }
127
+ }
128
+
129
+ // 균등 샘플링 (PostgreSQL ROW_NUMBER 사용)
130
+ private async sampleData(
131
+ query: Knex.QueryBuilder,
132
+ limit: number,
133
+ ): Promise<Record<string, unknown>[]> {
134
+ const [{ count }] = await query.clone().count("* as count");
135
+ const total = Number(count);
136
+
137
+ if (total <= limit) {
138
+ return await query.limit(limit);
139
+ }
140
+
141
+ // 균등 간격 계산
142
+ const interval = Math.floor(total / limit);
143
+
144
+ // 테이블명 추출
145
+ const tableName = query.toString().match(/from\s+"?(\w+)"?/i)?.[1];
146
+ if (!tableName) {
147
+ throw new Error("Could not extract table name from query");
148
+ }
149
+
150
+ // ROW_NUMBER()로 한 번에 균등 샘플링 (단일 쿼리)
151
+ const result = await this.db.raw(
152
+ `
153
+ WITH numbered_rows AS (
154
+ SELECT *, ROW_NUMBER() OVER (ORDER BY id) as rn
155
+ FROM ??
156
+ )
157
+ SELECT * FROM numbered_rows
158
+ WHERE MOD(rn - 1, ?) = 0
159
+ LIMIT ?
160
+ `,
161
+ [tableName, interval, limit],
162
+ );
163
+
164
+ return result.rows;
165
+ }
166
+
167
+ private async randomSample(
168
+ query: Knex.QueryBuilder,
169
+ limit: number,
170
+ ): Promise<Record<string, unknown>[]> {
171
+ return await query.orderByRaw("RANDOM()").limit(limit);
172
+ }
173
+
174
+ private findTimestampColumn(entity: Entity, columnName: string): string | null {
175
+ const prop = entity.props.find((p) => p.name === columnName);
176
+ return prop?.name || null;
177
+ }
178
+
179
+ private async loadFromFile(filePath: string): Promise<Record<string, unknown>[]> {
180
+ const fs = await import("fs/promises");
181
+ const content = await fs.readFile(filePath, "utf-8");
182
+
183
+ if (filePath.endsWith(".json")) {
184
+ const parsed = JSON.parse(content);
185
+ if (!Array.isArray(parsed)) {
186
+ throw new Error("JSON file must contain an array");
187
+ }
188
+ return parsed as Record<string, unknown>[];
189
+ } else if (filePath.endsWith(".csv")) {
190
+ const lines = content.split("\n").filter((line) => line.trim());
191
+ if (lines.length === 0) return [];
192
+
193
+ const headers = lines[0].split(",").map((h) => h.trim());
194
+ return lines.slice(1).map((line) => {
195
+ const values = line.split(",");
196
+ return headers.reduce(
197
+ (obj: Record<string, unknown>, header: string, i: number) => {
198
+ obj[header] = values[i]?.trim();
199
+ return obj;
200
+ },
201
+ {} as Record<string, unknown>,
202
+ );
203
+ });
204
+ }
205
+
206
+ throw new Error(`Unsupported file format: ${filePath}`);
207
+ }
208
+
209
+ async exploreRelation(
210
+ entityName: string,
211
+ relationProp: string,
212
+ options?: Partial<DataExplorerOptions>,
213
+ ): Promise<Record<string, unknown>[]> {
214
+ const entity = this.entityManager.get(entityName);
215
+ const prop = entity.props.find((p) => p.name === relationProp);
216
+
217
+ if (!prop || !isRelationProp(prop)) {
218
+ throw new Error(`Relation property not found: ${entityName}.${relationProp}`);
219
+ }
220
+
221
+ const dataSource = prop.cone?.dataSource;
222
+ const strategy = dataSource?.strategy || options?.strategy || "sample";
223
+ const config =
224
+ dataSource?.config && typeof dataSource.config === "object"
225
+ ? (dataSource.config as Record<string, unknown>)
226
+ : {};
227
+
228
+ return this.explore(prop.with, {
229
+ strategy,
230
+ limit: options?.limit || (typeof config.limit === "number" ? config.limit : 10),
231
+ ...(typeof config === "object" ? config : {}),
232
+ ...options,
233
+ });
234
+ }
235
+
236
+ /**
237
+ * 여러 relation을 병렬로 조회합니다 (N+1 문제 해결)
238
+ */
239
+ async exploreRelations(
240
+ entityName: string,
241
+ relationProps: string[],
242
+ options?: Partial<DataExplorerOptions>,
243
+ ): Promise<Record<string, Record<string, unknown>[]>> {
244
+ const results = await Promise.all(
245
+ relationProps.map(async (prop) => {
246
+ const data = await this.exploreRelation(entityName, prop, options);
247
+ return [prop, data] as const;
248
+ }),
249
+ );
250
+
251
+ return Object.fromEntries(results);
252
+ }
253
+
254
+ private generateCacheKey(entityName: string, options: DataExplorerOptions): string {
255
+ const parts = [
256
+ `DataExplorer:${entityName}`,
257
+ options.strategy,
258
+ options.limit?.toString() || "default",
259
+ ];
260
+
261
+ if (options.where) {
262
+ parts.push(JSON.stringify(options.where));
263
+ }
264
+ if (options.orderBy) {
265
+ parts.push(options.orderBy);
266
+ }
267
+ if (options.ids) {
268
+ parts.push(options.ids.join(","));
269
+ }
270
+
271
+ return parts.join(":");
272
+ }
273
+
274
+ /**
275
+ * Entity와 관련된 데이터를 재귀적으로 탐색합니다.
276
+ * BelongsToOne, OneToOne(hasJoinColumn) relation을 따라가며 참조 데이터를 수집합니다.
277
+ */
278
+ async exploreWithRelations(
279
+ entityName: string,
280
+ options: ExploreWithRelationsOptions,
281
+ ): Promise<ExploreWithRelationsResult> {
282
+ const includeRelations = options.includeRelations ?? true;
283
+ const maxDepth = options.maxDepth ?? 2;
284
+
285
+ // 메인 entity 조회
286
+ const mainRecords = await this.explore(entityName, options);
287
+
288
+ const result: ExploreWithRelationsResult = {
289
+ main: {
290
+ entityId: entityName,
291
+ records: mainRecords,
292
+ },
293
+ related: new Map(),
294
+ };
295
+
296
+ // 관련 데이터 수집하지 않으면 바로 리턴
297
+ if (!includeRelations || maxDepth <= 0) {
298
+ return result;
299
+ }
300
+
301
+ // 이미 조회한 entity 추적 (중복 방지)
302
+ const visited = new Set<string>([entityName]);
303
+
304
+ // 재귀적으로 관련 데이터 수집
305
+ await this.collectRelatedData(entityName, mainRecords, result.related, visited, maxDepth);
306
+
307
+ return result;
308
+ }
309
+
310
+ /**
311
+ * 관련 데이터를 재귀적으로 수집합니다 (private helper)
312
+ */
313
+ private async collectRelatedData(
314
+ entityName: string,
315
+ records: Record<string, unknown>[],
316
+ relatedMap: Map<string, Record<string, unknown>[]>,
317
+ visited: Set<string>,
318
+ remainingDepth: number,
319
+ ): Promise<void> {
320
+ if (remainingDepth <= 0 || records.length === 0) {
321
+ return;
322
+ }
323
+
324
+ const entity = this.entityManager.get(entityName);
325
+ const recordIds = records.map((r) => r.id).filter((id) => id != null);
326
+
327
+ // 1. Forward references: 이 entity가 참조하는 다른 entity
328
+ const forwardRelationProps = entity.props.filter(
329
+ (prop) =>
330
+ isRelationProp(prop) &&
331
+ (isBelongsToOneRelationProp(prop) || (isOneToOneRelationProp(prop) && prop.hasJoinColumn)),
332
+ );
333
+
334
+ for (const prop of forwardRelationProps) {
335
+ if (!isRelationProp(prop)) continue;
336
+
337
+ const targetEntityName = prop.with;
338
+
339
+ // 이미 조회한 entity는 스킵 (순환 참조 방지)
340
+ if (visited.has(targetEntityName)) {
341
+ continue;
342
+ }
343
+
344
+ // 참조하는 ID들 수집
345
+ const foreignKeyName = `${prop.name}_id`;
346
+ const referencedIds = records
347
+ .map((record) => record[foreignKeyName])
348
+ .filter((id) => id != null) as number[];
349
+
350
+ if (referencedIds.length === 0) {
351
+ continue;
352
+ }
353
+
354
+ // 중복 제거
355
+ const uniqueIds = [...new Set(referencedIds)];
356
+
357
+ // 참조 데이터 조회
358
+ const referencedRecords = await this.explore(targetEntityName, {
359
+ strategy: "ids",
360
+ ids: uniqueIds,
361
+ });
362
+
363
+ // 결과에 추가
364
+ relatedMap.set(targetEntityName, referencedRecords);
365
+ visited.add(targetEntityName);
366
+
367
+ // 재귀: 참조된 데이터의 관련 데이터도 수집
368
+ await this.collectRelatedData(
369
+ targetEntityName,
370
+ referencedRecords,
371
+ relatedMap,
372
+ visited,
373
+ remainingDepth - 1,
374
+ );
375
+ }
376
+
377
+ // 2. Backward references: 이 entity를 참조하는 다른 entity
378
+ // 모든 entity를 순회하며 현재 entity를 참조하는 relation 찾기
379
+ const allEntities = this.entityManager.getAllEntities();
380
+
381
+ for (const otherEntity of allEntities) {
382
+ const otherEntityName = otherEntity.id;
383
+
384
+ // 이미 조회했거나 자기 자신이면 스킵
385
+ if (visited.has(otherEntityName) || otherEntityName === entityName) {
386
+ continue;
387
+ }
388
+
389
+ // 현재 entity를 참조하는 relation prop 찾기
390
+ const backwardRelations = otherEntity.props.filter(
391
+ (prop) =>
392
+ isRelationProp(prop) &&
393
+ prop.with === entityName &&
394
+ (isBelongsToOneRelationProp(prop) ||
395
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)),
396
+ );
397
+
398
+ for (const prop of backwardRelations) {
399
+ if (!isRelationProp(prop)) continue;
400
+
401
+ // otherEntity가 현재 entity를 참조하는 FK 컬럼
402
+ const foreignKeyName = `${prop.name}_id`;
403
+
404
+ // 현재 레코드들을 참조하는 otherEntity 레코드 조회
405
+ const query = this.db(otherEntity.table).whereIn(foreignKeyName, recordIds);
406
+ const backwardRecords = await query;
407
+
408
+ if (backwardRecords.length === 0) {
409
+ continue;
410
+ }
411
+
412
+ // 결과에 추가
413
+ relatedMap.set(otherEntityName, backwardRecords);
414
+ visited.add(otherEntityName);
415
+
416
+ // 재귀: 역참조 데이터의 관련 데이터도 수집
417
+ await this.collectRelatedData(
418
+ otherEntityName,
419
+ backwardRecords,
420
+ relatedMap,
421
+ visited,
422
+ remainingDepth - 1,
423
+ );
424
+ }
425
+ }
426
+ }
427
+ }