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,302 @@
1
+ import chalk from "chalk";
2
+ import prompts from "prompts";
3
+ import { Sonamu } from "../api";
4
+ import { DB } from "../database/db";
5
+ import { createKnexInstance } from "../database/knex";
6
+ import { EntityManager } from "../entity/entity-manager";
7
+ import { DataExplorer, type DataExplorerStrategy } from "../testing/data-explorer";
8
+ import { FixtureGenerator } from "../testing/fixture-generator";
9
+
10
+ interface FixtureCommandOptions {
11
+ _?: string[];
12
+ all?: boolean;
13
+ include?: string;
14
+ exclude?: string;
15
+ count?: string;
16
+ "save-to"?: string;
17
+ strategy?: DataExplorerStrategy;
18
+ limit?: string;
19
+ }
20
+
21
+ /**
22
+ * fixture gen 명령어 - cone 메타데이터를 활용하여 자동으로 fixture를 생성합니다.
23
+ */
24
+ export async function fixtureGenCommand(options: FixtureCommandOptions) {
25
+ try {
26
+ if (!EntityManager.isAutoloaded) {
27
+ await EntityManager.autoload();
28
+ }
29
+
30
+ let entityNames: string[];
31
+
32
+ if (options.all) {
33
+ entityNames = EntityManager.getAllIds();
34
+ if (options.exclude) {
35
+ const excludeList = options.exclude.split(",").map((s: string) => s.trim());
36
+ entityNames = entityNames.filter((name) => !excludeList.includes(name));
37
+ }
38
+ } else if (options.include) {
39
+ entityNames = options.include.split(",").map((s: string) => s.trim());
40
+ } else {
41
+ const result = await prompts({
42
+ type: "multiselect",
43
+ name: "entities",
44
+ message: "Fixture를 생성할 Entity를 선택하세요:",
45
+ choices: EntityManager.getAllIds().map((id) => ({ title: id, value: id })),
46
+ min: 1,
47
+ });
48
+
49
+ if (!result.entities || result.entities.length === 0) {
50
+ console.log(chalk.yellow("취소되었습니다."));
51
+ return;
52
+ }
53
+
54
+ entityNames = result.entities;
55
+ }
56
+
57
+ let count = options.count ? Number.parseInt(options.count, 10) : 5;
58
+ if (!options.count) {
59
+ const result = await prompts({
60
+ type: "number",
61
+ name: "count",
62
+ message: "각 Entity별 생성 개수:",
63
+ initial: 5,
64
+ min: 1,
65
+ });
66
+
67
+ if (!result.count) {
68
+ console.log(chalk.yellow("취소되었습니다."));
69
+ return;
70
+ }
71
+
72
+ count = result.count;
73
+ }
74
+
75
+ let saveTarget = options["save-to"] || "db";
76
+ if (!options["save-to"]) {
77
+ const result = await prompts({
78
+ type: "select",
79
+ name: "saveTarget",
80
+ message: "저장 방식:",
81
+ choices: [
82
+ { title: "Fixture DB에 저장", value: "db" },
83
+ { title: "파일로 저장 (자동 파일명)", value: "file" },
84
+ { title: "파일로 저장 (파일명 지정)", value: "file:custom" },
85
+ { title: "저장 안 함 (출력만)", value: "none" },
86
+ ],
87
+ });
88
+
89
+ saveTarget = result.saveTarget;
90
+
91
+ if (saveTarget === "file:custom") {
92
+ const filenameResult = await prompts({
93
+ type: "text",
94
+ name: "filename",
95
+ message: "파일명:",
96
+ initial: "fixtures.json",
97
+ });
98
+
99
+ if (!filenameResult.filename) {
100
+ console.log(chalk.yellow("취소되었습니다."));
101
+ return;
102
+ }
103
+
104
+ saveTarget = `file:${filenameResult.filename}`;
105
+ }
106
+ }
107
+
108
+ // fixture gen: fixture DB 내에서 참조 관계를 해결하고 저장합니다
109
+ const fixtureDb = createKnexInstance(Sonamu.dbConfig.fixture);
110
+ const generator = new FixtureGenerator(fixtureDb, fixtureDb, "fixture", EntityManager);
111
+
112
+ console.log(chalk.cyan(`\n${entityNames.join(", ")} 생성 중...`));
113
+
114
+ const specs = entityNames.map((entityName) => ({
115
+ entity: entityName,
116
+ count,
117
+ overrides: {},
118
+ }));
119
+
120
+ const results = await generator.generateBatch(specs);
121
+
122
+ console.log(chalk.green(`\n✅ ${results.length}개 fixture 생성 완료`));
123
+
124
+ if (saveTarget === "none") {
125
+ console.log(JSON.stringify(results, null, 2));
126
+ } else if (saveTarget === "db") {
127
+ // generateBatch가 이미 DB에 저장했으므로 별도 저장이 불필요합니다.
128
+ console.log(chalk.green("Fixture DB에 저장되었습니다."));
129
+ } else if (saveTarget.startsWith("file")) {
130
+ const fs = await import("node:fs/promises");
131
+ const path = await import("node:path");
132
+
133
+ const fixturesDir = path.join(process.cwd(), "test", "fixtures");
134
+ try {
135
+ await fs.access(fixturesDir);
136
+ } catch {
137
+ await fs.mkdir(fixturesDir, { recursive: true });
138
+ }
139
+
140
+ for (const entityName of entityNames) {
141
+ const entityResults = results.filter((r) => r.entityId === entityName);
142
+
143
+ if (entityResults.length === 0) {
144
+ continue;
145
+ }
146
+
147
+ let filename: string;
148
+ if (saveTarget === "file") {
149
+ const entity = EntityManager.get(entityName);
150
+ filename = `${entity.table}.json`;
151
+ } else {
152
+ filename = saveTarget.replace("file:", "");
153
+ }
154
+
155
+ const filepath = path.join(fixturesDir, filename);
156
+ await fs.writeFile(
157
+ filepath,
158
+ JSON.stringify(
159
+ entityResults.map((r) => r.data),
160
+ null,
161
+ 2,
162
+ ),
163
+ );
164
+ console.log(chalk.green(`✅ ${filepath} 저장 완료`));
165
+ }
166
+ }
167
+ } catch (error) {
168
+ console.error(
169
+ chalk.red(
170
+ "Fixture 생성 중 오류가 발생했습니다.\n" +
171
+ "원인: Entity 정의나 DB 연결을 확인해주세요.\n" +
172
+ "자세한 내용:",
173
+ ),
174
+ error,
175
+ );
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * fixture fetch 명령어 - 실제 운영 DB에서 데이터를 가져와 fixture로 저장합니다.
182
+ * 관계된 데이터도 함께 가져오므로 현실적인 테스트 데이터를 확보할 수 있습니다.
183
+ */
184
+ export async function fixtureFetchCommand(options: FixtureCommandOptions) {
185
+ try {
186
+ if (!EntityManager.isAutoloaded) {
187
+ await EntityManager.autoload();
188
+ }
189
+
190
+ let entityNames: string[];
191
+
192
+ if (options.all) {
193
+ entityNames = EntityManager.getAllIds();
194
+ if (options.exclude) {
195
+ const excludeList = options.exclude.split(",").map((s: string) => s.trim());
196
+ entityNames = entityNames.filter((name) => !excludeList.includes(name));
197
+ }
198
+ } else if (options.include) {
199
+ entityNames = options.include.split(",").map((s: string) => s.trim());
200
+ } else {
201
+ const result = await prompts({
202
+ type: "multiselect",
203
+ name: "entities",
204
+ message: "Import할 Entity를 선택하세요:",
205
+ choices: EntityManager.getAllIds().map((id) => ({ title: id, value: id })),
206
+ min: 1,
207
+ });
208
+
209
+ if (!result.entities || result.entities.length === 0) {
210
+ console.log(chalk.yellow("취소되었습니다."));
211
+ return;
212
+ }
213
+
214
+ entityNames = result.entities;
215
+ }
216
+
217
+ const strategy: DataExplorerStrategy = options.strategy ?? "recent";
218
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : 10;
219
+
220
+ // fixture fetch: production 데이터를 fixture DB로 import합니다
221
+ const sourceDb = DB.getDB("r"); // production_master (또는 development_master)
222
+ const fixtureDb = createKnexInstance(Sonamu.dbConfig.fixture);
223
+ const generator = new FixtureGenerator(sourceDb, fixtureDb, "fixture", EntityManager);
224
+
225
+ console.log(chalk.cyan(`\n${entityNames.join(", ")} import 중...`));
226
+
227
+ for (const entityName of entityNames) {
228
+ const results = await generator.importFromSource(entityName, {
229
+ strategy,
230
+ limit,
231
+ includeRelations: true,
232
+ maxDepth: 2,
233
+ });
234
+
235
+ console.log(chalk.green(`✅ ${entityName}: ${results.length}개 import 완료`));
236
+ }
237
+ } catch (error) {
238
+ console.error(
239
+ chalk.red(
240
+ "실제 DB에서 데이터를 가져오는 중 오류가 발생했습니다.\n" +
241
+ "원인: 소스 DB 연결 설정(sonamu.config.ts)이나 Entity 관계 정의를 확인해주세요.\n" +
242
+ "자세한 내용:",
243
+ ),
244
+ error,
245
+ );
246
+ throw error;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * fixture explore 명령어 - DB의 실제 데이터를 조회하여 확인합니다.
252
+ * 저장하지 않고 조회만 하므로 데이터를 빠르게 확인할 때 유용합니다.
253
+ */
254
+ export async function fixtureExploreCommand(options: FixtureCommandOptions) {
255
+ try {
256
+ if (!EntityManager.isAutoloaded) {
257
+ await EntityManager.autoload();
258
+ }
259
+
260
+ let entityName = options.include;
261
+
262
+ if (!entityName) {
263
+ const result = await prompts({
264
+ type: "select",
265
+ name: "entity",
266
+ message: "탐색할 Entity:",
267
+ choices: EntityManager.getAllIds().map((id) => ({ title: id, value: id })),
268
+ });
269
+
270
+ if (!result.entity) {
271
+ console.log(chalk.yellow("취소되었습니다."));
272
+ return;
273
+ }
274
+
275
+ entityName = result.entity;
276
+ }
277
+
278
+ if (!entityName) {
279
+ throw new Error("Entity name is required");
280
+ }
281
+
282
+ const strategy: DataExplorerStrategy = options.strategy ?? "sample";
283
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : 10;
284
+
285
+ const db = DB.getDB("r");
286
+ const explorer = new DataExplorer(db, EntityManager);
287
+ const data = await explorer.explore(entityName, { strategy, limit });
288
+
289
+ console.log(chalk.cyan(`\n${entityName} ${data.length}개 조회 완료:`));
290
+ console.table(data);
291
+ } catch (error) {
292
+ console.error(
293
+ chalk.red(
294
+ "데이터 조회 중 오류가 발생했습니다.\n" +
295
+ "원인: DB 연결이나 Entity 정의를 확인해주세요.\n" +
296
+ "자세한 내용:",
297
+ ),
298
+ error,
299
+ );
300
+ throw error;
301
+ }
302
+ }
@@ -77,11 +77,12 @@ function serializeArgs(args: unknown[]): string {
77
77
  * 메서드의 결과를 캐싱합니다.
78
78
  *
79
79
  * @example
80
- * class UserModelClass extends BaseModel {
80
+ * class UserModelClass extends BaseModelClass<...> {
81
81
  * @cache({ ttl: '10m', tags: ['user'] })
82
82
  * @api()
83
- * async findById(id: number) {
84
- * return this.findOne(['id', id]);
83
+ * async findById(subset: UserSubsetKey, id: string) {
84
+ * const { rows } = await this.findMany(subset, { id, num: 1, page: 1 });
85
+ * return rows[0];
85
86
  * }
86
87
  * }
87
88
  */
@@ -0,0 +1,363 @@
1
+ import type { Cone, EntityJson } from "../types/types";
2
+
3
+ /**
4
+ * Cone 생성 컨텍스트
5
+ *
6
+ * Entity 정보와 생성 옵션을 담고 있습니다.
7
+ */
8
+ export type ConeGenerationContext = {
9
+ entity: EntityJson;
10
+ locale?: "ko" | "en" | "ja";
11
+ existingCones?: Record<string, Cone>;
12
+ /** true인 경우 fixtureHint가 없는 cone만 생성 */
13
+ onlyEmpty?: boolean;
14
+ };
15
+
16
+ /**
17
+ * Cone 생성 결과
18
+ *
19
+ * Entity, Props, Subsets, Enums의 cone 메타데이터를 담고 있습니다.
20
+ */
21
+ export type ConeGenerationResult = {
22
+ entityCone?: Cone;
23
+ propCones: Record<string, Cone>;
24
+ subsetCones: Record<string, Cone>;
25
+ enumCones: Record<string, Cone>;
26
+ tokensUsed: number;
27
+ };
28
+
29
+ /**
30
+ * LLM을 사용하여 Entity의 cone 메타데이터를 생성합니다.
31
+ *
32
+ * @param context - Entity 정보와 생성 옵션
33
+ * @returns 생성된 cone 메타데이터
34
+ */
35
+ export async function generateCones(context: ConeGenerationContext): Promise<ConeGenerationResult> {
36
+ const apiKey = getApiKey();
37
+ const prompt = buildPrompt(context);
38
+ const { text: responseText, tokensUsed } = await callAnthropicAPI(prompt, apiKey);
39
+ const result = parseConeResponse(responseText);
40
+ result.tokensUsed = tokensUsed;
41
+
42
+ if (context.existingCones) {
43
+ if (context.onlyEmpty) {
44
+ return mergeOnlyEmpty(result, context.existingCones);
45
+ }
46
+ return mergeWithExisting(result, context.existingCones);
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * API 키를 가져옵니다.
54
+ *
55
+ * Sonamu.secret 또는 환경변수에서 가져옵니다.
56
+ */
57
+ function getApiKey(): string {
58
+ // Sonamu.secret은 런타임에 로드되므로 동적으로 import
59
+ let apiKey: string | undefined;
60
+
61
+ try {
62
+ // Sonamu가 초기화되어 있는 경우
63
+ const { Sonamu } = require("../api");
64
+ apiKey = Sonamu.secret?.anthropic_api_key;
65
+ } catch {
66
+ // Sonamu가 초기화되지 않은 경우 (테스트 등)
67
+ apiKey = undefined;
68
+ }
69
+
70
+ if (!apiKey) {
71
+ apiKey = process.env.ANTHROPIC_API_KEY;
72
+ }
73
+
74
+ if (!apiKey) {
75
+ throw new Error(
76
+ "ANTHROPIC_API_KEY not found. " +
77
+ "Set ANTHROPIC_API_KEY environment variable or add it to sonamu.secret.ts",
78
+ );
79
+ }
80
+
81
+ return apiKey;
82
+ }
83
+
84
+ /**
85
+ * LLM 프롬프트를 생성합니다.
86
+ *
87
+ * ai-client.ts 패턴을 참고하여 명확한 지시사항과 출력 형식을 제공합니다.
88
+ */
89
+ function buildPrompt(context: ConeGenerationContext): string {
90
+ const locale = context.locale || "ko";
91
+ const localeDesc = {
92
+ ko: "Korean",
93
+ en: "English",
94
+ ja: "Japanese",
95
+ }[locale];
96
+
97
+ return `You are a Sonamu framework expert. Generate cone metadata for database entity fixture generation.
98
+
99
+ ENTITY STRUCTURE:
100
+ ${JSON.stringify(context.entity, null, 2)}
101
+
102
+ LOCALE: ${locale} (${localeDesc})
103
+
104
+ INSTRUCTIONS:
105
+ 1. Entity cone metadata:
106
+ - desc: Short description of what this entity represents
107
+ - note: Explain the entity's purpose, relationships, and business context
108
+ - tags: Relevant categorization tags
109
+ - fixtureHint: Overall guidance for generating test data for this entity
110
+
111
+ 2. For each prop, generate appropriate cone metadata:
112
+ - desc: Short description in ${localeDesc}
113
+ - note: Additional notes if needed
114
+ - fixtureHint: Detailed guidance for realistic test data generation
115
+ - fixtureGenerator: faker.js expression when applicable
116
+
117
+ 3. Field type → faker.js mapping:
118
+ - email → faker.internet.email()
119
+ - phone → faker.phone.number()
120
+ - name/username → faker.person.fullName() (with locale)
121
+ - birth_date → faker.date.birthdate({ min: 18, max: 65, mode: 'age' })
122
+ - salary → faker.number.int({ min: 30_000_000, max: 150_000_000 }) for ko locale
123
+ - company_name → faker.company.name()
124
+ - address → faker.location.streetAddress()
125
+
126
+ 4. Relation fields (BelongsToOne, OneToOne with hasJoinColumn):
127
+ - Always add dataSource: { strategy: "recent", config: { limit: 3-5 } }
128
+ - fixtureHint: Explain that it references existing data
129
+
130
+ 5. Subsets cone metadata (IMPORTANT - generate for ALL subsets):
131
+ - desc: Describe what this subset represents and what fields it includes
132
+ - note: Explain the use case and when to use this subset
133
+
134
+ 6. Enums cone metadata (IMPORTANT - generate for ALL enums):
135
+ - desc: Describe what this enum represents
136
+ - fixtureHint: If any prop uses this enum type, copy that prop's fixtureHint here
137
+ Example: If prop "status" (type: enum, enum: "UserStatus") has fixtureHint "User status: active, inactive, suspended",
138
+ then UserStatus enum's fixtureHint should be the same
139
+ - For each enum value, provide desc explaining what that specific value means
140
+
141
+ 7. Korean field names (locale=ko):
142
+ - Infer meaning and generate appropriate faker
143
+ - "이름" → faker.person.fullName()
144
+ - "생년월일" → faker.date.birthdate()
145
+ - "주소" → faker.location.streetAddress()
146
+
147
+ 8. Locale-specific values:
148
+ - ko: Korean names, addresses, phone numbers (010-XXXX-XXXX format)
149
+ - en: English names, US addresses
150
+ - ja: Japanese names, addresses
151
+
152
+ ${
153
+ context.existingCones
154
+ ? `
155
+ EXISTING CONES (preserve these if present):
156
+ ${JSON.stringify(context.existingCones, null, 2)}
157
+ `
158
+ : ""
159
+ }
160
+
161
+ OUTPUT FORMAT:
162
+ Return ONLY valid JSON (no markdown, no code blocks). Use this exact structure:
163
+ {
164
+ "entityCone": {
165
+ "desc": "${localeDesc} description of the entity",
166
+ "note": "Optional detailed notes about the entity's purpose and relationships",
167
+ "tags": ["optional", "tags"],
168
+ "fixtureHint": "Guidance for generating fixtures of this entity (what to consider, relationships, constraints)"
169
+ },
170
+ "propCones": {
171
+ "prop_name": {
172
+ "desc": "${localeDesc} description",
173
+ "note": "Optional additional notes about this field",
174
+ "fixtureHint": "Detailed generation guidance for this specific field",
175
+ "fixtureGenerator": "faker.xxx.yyy()",
176
+ "dataSource": { "strategy": "recent", "config": { "limit": 5 } } // Only for relation fields
177
+ }
178
+ },
179
+ "subsetCones": {
180
+ "A": {
181
+ "desc": "${localeDesc} description of subset A",
182
+ "note": "What fields are included in this subset",
183
+ "fixtureHint": "Optional guidance if this subset affects fixture generation"
184
+ }
185
+ },
186
+ "enumCones": {
187
+ "EnumName": {
188
+ "desc": "${localeDesc} description of the enum",
189
+ "fixtureHint": "Optional guidance for generating enum values in fixtures",
190
+ "values": {
191
+ "VALUE_KEY": {
192
+ "desc": "${localeDesc} description of this enum value"
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ IMPORTANT: Return pure JSON only. Do NOT wrap in markdown code blocks.`;
200
+ }
201
+
202
+ /**
203
+ * Anthropic API를 호출하여 LLM 응답을 받습니다.
204
+ *
205
+ * @param prompt - 생성할 프롬프트
206
+ * @param apiKey - Anthropic API 키
207
+ * @returns LLM 응답 텍스트 및 토큰 사용량
208
+ */
209
+ async function callAnthropicAPI(
210
+ prompt: string,
211
+ apiKey: string,
212
+ ): Promise<{ text: string; tokensUsed: number }> {
213
+ try {
214
+ // @ai-sdk/anthropic과 ai 패키지는 optional dependency이므로 동적 import
215
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
216
+ const { generateText } = await import("ai");
217
+
218
+ const anthropic = createAnthropic({
219
+ apiKey,
220
+ });
221
+
222
+ const { text, usage } = await generateText({
223
+ model: anthropic("claude-sonnet-4-5"),
224
+ prompt,
225
+ });
226
+
227
+ const tokensUsed = usage?.totalTokens || 0;
228
+ if (usage) {
229
+ console.log(`[Cone Generator] Tokens used: ${tokensUsed}`);
230
+ }
231
+
232
+ return { text, tokensUsed };
233
+ } catch (error: unknown) {
234
+ if (error && typeof error === "object" && "statusCode" in error) {
235
+ const statusCode = (error as { statusCode: number }).statusCode;
236
+ if (statusCode === 429) {
237
+ throw new Error("Rate limit exceeded. Please try again later.");
238
+ }
239
+ }
240
+
241
+ const message = error instanceof Error ? error.message : "Unknown error";
242
+ throw new Error(`LLM API failed: ${message}`);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * LLM 응답을 파싱하여 ConeGenerationResult로 변환합니다.
248
+ *
249
+ * Markdown 코드 블록이 포함되어 있으면 제거합니다.
250
+ */
251
+ function parseConeResponse(text: string): ConeGenerationResult {
252
+ let jsonText = text.trim();
253
+ jsonText = jsonText.replace(/^```json\s*/i, "");
254
+ jsonText = jsonText.replace(/```\s*$/, "");
255
+ jsonText = jsonText.trim();
256
+
257
+ try {
258
+ const parsed = JSON.parse(jsonText);
259
+
260
+ if (!parsed.propCones || typeof parsed.propCones !== "object") {
261
+ throw new Error("Invalid response: propCones is required and must be an object");
262
+ }
263
+
264
+ return {
265
+ entityCone: parsed.entityCone,
266
+ propCones: parsed.propCones,
267
+ subsetCones: parsed.subsetCones || {},
268
+ enumCones: parsed.enumCones || {},
269
+ tokensUsed: 0,
270
+ };
271
+ } catch (error) {
272
+ const message = error instanceof Error ? error.message : "Unknown error";
273
+ throw new Error(
274
+ `Failed to parse LLM response: ${message}\n\n` +
275
+ `Original response:\n${text}\n\n` +
276
+ `Cleaned JSON:\n${jsonText}`,
277
+ );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * 생성된 cone을 기존 cone과 병합합니다.
283
+ *
284
+ * 기존 cone이 있으면 보존하고, 없는 경우에만 생성된 cone을 사용합니다.
285
+ */
286
+ function mergeWithExisting(
287
+ generated: ConeGenerationResult,
288
+ existing: Record<string, Cone>,
289
+ ): ConeGenerationResult {
290
+ const result = { ...generated };
291
+
292
+ const entityKey = `entity:${generated.entityCone ? "present" : "missing"}`;
293
+ if (existing[entityKey]) {
294
+ result.entityCone = existing[entityKey];
295
+ }
296
+
297
+ for (const propName of Object.keys(generated.propCones)) {
298
+ const key = `prop:${propName}`;
299
+ if (existing[key]) {
300
+ result.propCones[propName] = existing[key];
301
+ }
302
+ }
303
+
304
+ for (const enumId of Object.keys(generated.enumCones)) {
305
+ const key = `enum:${enumId}`;
306
+ if (existing[key]) {
307
+ result.enumCones[enumId] = existing[key];
308
+ }
309
+ }
310
+
311
+ for (const subsetKey of Object.keys(generated.subsetCones)) {
312
+ const key = `subset:${subsetKey}`;
313
+ if (existing[key]) {
314
+ result.subsetCones[subsetKey] = existing[key];
315
+ }
316
+ }
317
+
318
+ return result;
319
+ }
320
+
321
+ /**
322
+ * fixtureHint가 없는 cone만 생성하고 나머지는 보존합니다.
323
+ *
324
+ * 기존 cone에 fixtureHint가 있으면 보존하고, 없으면 새로 생성된 cone을 사용합니다.
325
+ */
326
+ function mergeOnlyEmpty(
327
+ generated: ConeGenerationResult,
328
+ existing: Record<string, Cone>,
329
+ ): ConeGenerationResult {
330
+ const result = { ...generated };
331
+
332
+ // Entity cone: fixtureHint가 있으면 보존
333
+ const entityKey = `entity:${generated.entityCone ? "present" : "missing"}`;
334
+ if (existing[entityKey]?.fixtureHint) {
335
+ result.entityCone = existing[entityKey];
336
+ }
337
+
338
+ // Prop cones: fixtureHint가 있으면 보존
339
+ for (const propName of Object.keys(generated.propCones)) {
340
+ const key = `prop:${propName}`;
341
+ if (existing[key]?.fixtureHint) {
342
+ result.propCones[propName] = existing[key];
343
+ }
344
+ }
345
+
346
+ // Enum cones: fixtureHint가 있으면 보존 (enum은 보통 fixtureHint가 없지만 일관성을 위해)
347
+ for (const enumId of Object.keys(generated.enumCones)) {
348
+ const key = `enum:${enumId}`;
349
+ if (existing[key]?.fixtureHint) {
350
+ result.enumCones[enumId] = existing[key];
351
+ }
352
+ }
353
+
354
+ // Subset cones: fixtureHint가 있으면 보존 (subset도 보통 fixtureHint가 없지만 일관성을 위해)
355
+ for (const subsetKey of Object.keys(generated.subsetCones)) {
356
+ const key = `subset:${subsetKey}`;
357
+ if (existing[key]?.fixtureHint) {
358
+ result.subsetCones[subsetKey] = existing[key];
359
+ }
360
+ }
361
+
362
+ return result;
363
+ }