sonamu 0.5.7 → 0.7.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 (529) hide show
  1. package/.swcrc.project-default +18 -0
  2. package/bin/cli.js +24 -0
  3. package/dist/ai/agents/agent.d.ts +11 -0
  4. package/dist/ai/agents/agent.d.ts.map +1 -0
  5. package/dist/ai/agents/agent.js +65 -0
  6. package/dist/ai/agents/index.d.ts +3 -0
  7. package/dist/ai/agents/index.d.ts.map +1 -0
  8. package/dist/ai/agents/index.js +4 -0
  9. package/dist/ai/agents/types.d.ts +43 -0
  10. package/dist/ai/agents/types.d.ts.map +1 -0
  11. package/dist/ai/agents/types.js +3 -0
  12. package/dist/ai/index.d.ts +2 -0
  13. package/dist/ai/index.d.ts.map +1 -0
  14. package/dist/ai/index.js +3 -0
  15. package/dist/ai/providers/rtzr/api.d.ts +22 -0
  16. package/dist/ai/providers/rtzr/api.d.ts.map +1 -0
  17. package/dist/ai/providers/rtzr/api.js +28 -0
  18. package/dist/ai/providers/rtzr/error.d.ts +18 -0
  19. package/dist/ai/providers/rtzr/error.d.ts.map +1 -0
  20. package/dist/ai/providers/rtzr/error.js +29 -0
  21. package/dist/ai/providers/rtzr/index.d.ts +5 -0
  22. package/dist/ai/providers/rtzr/index.d.ts.map +1 -0
  23. package/dist/ai/providers/rtzr/index.js +6 -0
  24. package/dist/ai/providers/rtzr/model.d.ts +52 -0
  25. package/dist/ai/providers/rtzr/model.d.ts.map +1 -0
  26. package/dist/ai/providers/rtzr/model.js +137 -0
  27. package/dist/ai/providers/rtzr/options.d.ts +7 -0
  28. package/dist/ai/providers/rtzr/options.d.ts.map +1 -0
  29. package/dist/ai/providers/rtzr/options.js +47 -0
  30. package/dist/ai/providers/rtzr/provider.d.ts +18 -0
  31. package/dist/ai/providers/rtzr/provider.d.ts.map +1 -0
  32. package/dist/ai/providers/rtzr/provider.js +54 -0
  33. package/dist/ai/providers/rtzr/utils.d.ts +19 -0
  34. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -0
  35. package/dist/ai/providers/rtzr/utils.js +88 -0
  36. package/dist/api/base-frame.d.ts +2 -2
  37. package/dist/api/base-frame.d.ts.map +1 -1
  38. package/dist/api/base-frame.js +13 -2
  39. package/dist/api/caster.d.ts.map +1 -1
  40. package/dist/api/caster.js +71 -2
  41. package/dist/api/code-converters.d.ts +58 -14
  42. package/dist/api/code-converters.d.ts.map +1 -1
  43. package/dist/api/code-converters.js +258 -2
  44. package/dist/api/config.d.ts +90 -0
  45. package/dist/api/config.d.ts.map +1 -0
  46. package/dist/api/config.js +25 -0
  47. package/dist/api/context.d.ts +4 -2
  48. package/dist/api/context.d.ts.map +1 -1
  49. package/dist/api/context.js +3 -2
  50. package/dist/api/decorators.d.ts +20 -6
  51. package/dist/api/decorators.d.ts.map +1 -1
  52. package/dist/api/decorators.js +235 -2
  53. package/dist/api/index.d.ts +2 -2
  54. package/dist/api/index.d.ts.map +1 -1
  55. package/dist/api/index.js +9 -2
  56. package/dist/api/sonamu.d.ts +10 -24
  57. package/dist/api/sonamu.d.ts.map +1 -1
  58. package/dist/api/sonamu.js +514 -2
  59. package/dist/api/validator.d.ts +6 -0
  60. package/dist/api/validator.d.ts.map +1 -0
  61. package/dist/api/validator.js +81 -0
  62. package/dist/bin/build-config.d.ts +6 -1
  63. package/dist/bin/build-config.d.ts.map +1 -1
  64. package/dist/bin/build-config.js +15 -2
  65. package/dist/bin/cli.js +519 -2
  66. package/dist/bin/hot-hook-register.d.ts +11 -0
  67. package/dist/bin/hot-hook-register.d.ts.map +1 -0
  68. package/dist/bin/hot-hook-register.js +21 -0
  69. package/dist/bin/loader-register.d.ts +2 -0
  70. package/dist/bin/loader-register.d.ts.map +1 -0
  71. package/dist/bin/loader-register.js +34 -0
  72. package/dist/database/_batch_update.d.ts +5 -3
  73. package/dist/database/_batch_update.d.ts.map +1 -1
  74. package/dist/database/_batch_update.js +95 -2
  75. package/dist/database/base-model.d.ts +96 -10
  76. package/dist/database/base-model.d.ts.map +1 -1
  77. package/dist/database/base-model.js +390 -2
  78. package/dist/database/base-model.types.d.ts +93 -0
  79. package/dist/database/base-model.types.d.ts.map +1 -0
  80. package/dist/database/base-model.types.js +10 -0
  81. package/dist/database/code-generator.d.ts +1 -1
  82. package/dist/database/code-generator.d.ts.map +1 -1
  83. package/dist/database/code-generator.js +54 -2
  84. package/dist/database/db.d.ts +6 -21
  85. package/dist/database/db.d.ts.map +1 -1
  86. package/dist/database/db.js +129 -2
  87. package/dist/database/puri-subset.test-d.js +81 -0
  88. package/dist/database/puri-subset.types.d.ts +123 -0
  89. package/dist/database/puri-subset.types.d.ts.map +1 -0
  90. package/dist/database/puri-subset.types.js +16 -0
  91. package/dist/database/puri-wrapper.d.ts +13 -11
  92. package/dist/database/puri-wrapper.d.ts.map +1 -1
  93. package/dist/database/puri-wrapper.js +109 -2
  94. package/dist/database/puri.d.ts +41 -23
  95. package/dist/database/puri.d.ts.map +1 -1
  96. package/dist/database/puri.js +601 -2
  97. package/dist/database/puri.types.d.ts +25 -6
  98. package/dist/database/puri.types.d.ts.map +1 -1
  99. package/dist/database/puri.types.js +6 -2
  100. package/dist/database/transaction-context.d.ts +1 -1
  101. package/dist/database/transaction-context.d.ts.map +1 -1
  102. package/dist/database/transaction-context.js +14 -2
  103. package/dist/database/upsert-builder.d.ts +9 -3
  104. package/dist/database/upsert-builder.d.ts.map +1 -1
  105. package/dist/database/upsert-builder.js +365 -2
  106. package/dist/entity/entity-manager.d.ts +167 -2
  107. package/dist/entity/entity-manager.d.ts.map +1 -1
  108. package/dist/entity/entity-manager.js +130 -2
  109. package/dist/entity/entity.d.ts +5 -3
  110. package/dist/entity/entity.d.ts.map +1 -1
  111. package/dist/entity/entity.js +750 -2
  112. package/dist/exceptions/error-handler.d.ts +1 -1
  113. package/dist/exceptions/error-handler.d.ts.map +1 -1
  114. package/dist/exceptions/error-handler.js +29 -2
  115. package/dist/exceptions/so-exceptions.d.ts +1 -1
  116. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  117. package/dist/exceptions/so-exceptions.js +85 -2
  118. package/dist/file-storage/driver.d.ts +1 -1
  119. package/dist/file-storage/driver.d.ts.map +1 -1
  120. package/dist/file-storage/driver.js +79 -2
  121. package/dist/file-storage/file-storage.js +75 -2
  122. package/dist/index.d.ts +18 -9
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +34 -2
  125. package/dist/migration/code-generation.d.ts +1 -1
  126. package/dist/migration/code-generation.d.ts.map +1 -1
  127. package/dist/migration/code-generation.js +614 -2
  128. package/dist/migration/migration-set.d.ts +2 -10
  129. package/dist/migration/migration-set.d.ts.map +1 -1
  130. package/dist/migration/migration-set.js +213 -2
  131. package/dist/migration/migrator.d.ts +24 -82
  132. package/dist/migration/migrator.d.ts.map +1 -1
  133. package/dist/migration/migrator.js +330 -2
  134. package/dist/migration/postgresql-schema-reader.d.ts +51 -0
  135. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -0
  136. package/dist/migration/postgresql-schema-reader.js +245 -0
  137. package/dist/migration/types.d.ts +6 -38
  138. package/dist/migration/types.d.ts.map +1 -1
  139. package/dist/migration/types.js +3 -2
  140. package/dist/naite/messaging-types.d.ts +43 -0
  141. package/dist/naite/messaging-types.d.ts.map +1 -0
  142. package/dist/naite/messaging-types.js +7 -0
  143. package/dist/naite/naite-reporter.d.ts +41 -0
  144. package/dist/naite/naite-reporter.d.ts.map +1 -0
  145. package/dist/naite/naite-reporter.js +102 -0
  146. package/dist/naite/naite.d.ts +95 -0
  147. package/dist/naite/naite.d.ts.map +1 -0
  148. package/dist/naite/naite.js +316 -0
  149. package/dist/stream/index.js +3 -2
  150. package/dist/stream/sse.d.ts +2 -2
  151. package/dist/stream/sse.d.ts.map +1 -1
  152. package/dist/stream/sse.js +38 -2
  153. package/dist/syncer/api-parser.d.ts +10 -0
  154. package/dist/syncer/api-parser.d.ts.map +1 -0
  155. package/dist/syncer/api-parser.js +240 -0
  156. package/dist/syncer/checksum.d.ts +21 -0
  157. package/dist/syncer/checksum.d.ts.map +1 -0
  158. package/dist/syncer/checksum.js +98 -0
  159. package/dist/syncer/code-generator.d.ts +20 -0
  160. package/dist/syncer/code-generator.d.ts.map +1 -0
  161. package/dist/syncer/code-generator.js +161 -0
  162. package/dist/syncer/entity-operations.d.ts +17 -0
  163. package/dist/syncer/entity-operations.d.ts.map +1 -0
  164. package/dist/syncer/entity-operations.js +59 -0
  165. package/dist/syncer/file-patterns.d.ts +29 -0
  166. package/dist/syncer/file-patterns.d.ts.map +1 -0
  167. package/dist/syncer/file-patterns.js +38 -0
  168. package/dist/syncer/index.d.ts +6 -0
  169. package/dist/syncer/index.d.ts.map +1 -1
  170. package/dist/syncer/index.js +9 -2
  171. package/dist/syncer/module-loader.d.ts +35 -0
  172. package/dist/syncer/module-loader.d.ts.map +1 -0
  173. package/dist/syncer/module-loader.js +87 -0
  174. package/dist/syncer/syncer.d.ts +98 -106
  175. package/dist/syncer/syncer.d.ts.map +1 -1
  176. package/dist/syncer/syncer.js +422 -2
  177. package/dist/template/entity-converter.d.ts +14 -0
  178. package/dist/template/entity-converter.d.ts.map +1 -0
  179. package/dist/template/entity-converter.js +108 -0
  180. package/dist/template/helpers.d.ts +23 -0
  181. package/dist/template/helpers.d.ts.map +1 -0
  182. package/dist/template/helpers.js +64 -0
  183. package/dist/{templates → template/implementations}/entity.template.d.ts +3 -3
  184. package/dist/template/implementations/entity.template.d.ts.map +1 -0
  185. package/dist/template/implementations/entity.template.js +86 -0
  186. package/dist/{templates → template/implementations}/generated.template.d.ts +3 -4
  187. package/dist/template/implementations/generated.template.d.ts.map +1 -0
  188. package/dist/template/implementations/generated.template.js +249 -0
  189. package/dist/{templates → template/implementations}/generated_http.template.d.ts +3 -4
  190. package/dist/template/implementations/generated_http.template.d.ts.map +1 -0
  191. package/dist/template/implementations/generated_http.template.js +131 -0
  192. package/dist/{templates → template/implementations}/generated_sso.template.d.ts +4 -5
  193. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -0
  194. package/dist/template/implementations/generated_sso.template.js +134 -0
  195. package/dist/{templates → template/implementations}/init_types.template.d.ts +3 -3
  196. package/dist/template/implementations/init_types.template.d.ts.map +1 -0
  197. package/dist/template/implementations/init_types.template.js +38 -0
  198. package/dist/template/implementations/model.template.d.ts +17 -0
  199. package/dist/template/implementations/model.template.d.ts.map +1 -0
  200. package/dist/template/implementations/model.template.js +181 -0
  201. package/dist/{templates → template/implementations}/model_test.template.d.ts +3 -3
  202. package/dist/template/implementations/model_test.template.d.ts.map +1 -0
  203. package/dist/template/implementations/model_test.template.js +35 -0
  204. package/dist/{templates → template/implementations}/service.template.d.ts +6 -6
  205. package/dist/template/implementations/service.template.d.ts.map +1 -0
  206. package/dist/template/implementations/service.template.js +201 -0
  207. package/dist/{templates → template/implementations}/view_enums_buttonset.template.d.ts +3 -3
  208. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +1 -0
  209. package/dist/template/implementations/view_enums_buttonset.template.js +31 -0
  210. package/dist/{templates → template/implementations}/view_enums_dropdown.template.d.ts +3 -4
  211. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +1 -0
  212. package/dist/template/implementations/view_enums_dropdown.template.js +50 -0
  213. package/dist/{templates → template/implementations}/view_enums_select.template.d.ts +3 -3
  214. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -0
  215. package/dist/template/implementations/view_enums_select.template.js +55 -0
  216. package/dist/{templates → template/implementations}/view_form.template.d.ts +5 -5
  217. package/dist/template/implementations/view_form.template.d.ts.map +1 -0
  218. package/dist/template/implementations/view_form.template.js +337 -0
  219. package/dist/{templates → template/implementations}/view_id_all_select.template.d.ts +3 -3
  220. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -0
  221. package/dist/template/implementations/view_id_all_select.template.js +31 -0
  222. package/dist/{templates → template/implementations}/view_id_async_select.template.d.ts +3 -3
  223. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -0
  224. package/dist/template/implementations/view_id_async_select.template.js +105 -0
  225. package/dist/{templates → template/implementations}/view_list.template.d.ts +5 -13
  226. package/dist/template/implementations/view_list.template.d.ts.map +1 -0
  227. package/dist/template/implementations/view_list.template.js +475 -0
  228. package/dist/template/implementations/view_list_columns.template.d.ts +17 -0
  229. package/dist/template/implementations/view_list_columns.template.d.ts.map +1 -0
  230. package/dist/template/implementations/view_list_columns.template.js +49 -0
  231. package/dist/{templates → template/implementations}/view_search_input.template.d.ts +3 -3
  232. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -0
  233. package/dist/template/implementations/view_search_input.template.js +64 -0
  234. package/dist/template/index.d.ts +7 -0
  235. package/dist/template/index.d.ts.map +1 -0
  236. package/dist/template/index.js +8 -0
  237. package/dist/template/template-manager.d.ts +56 -0
  238. package/dist/template/template-manager.d.ts.map +1 -0
  239. package/dist/template/template-manager.js +125 -0
  240. package/dist/template/template-types.d.ts +16 -0
  241. package/dist/template/template-types.d.ts.map +1 -0
  242. package/dist/template/template-types.js +7 -0
  243. package/dist/template/template.d.ts +49 -0
  244. package/dist/template/template.d.ts.map +1 -0
  245. package/dist/template/template.js +60 -0
  246. package/dist/template/zod-converter.d.ts +51 -0
  247. package/dist/template/zod-converter.d.ts.map +1 -0
  248. package/dist/template/zod-converter.js +449 -0
  249. package/dist/testing/_relation-graph.d.ts +1 -1
  250. package/dist/testing/_relation-graph.d.ts.map +1 -1
  251. package/dist/testing/_relation-graph.js +89 -2
  252. package/dist/testing/fixture-manager.d.ts +42 -11
  253. package/dist/testing/fixture-manager.d.ts.map +1 -1
  254. package/dist/testing/fixture-manager.js +623 -2
  255. package/dist/types/types.d.ts +747 -143
  256. package/dist/types/types.d.ts.map +1 -1
  257. package/dist/types/types.js +546 -2
  258. package/dist/typings/knex.d.js +3 -2
  259. package/dist/utils/async-utils.d.ts +7 -0
  260. package/dist/utils/async-utils.d.ts.map +1 -1
  261. package/dist/utils/async-utils.js +57 -2
  262. package/dist/utils/console-util.d.ts +2 -0
  263. package/dist/utils/console-util.d.ts.map +1 -0
  264. package/dist/utils/console-util.js +6 -0
  265. package/dist/utils/controller.d.ts +1 -0
  266. package/dist/utils/controller.d.ts.map +1 -1
  267. package/dist/utils/controller.js +29 -2
  268. package/dist/utils/esm-utils.d.ts +39 -0
  269. package/dist/utils/esm-utils.d.ts.map +1 -0
  270. package/dist/utils/esm-utils.js +49 -0
  271. package/dist/utils/formatter.d.ts +3 -0
  272. package/dist/utils/formatter.d.ts.map +1 -0
  273. package/dist/utils/formatter.js +110 -0
  274. package/dist/utils/fs-utils.d.ts +1 -1
  275. package/dist/utils/fs-utils.d.ts.map +1 -1
  276. package/dist/utils/fs-utils.js +17 -2
  277. package/dist/utils/lodash-able.d.ts.map +1 -1
  278. package/dist/utils/lodash-able.js +6 -2
  279. package/dist/utils/model.js +22 -2
  280. package/dist/utils/object-utils.d.ts +44 -0
  281. package/dist/utils/object-utils.d.ts.map +1 -0
  282. package/dist/utils/object-utils.js +191 -0
  283. package/dist/utils/path-utils.d.ts +89 -0
  284. package/dist/utils/path-utils.d.ts.map +1 -0
  285. package/dist/utils/path-utils.js +60 -0
  286. package/dist/utils/process-utils.d.ts +13 -0
  287. package/dist/utils/process-utils.d.ts.map +1 -0
  288. package/dist/utils/process-utils.js +36 -0
  289. package/dist/utils/sql-parser.d.ts +5 -1
  290. package/dist/utils/sql-parser.d.ts.map +1 -1
  291. package/dist/utils/sql-parser.js +46 -2
  292. package/dist/utils/type-utils.d.ts +23 -0
  293. package/dist/utils/type-utils.d.ts.map +1 -0
  294. package/dist/utils/type-utils.js +45 -0
  295. package/dist/utils/utils.d.ts +10 -7
  296. package/dist/utils/utils.d.ts.map +1 -1
  297. package/dist/utils/utils.js +72 -2
  298. package/dist/utils/zod-error.d.ts +1 -1
  299. package/dist/utils/zod-error.d.ts.map +1 -1
  300. package/dist/utils/zod-error.js +19 -2
  301. package/package.json +65 -27
  302. package/src/ai/agents/agent.ts +87 -0
  303. package/src/ai/agents/index.ts +2 -0
  304. package/src/ai/agents/types.ts +47 -0
  305. package/src/ai/index.ts +1 -0
  306. package/src/ai/providers/rtzr/api.ts +37 -0
  307. package/src/ai/providers/rtzr/error.ts +34 -0
  308. package/src/ai/providers/rtzr/index.ts +4 -0
  309. package/src/ai/providers/rtzr/model.ts +201 -0
  310. package/src/ai/providers/rtzr/options.ts +49 -0
  311. package/src/ai/providers/rtzr/provider.ts +91 -0
  312. package/src/ai/providers/rtzr/utils.ts +127 -0
  313. package/src/api/base-frame.ts +4 -2
  314. package/src/api/caster.ts +17 -23
  315. package/src/api/code-converters.ts +178 -535
  316. package/src/api/config.ts +125 -0
  317. package/src/api/context.ts +7 -17
  318. package/src/api/decorators.ts +176 -46
  319. package/src/api/index.ts +2 -2
  320. package/src/api/sonamu.ts +190 -167
  321. package/src/api/validator.ts +83 -0
  322. package/src/bin/build-config.ts +8 -1
  323. package/src/bin/cli.ts +258 -124
  324. package/src/bin/hot-hook-register.ts +22 -0
  325. package/src/bin/loader-register.ts +38 -0
  326. package/src/database/_batch_update.ts +46 -31
  327. package/src/database/base-model.ts +390 -182
  328. package/src/database/base-model.types.ts +155 -0
  329. package/src/database/code-generator.ts +13 -32
  330. package/src/database/db.ts +40 -96
  331. package/src/database/puri-subset.test-d.ts +471 -0
  332. package/src/database/puri-subset.types.ts +195 -0
  333. package/src/database/puri-wrapper.ts +58 -67
  334. package/src/database/puri.ts +229 -148
  335. package/src/database/puri.types.ts +76 -30
  336. package/src/database/transaction-context.ts +1 -1
  337. package/src/database/upsert-builder.ts +262 -132
  338. package/src/entity/entity-manager.ts +48 -36
  339. package/src/entity/entity.ts +330 -248
  340. package/src/exceptions/error-handler.ts +3 -3
  341. package/src/exceptions/so-exceptions.ts +11 -11
  342. package/src/file-storage/driver.ts +5 -5
  343. package/src/file-storage/file-storage.ts +2 -2
  344. package/src/index.ts +18 -10
  345. package/src/migration/code-generation.ts +185 -172
  346. package/src/migration/migration-set.ts +80 -293
  347. package/src/migration/migrator.ts +199 -571
  348. package/src/migration/mysql-schema-reader.ts.txt +272 -0
  349. package/src/migration/postgresql-schema-reader.ts +310 -0
  350. package/src/migration/types.ts +6 -39
  351. package/src/naite/messaging-types.ts +51 -0
  352. package/src/naite/naite-reporter.ts +128 -0
  353. package/src/naite/naite.ts +415 -0
  354. package/src/shared/web.shared.ts.txt +20 -24
  355. package/src/stream/sse.ts +5 -5
  356. package/src/syncer/api-parser.ts +282 -0
  357. package/src/syncer/checksum.ts +140 -0
  358. package/src/syncer/code-generator.ts +198 -0
  359. package/src/syncer/entity-operations.ts +65 -0
  360. package/src/syncer/file-patterns.ts +56 -0
  361. package/src/syncer/index.ts +6 -0
  362. package/src/syncer/module-loader.ts +128 -0
  363. package/src/syncer/syncer.ts +389 -1453
  364. package/src/template/entity-converter.ts +114 -0
  365. package/src/template/helpers.ts +81 -0
  366. package/src/{templates → template/implementations}/entity.template.ts +7 -7
  367. package/src/{templates → template/implementations}/generated.template.ts +101 -101
  368. package/src/{templates → template/implementations}/generated_http.template.ts +27 -57
  369. package/src/template/implementations/generated_sso.template.ts +151 -0
  370. package/src/{templates → template/implementations}/init_types.template.ts +5 -7
  371. package/src/{templates → template/implementations}/model.template.ts +52 -43
  372. package/src/{templates → template/implementations}/model_test.template.ts +5 -5
  373. package/src/{templates → template/implementations}/service.template.ts +66 -82
  374. package/src/{templates → template/implementations}/view_enums_buttonset.template.ts +3 -3
  375. package/src/{templates → template/implementations}/view_enums_dropdown.template.ts +4 -20
  376. package/src/{templates → template/implementations}/view_enums_select.template.ts +4 -4
  377. package/src/{templates → template/implementations}/view_form.template.ts +40 -83
  378. package/src/{templates → template/implementations}/view_id_all_select.template.ts +3 -3
  379. package/src/{templates → template/implementations}/view_id_async_select.template.ts +10 -24
  380. package/src/{templates → template/implementations}/view_list.template.ts +60 -152
  381. package/src/{templates → template/implementations}/view_list_columns.template.ts +5 -11
  382. package/src/{templates → template/implementations}/view_search_input.template.ts +3 -3
  383. package/src/template/index.ts +6 -0
  384. package/src/template/template-manager.ts +166 -0
  385. package/src/template/template-types.ts +16 -0
  386. package/src/template/template.ts +105 -0
  387. package/src/template/zod-converter.ts +525 -0
  388. package/src/testing/_relation-graph.ts +18 -11
  389. package/src/testing/fixture-manager.ts +472 -359
  390. package/src/types/types.ts +553 -308
  391. package/src/typings/knex.d.ts +7 -9
  392. package/src/utils/async-utils.ts +23 -10
  393. package/src/utils/console-util.ts +4 -0
  394. package/src/utils/controller.ts +3 -0
  395. package/src/utils/esm-utils.ts +59 -0
  396. package/src/utils/formatter.ts +109 -0
  397. package/src/utils/fs-utils.ts +1 -1
  398. package/src/utils/lodash-able.ts +1 -4
  399. package/src/utils/object-utils.ts +217 -0
  400. package/src/utils/path-utils.ts +99 -0
  401. package/src/utils/process-utils.ts +46 -0
  402. package/src/utils/sql-parser.ts +23 -5
  403. package/src/utils/type-utils.ts +83 -0
  404. package/src/utils/utils.ts +66 -43
  405. package/src/utils/zod-error.ts +3 -4
  406. package/dist/api/base-frame.js.map +0 -1
  407. package/dist/api/caster.js.map +0 -1
  408. package/dist/api/code-converters.js.map +0 -1
  409. package/dist/api/context.js.map +0 -1
  410. package/dist/api/decorators.js.map +0 -1
  411. package/dist/api/index.js.map +0 -1
  412. package/dist/api/sonamu.js.map +0 -1
  413. package/dist/bin/build-config.js.map +0 -1
  414. package/dist/bin/cli-wrapper.d.ts +0 -3
  415. package/dist/bin/cli-wrapper.d.ts.map +0 -1
  416. package/dist/bin/cli-wrapper.js +0 -3
  417. package/dist/bin/cli-wrapper.js.map +0 -1
  418. package/dist/bin/cli.js.map +0 -1
  419. package/dist/database/_batch_update.js.map +0 -1
  420. package/dist/database/base-model.js.map +0 -1
  421. package/dist/database/code-generator.js.map +0 -1
  422. package/dist/database/db.js.map +0 -1
  423. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts +0 -2
  424. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts.map +0 -1
  425. package/dist/database/knex-plugins/knex-on-duplicate-update.js +0 -2
  426. package/dist/database/knex-plugins/knex-on-duplicate-update.js.map +0 -1
  427. package/dist/database/puri-wrapper.js.map +0 -1
  428. package/dist/database/puri.js.map +0 -1
  429. package/dist/database/puri.types.js.map +0 -1
  430. package/dist/database/transaction-context.js.map +0 -1
  431. package/dist/database/upsert-builder.js.map +0 -1
  432. package/dist/entity/entity-manager.js.map +0 -1
  433. package/dist/entity/entity-utils.d.ts +0 -61
  434. package/dist/entity/entity-utils.d.ts.map +0 -1
  435. package/dist/entity/entity-utils.js +0 -2
  436. package/dist/entity/entity-utils.js.map +0 -1
  437. package/dist/entity/entity.js.map +0 -1
  438. package/dist/exceptions/error-handler.js.map +0 -1
  439. package/dist/exceptions/so-exceptions.js.map +0 -1
  440. package/dist/file-storage/driver.js.map +0 -1
  441. package/dist/file-storage/file-storage.js.map +0 -1
  442. package/dist/index.js.map +0 -1
  443. package/dist/migration/code-generation.js.map +0 -1
  444. package/dist/migration/migration-set.js.map +0 -1
  445. package/dist/migration/migrator.js.map +0 -1
  446. package/dist/migration/types.js.map +0 -1
  447. package/dist/stream/index.js.map +0 -1
  448. package/dist/stream/sse.js.map +0 -1
  449. package/dist/syncer/index.js.map +0 -1
  450. package/dist/syncer/syncer.js.map +0 -1
  451. package/dist/templates/base-template.d.ts +0 -13
  452. package/dist/templates/base-template.d.ts.map +0 -1
  453. package/dist/templates/base-template.js +0 -2
  454. package/dist/templates/base-template.js.map +0 -1
  455. package/dist/templates/entity.template.d.ts.map +0 -1
  456. package/dist/templates/entity.template.js +0 -2
  457. package/dist/templates/entity.template.js.map +0 -1
  458. package/dist/templates/generated.template.d.ts.map +0 -1
  459. package/dist/templates/generated.template.js +0 -2
  460. package/dist/templates/generated.template.js.map +0 -1
  461. package/dist/templates/generated_http.template.d.ts.map +0 -1
  462. package/dist/templates/generated_http.template.js +0 -2
  463. package/dist/templates/generated_http.template.js.map +0 -1
  464. package/dist/templates/generated_sso.template.d.ts.map +0 -1
  465. package/dist/templates/generated_sso.template.js +0 -2
  466. package/dist/templates/generated_sso.template.js.map +0 -1
  467. package/dist/templates/index.d.ts +0 -2
  468. package/dist/templates/index.d.ts.map +0 -1
  469. package/dist/templates/index.js +0 -2
  470. package/dist/templates/index.js.map +0 -1
  471. package/dist/templates/init_types.template.d.ts.map +0 -1
  472. package/dist/templates/init_types.template.js +0 -2
  473. package/dist/templates/init_types.template.js.map +0 -1
  474. package/dist/templates/model.template.d.ts +0 -17
  475. package/dist/templates/model.template.d.ts.map +0 -1
  476. package/dist/templates/model.template.js +0 -2
  477. package/dist/templates/model.template.js.map +0 -1
  478. package/dist/templates/model_test.template.d.ts.map +0 -1
  479. package/dist/templates/model_test.template.js +0 -2
  480. package/dist/templates/model_test.template.js.map +0 -1
  481. package/dist/templates/service.template.d.ts.map +0 -1
  482. package/dist/templates/service.template.js +0 -2
  483. package/dist/templates/service.template.js.map +0 -1
  484. package/dist/templates/view_enums_buttonset.template.d.ts.map +0 -1
  485. package/dist/templates/view_enums_buttonset.template.js +0 -2
  486. package/dist/templates/view_enums_buttonset.template.js.map +0 -1
  487. package/dist/templates/view_enums_dropdown.template.d.ts.map +0 -1
  488. package/dist/templates/view_enums_dropdown.template.js +0 -2
  489. package/dist/templates/view_enums_dropdown.template.js.map +0 -1
  490. package/dist/templates/view_enums_select.template.d.ts.map +0 -1
  491. package/dist/templates/view_enums_select.template.js +0 -2
  492. package/dist/templates/view_enums_select.template.js.map +0 -1
  493. package/dist/templates/view_form.template.d.ts.map +0 -1
  494. package/dist/templates/view_form.template.js +0 -2
  495. package/dist/templates/view_form.template.js.map +0 -1
  496. package/dist/templates/view_id_all_select.template.d.ts.map +0 -1
  497. package/dist/templates/view_id_all_select.template.js +0 -2
  498. package/dist/templates/view_id_all_select.template.js.map +0 -1
  499. package/dist/templates/view_id_async_select.template.d.ts.map +0 -1
  500. package/dist/templates/view_id_async_select.template.js +0 -2
  501. package/dist/templates/view_id_async_select.template.js.map +0 -1
  502. package/dist/templates/view_list.template.d.ts.map +0 -1
  503. package/dist/templates/view_list.template.js +0 -2
  504. package/dist/templates/view_list.template.js.map +0 -1
  505. package/dist/templates/view_list_columns.template.d.ts +0 -17
  506. package/dist/templates/view_list_columns.template.d.ts.map +0 -1
  507. package/dist/templates/view_list_columns.template.js +0 -2
  508. package/dist/templates/view_list_columns.template.js.map +0 -1
  509. package/dist/templates/view_search_input.template.d.ts.map +0 -1
  510. package/dist/templates/view_search_input.template.js +0 -2
  511. package/dist/templates/view_search_input.template.js.map +0 -1
  512. package/dist/testing/_relation-graph.js.map +0 -1
  513. package/dist/testing/fixture-manager.js.map +0 -1
  514. package/dist/types/types.js.map +0 -1
  515. package/dist/typings/knex.d.js.map +0 -1
  516. package/dist/utils/async-utils.js.map +0 -1
  517. package/dist/utils/controller.js.map +0 -1
  518. package/dist/utils/fs-utils.js.map +0 -1
  519. package/dist/utils/lodash-able.js.map +0 -1
  520. package/dist/utils/model.js.map +0 -1
  521. package/dist/utils/sql-parser.js.map +0 -1
  522. package/dist/utils/utils.js.map +0 -1
  523. package/dist/utils/zod-error.js.map +0 -1
  524. package/src/bin/cli-wrapper.ts +0 -75
  525. package/src/database/knex-plugins/knex-on-duplicate-update.ts +0 -45
  526. package/src/entity/entity-utils.ts +0 -291
  527. package/src/templates/base-template.ts +0 -19
  528. package/src/templates/generated_sso.template.ts +0 -138
  529. package/src/templates/index.ts +0 -1
@@ -1,27 +1,38 @@
1
+ import assert from "assert";
1
2
  import chalk from "chalk";
2
- import _ from "lodash";
3
+ import { execSync } from "child_process";
4
+ import { readFileSync, writeFileSync } from "fs";
5
+ import inflection from "inflection";
6
+ import knex, { type Knex } from "knex";
7
+ import { unique } from "radashi";
8
+ import { inspect } from "util";
3
9
  import { Sonamu } from "../api";
10
+ import { BaseModel } from "../database/base-model";
11
+ import type { SonamuDBConfig } from "../database/db";
12
+ import { type UBRef, UpsertBuilder } from "../database/upsert-builder";
13
+ import type { Entity } from "../entity/entity";
4
14
  import { EntityManager } from "../entity/entity-manager";
5
15
  import {
6
- EntityProp,
7
- FixtureImportResult,
8
- FixtureRecord,
9
- FixtureSearchOptions,
10
- ManyToManyRelationProp,
16
+ type EntityProp,
17
+ type FixtureImportResult,
18
+ type FixtureRecord,
19
+ type FixtureSearchOptions,
11
20
  isBelongsToOneRelationProp,
12
21
  isHasManyRelationProp,
13
22
  isManyToManyRelationProp,
14
23
  isOneToOneRelationProp,
15
24
  isRelationProp,
16
25
  isVirtualProp,
26
+ type ManyToManyRelationProp,
17
27
  } from "../types/types";
18
- import { Entity } from "../entity/entity";
19
- import inflection from "inflection";
20
- import { readFileSync, writeFileSync } from "fs";
21
28
  import { RelationGraph } from "./_relation-graph";
22
- import knex, { Knex } from "knex";
23
- import { BaseModel } from "../database/base-model";
24
- import { SonamuDBConfig } from "../database/db";
29
+
30
+ /** 사용자 지정 중복 확인 컬럼 (entityId별로 지정) */
31
+ export interface DuplicateCheckOptions {
32
+ columns?: {
33
+ [entityId: string]: string[];
34
+ };
35
+ }
25
36
 
26
37
  export class FixtureManagerClass {
27
38
  private _tdb: Knex | null = null;
@@ -49,6 +60,12 @@ export class FixtureManagerClass {
49
60
 
50
61
  private relationGraph = new RelationGraph();
51
62
 
63
+ // UpsertBuilder 기반 import를 위한 상태
64
+ private builder: UpsertBuilder = new UpsertBuilder();
65
+ private fixtureRefMap: Map<string, UBRef> = new Map();
66
+ private uuidToFixtureId: Map<string, string> = new Map();
67
+ private skippedFixtures: Map<string, { entityId: string; existingId: number }> = new Map();
68
+
52
69
  init() {
53
70
  if (this._tdb !== null) {
54
71
  return;
@@ -57,89 +74,19 @@ export class FixtureManagerClass {
57
74
  const tConn = Sonamu.dbConfig.test.connection as Knex.ConnectionConfig & {
58
75
  port?: number;
59
76
  };
60
- const pConn = Sonamu.dbConfig.production_master
61
- .connection as Knex.ConnectionConfig & { port?: number };
77
+ const pConn = Sonamu.dbConfig.production_master.connection as Knex.ConnectionConfig & {
78
+ port?: number;
79
+ };
62
80
  if (
63
- `${tConn.host ?? "localhost"}:${tConn.port ?? 3306}/${
64
- tConn.database
65
- }` ===
66
- `${pConn.host ?? "localhost"}:${pConn.port ?? 3306}/${pConn.database}`
81
+ `${tConn.host ?? "localhost"}:${tConn.port ?? 5432}/${tConn.database}` ===
82
+ `${pConn.host ?? "localhost"}:${pConn.port ?? 5432}/${pConn.database}`
67
83
  ) {
68
- throw new Error(
69
- `테스트DB와 프로덕션DB에 동일한 데이터베이스가 사용되었습니다.`
70
- );
84
+ throw new Error(`테스트DB와 프로덕션DB에 동일한 데이터베이스가 사용되었습니다.`);
71
85
  }
72
86
  }
73
87
 
74
88
  this.tdb = knex(Sonamu.dbConfig.test);
75
- this.fdb = knex(Sonamu.dbConfig.fixture_local);
76
- }
77
-
78
- async cleanAndSeed(usingTables?: string[]) {
79
- const tableNames: string[] = await (async () => {
80
- if (usingTables) {
81
- return usingTables;
82
- }
83
- if (this.cachedTableNames) {
84
- return this.cachedTableNames;
85
- }
86
-
87
- const [tables] = await this.tdb.raw(
88
- `SHOW TABLE STATUS WHERE Engine IS NOT NULL AND Name != 'migrations'`
89
- );
90
- const tableNames = tables.map(
91
- (tableInfo: { Name: string }) => tableInfo["Name"]
92
- );
93
- this.cachedTableNames = tableNames;
94
- return tableNames;
95
- })();
96
-
97
- // migrations 제외한 테이블 목록
98
- const tableListStr = tableNames.join(", ");
99
-
100
- // 한 번에 모든 테이블 체크섬 확인
101
- const [fdbChecksumRows] = await this.fdb.raw<
102
- [{ Table: string; Checksum: number }[]]
103
- >(`CHECKSUM TABLE ${tableListStr}`);
104
- const [tdbChecksumRows] = await this.tdb.raw<
105
- [{ Table: string; Checksum: number }[]]
106
- >(`CHECKSUM TABLE ${tableListStr}`);
107
-
108
- // 체크섬 맵 생성
109
- const fdbChecksums = new Map(
110
- fdbChecksumRows.map((row) => [row.Table.split(".").pop()!, row.Checksum])
111
- );
112
- const tdbChecksums = new Map(
113
- tdbChecksumRows.map((row) => [row.Table.split(".").pop()!, row.Checksum])
114
- );
115
-
116
- // 변경된 테이블들만 처리
117
- const changedTables = tableNames.filter(
118
- (tableName) => fdbChecksums.get(tableName) !== tdbChecksums.get(tableName)
119
- );
120
-
121
- // 병렬로 truncate + insert 실행
122
- await this.tdb.transaction(async (trx) => {
123
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
124
-
125
- await Promise.all(
126
- changedTables.map(async (tableName) => {
127
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
128
- await trx(tableName).truncate();
129
- const rawQuery = `INSERT INTO ${
130
- (Sonamu.dbConfig.test.connection as Knex.ConnectionConfig).database
131
- }.${tableName}
132
- SELECT * FROM ${
133
- (Sonamu.dbConfig.fixture_local.connection as Knex.ConnectionConfig)
134
- .database
135
- }.${tableName}`;
136
- await trx.raw(rawQuery);
137
- })
138
- );
139
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
140
- });
141
-
142
- // console.timeEnd("FIXTURE-CleanAndSeed");
89
+ this.fdb = knex(Sonamu.dbConfig.fixture_remote);
143
90
  }
144
91
 
145
92
  async getChecksum(db: Knex, tableName: string) {
@@ -147,68 +94,48 @@ export class FixtureManagerClass {
147
94
  return checksumRow.Checksum;
148
95
  }
149
96
 
97
+ /**
98
+ 이제 FixtureManager.sync() 는 checksum 비교 없이 create database template 으로 수행합니다.
99
+ */
150
100
  async sync() {
151
- const frdb = knex(Sonamu.dbConfig.fixture_remote);
152
-
153
- const [tables] = await this.fdb.raw(
154
- "SHOW TABLE STATUS WHERE Engine IS NOT NULL"
155
- );
156
- const tableNames: string[] = tables.map(
157
- (table: any) => table.Name as string
101
+ const fixtureConn = Sonamu.dbConfig.fixture_remote.connection as Knex.PgConnectionConfig;
102
+ const testConn = Sonamu.dbConfig.test.connection as Knex.PgConnectionConfig;
103
+
104
+ // PostgreSQL 패스워드 환경변수 설정
105
+ const pgEnv = { PGPASSWORD: testConn.password || "" };
106
+
107
+ // 1. 연결 강제 종료
108
+ execSync(
109
+ `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "
110
+ SELECT pg_terminate_backend(pg_stat_activity.pid)
111
+ FROM pg_stat_activity
112
+ WHERE datname = '${testConn.database}'
113
+ AND pid <> pg_backend_pid();
114
+ "`,
115
+ { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
158
116
  );
159
117
 
160
- console.log(chalk.magenta("SYNC..."));
161
- await Promise.all(
162
- tableNames.map(async (tableName) => {
163
- if (tableName.startsWith("knex_migrations")) {
164
- return;
165
- }
166
-
167
- const remoteChecksum = await this.getChecksum(frdb, tableName);
168
- const localChecksum = await this.getChecksum(this.fdb, tableName);
169
-
170
- if (remoteChecksum !== localChecksum) {
171
- await this.fdb.transaction(async (transaction) => {
172
- await transaction.raw(`SET FOREIGN_KEY_CHECKS = 0`);
173
- await transaction(tableName).truncate();
174
-
175
- const rows = await frdb(tableName);
176
- if (rows.length === 0) {
177
- return;
178
- }
118
+ execSync(
119
+ `psql -h ${fixtureConn.host} -p ${fixtureConn.port ?? 5432} -U ${fixtureConn.user} -d postgres -c "
120
+ SELECT pg_terminate_backend(pg_stat_activity.pid)
121
+ FROM pg_stat_activity
122
+ WHERE datname = '${fixtureConn.database}'
123
+ AND pid <> pg_backend_pid();
124
+ "`,
125
+ { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
126
+ );
179
127
 
180
- console.log(chalk.blue(tableName), rows.length);
181
- await transaction
182
- .insert(
183
- rows.map((row: any) => {
184
- return Object.fromEntries(
185
- Object.entries(row).map(([key, value]) => {
186
- if (value === null) {
187
- return [key, null];
188
- } else if (typeof value === "boolean") {
189
- return [key, value ? 1 : 0];
190
- } else if (
191
- typeof value === "object" &&
192
- !(value instanceof Date)
193
- ) {
194
- return [key, JSON.stringify(value)];
195
- } else {
196
- return [key, value];
197
- }
198
- })
199
- );
200
- })
201
- )
202
- .into(tableName);
203
- console.log("OK");
204
- await transaction.raw(`SET FOREIGN_KEY_CHECKS = 1`);
205
- });
206
- }
207
- })
128
+ // 2. DROP DATABASE (별도 실행!)
129
+ execSync(
130
+ `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "DROP DATABASE IF EXISTS \\"${testConn.database}\\""`,
131
+ { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
208
132
  );
209
- console.log(chalk.magenta("DONE!"));
210
133
 
211
- await frdb.destroy();
134
+ // 3. CREATE DATABASE
135
+ execSync(
136
+ `psql -h ${testConn.host} -p ${testConn.port ?? 5432} -U ${testConn.user} -d postgres -c "CREATE DATABASE \\"${testConn.database}\\" TEMPLATE \\"${fixtureConn.database}\\""`,
137
+ { stdio: "inherit", env: { ...process.env, ...pgEnv } as NodeJS.ProcessEnv },
138
+ );
212
139
  }
213
140
 
214
141
  private visitedRecords = new Set<string>();
@@ -216,18 +143,18 @@ export class FixtureManagerClass {
216
143
  // 방문 기록 초기화 (새로운 import 작업 시작)
217
144
  this.visitedRecords.clear();
218
145
 
219
- const queries = _.uniq(
146
+ const queries = unique(
220
147
  (
221
148
  await Promise.all(
222
149
  ids.map(async (id) => {
223
150
  return await this.getImportQueries(entityId, "id", id);
224
- })
151
+ }),
225
152
  )
226
- ).flat()
153
+ ).flat(),
227
154
  );
228
155
 
229
156
  const wdb = BaseModel.getDB("w");
230
- for (let query of queries) {
157
+ for (const query of queries) {
231
158
  const [rsh] = await wdb.raw(query);
232
159
  console.log({
233
160
  query,
@@ -236,11 +163,7 @@ export class FixtureManagerClass {
236
163
  }
237
164
  }
238
165
 
239
- async getImportQueries(
240
- entityId: string,
241
- field: string,
242
- id: number
243
- ): Promise<string[]> {
166
+ async getImportQueries(entityId: string, field: string, id: number): Promise<string[]> {
244
167
  const recordKey = `${entityId}#${field}#${id}`;
245
168
 
246
169
  // 순환 참조 방지: 이미 방문한 레코드는 스킵
@@ -260,9 +183,9 @@ export class FixtureManagerClass {
260
183
  }
261
184
 
262
185
  // 픽스쳐DB, 실DB
263
- const fixtureDatabase = (Sonamu.dbConfig.fixture_remote.connection as any)
186
+ const fixtureDatabase = (Sonamu.dbConfig.fixture_remote.connection as Knex.ConnectionConfig)
264
187
  .database;
265
- const realDatabase = (Sonamu.dbConfig.production_master.connection as any)
188
+ const realDatabase = (Sonamu.dbConfig.production_master.connection as Knex.ConnectionConfig)
266
189
  .database;
267
190
 
268
191
  const selfQuery = `INSERT IGNORE INTO \`${fixtureDatabase}\`.\`${entity.table}\` (SELECT * FROM \`${realDatabase}\`.\`${entity.table}\` WHERE \`id\` = ${id})`;
@@ -271,8 +194,7 @@ export class FixtureManagerClass {
271
194
  .filter(
272
195
  ([, relation]) =>
273
196
  isBelongsToOneRelationProp(relation) ||
274
- (isOneToOneRelationProp(relation) &&
275
- relation.customJoinClause === undefined)
197
+ (isOneToOneRelationProp(relation) && relation.customJoinClause === undefined),
276
198
  )
277
199
  .map(([, relation]) => {
278
200
  /*
@@ -288,15 +210,13 @@ export class FixtureManagerClass {
288
210
  if (isOneToOneRelationProp(relation) && !relation.hasJoinColumn) {
289
211
  const relatedEntity = EntityManager.get(relation.with);
290
212
  const relatedIdColumnName = relatedEntity.props.find(
291
- (p) => isRelationProp(p) && p.with === entity.id
213
+ (p) => isRelationProp(p) && p.with === entity.id,
292
214
  )?.name;
293
215
  if (!relatedIdColumnName) {
294
- throw new Error(
295
- `${relatedEntity.id}의 ${entity.id} 관계 프롭을 찾을 수 없습니다.`
296
- );
216
+ throw new Error(`${relatedEntity.id}의 ${entity.id} 관계 프롭을 찾을 수 없습니다.`);
297
217
  }
298
218
  field = `${relatedIdColumnName}_id`;
299
- id = row["id"];
219
+ id = row.id;
300
220
  } else {
301
221
  field = "id";
302
222
  id = row[`${relation.name}_id`];
@@ -312,10 +232,10 @@ export class FixtureManagerClass {
312
232
  const relQueries = await Promise.all(
313
233
  args.map(async (args) => {
314
234
  return this.getImportQueries(args.entityId, args.field, args.id);
315
- })
235
+ }),
316
236
  );
317
237
 
318
- return [..._.uniq(relQueries.reverse().flat()), selfQuery];
238
+ return [...unique(relQueries.reverse().flat()), selfQuery];
319
239
  }
320
240
 
321
241
  async destroy() {
@@ -333,17 +253,17 @@ export class FixtureManagerClass {
333
253
  async getFixtures(
334
254
  sourceDBName: keyof SonamuDBConfig,
335
255
  targetDBName: keyof SonamuDBConfig,
336
- searchOptions: FixtureSearchOptions
256
+ searchOptions: FixtureSearchOptions,
257
+ duplicateCheck?: DuplicateCheckOptions,
337
258
  ) {
338
259
  const sourceDB = knex(Sonamu.dbConfig[sourceDBName]);
339
260
  const targetDB = knex(Sonamu.dbConfig[targetDBName]);
261
+
340
262
  const { entityId, field, value, searchType } = searchOptions;
341
263
 
342
264
  const entity = EntityManager.get(entityId);
343
265
  const column =
344
- entity.props.find((prop) => prop.name === field)?.type === "relation"
345
- ? `${field}_id`
346
- : field;
266
+ entity.props.find((prop) => prop.name === field)?.type === "relation" ? `${field}_id` : field;
347
267
 
348
268
  let query = sourceDB(entity.table);
349
269
  if (searchType === "equals") {
@@ -360,11 +280,11 @@ export class FixtureManagerClass {
360
280
  const fixtures: FixtureRecord[] = [];
361
281
  for (const row of rows) {
362
282
  const initialRecordsLength = fixtures.length;
363
- const newRecords = await this.createFixtureRecord(entity, row);
283
+ const newRecords = await this.createFixtureRecord(entity, row, {
284
+ _db: sourceDB,
285
+ });
364
286
  fixtures.push(...newRecords);
365
- const currentFixtureRecord = fixtures.find(
366
- (r) => r.fixtureId === `${entityId}#${row.id}`
367
- );
287
+ const currentFixtureRecord = fixtures.find((r) => r.fixtureId === `${entityId}#${row.id}`);
368
288
 
369
289
  if (currentFixtureRecord) {
370
290
  // 현재 fixture로부터 생성된 fetchedRecords 설정
@@ -378,23 +298,26 @@ export class FixtureManagerClass {
378
298
  for await (const fixture of fixtures) {
379
299
  const entity = EntityManager.get(fixture.entityId);
380
300
 
381
- // ID를 이용하여 targetDB에 레코드가 존재하는지 확인
382
- const row = await targetDB(entity.table).where("id", fixture.id).first();
383
- if (row) {
384
- const [record] = await this.createFixtureRecord(entity, row, {
385
- singleRecord: true,
386
- _db: targetDB,
387
- });
388
- fixture.target = record;
389
- continue;
301
+ // 사용자 지정 컬럼 기준 중복 확인 → target
302
+ const customColumns = duplicateCheck?.columns?.[fixture.entityId];
303
+ if (customColumns && customColumns.length > 0) {
304
+ const customDuplicateRow = await this.checkDuplicateByColumns(
305
+ targetDB,
306
+ entity,
307
+ fixture,
308
+ customColumns,
309
+ );
310
+ if (customDuplicateRow) {
311
+ const [record] = await this.createFixtureRecord(entity, customDuplicateRow, {
312
+ singleRecord: true,
313
+ _db: targetDB,
314
+ });
315
+ fixture.target = record;
316
+ }
390
317
  }
391
318
 
392
- // ID를 이용하여 targetDB에서 조회되지 않는 경우, unique 제약을 위반하는지 확인
393
- const uniqueRow = await this.checkUniqueViolation(
394
- targetDB,
395
- entity,
396
- fixture
397
- );
319
+ // Unique index 기준 중복 확인 fixture.unique
320
+ const uniqueRow = await this.checkUniqueViolation(targetDB, entity, fixture);
398
321
  if (uniqueRow) {
399
322
  const [record] = await this.createFixtureRecord(entity, uniqueRow, {
400
323
  singleRecord: true,
@@ -404,21 +327,33 @@ export class FixtureManagerClass {
404
327
  }
405
328
  }
406
329
 
407
- return _.uniqBy(fixtures, (f) => f.fixtureId);
330
+ await targetDB.destroy();
331
+ await sourceDB.destroy();
332
+
333
+ return unique(fixtures, (f) => f.fixtureId);
408
334
  }
409
335
 
410
336
  async createFixtureRecord(
411
337
  entity: Entity,
412
- row: any,
338
+ row: {
339
+ id: number;
340
+ [key: string]: string | number | boolean | null;
341
+ },
413
342
  options?: {
414
343
  singleRecord?: boolean;
415
344
  _db?: Knex;
416
- }
345
+ },
417
346
  ): Promise<FixtureRecord[]> {
418
347
  const records: FixtureRecord[] = [];
419
348
  const visitedEntities = new Set<string>();
420
349
 
421
- const create = async (entity: Entity, row: any) => {
350
+ const create = async (
351
+ entity: Entity,
352
+ row: {
353
+ id: number;
354
+ [key: string]: string | number | boolean | null;
355
+ },
356
+ ) => {
422
357
  const fixtureId = `${entity.id}#${row.id}`;
423
358
  if (visitedEntities.has(fixtureId)) {
424
359
  return;
@@ -451,9 +386,7 @@ export class FixtureManagerClass {
451
386
  const fromColumn = `${inflection.singularize(entity.table)}_id`;
452
387
  const toColumn = `${inflection.singularize(relatedEntity.table)}_id`;
453
388
 
454
- const relatedIds = await db(throughTable)
455
- .where(fromColumn, row.id)
456
- .pluck(toColumn);
389
+ const relatedIds = await db(throughTable).where(fromColumn, row.id).pluck(toColumn);
457
390
  record.columns[prop.name].value = relatedIds;
458
391
  } else if (isHasManyRelationProp(prop)) {
459
392
  const relatedEntity = EntityManager.get(prop.with);
@@ -464,12 +397,10 @@ export class FixtureManagerClass {
464
397
  } else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
465
398
  const relatedEntity = EntityManager.get(prop.with);
466
399
  const relatedProp = relatedEntity.props.find(
467
- (p) => isRelationProp(p) && p.with === entity.id
400
+ (p) => isRelationProp(p) && p.with === entity.id,
468
401
  );
469
402
  if (relatedProp) {
470
- const relatedRow = await db(relatedEntity.table)
471
- .where("id", row.id)
472
- .first();
403
+ const relatedRow = await db(relatedEntity.table).where("id", row.id).first();
473
404
  record.columns[prop.name].value = relatedRow?.id;
474
405
  }
475
406
  } else if (isRelationProp(prop)) {
@@ -480,9 +411,7 @@ export class FixtureManagerClass {
480
411
  }
481
412
  if (!options?.singleRecord && relatedId) {
482
413
  const relatedEntity = EntityManager.get(prop.with);
483
- const relatedRow = await db(relatedEntity.table)
484
- .where("id", relatedId)
485
- .first();
414
+ const relatedRow = await db(relatedEntity.table).where("id", relatedId).first();
486
415
  if (relatedRow) {
487
416
  await create(relatedEntity, relatedRow);
488
417
  }
@@ -498,217 +427,348 @@ export class FixtureManagerClass {
498
427
  return records;
499
428
  }
500
429
 
430
+ /**
431
+ * 1. RelationGraph로 fixture 단위 삽입 순서 계산 (self-reference 포함)
432
+ * 2. 순서대로 UpsertBuilder에 등록 (UBRef로 참조 관계 표현)
433
+ * 3. 테이블별 upsert 실행 (ID는 DB가 자동 할당)
434
+ */
501
435
  async insertFixtures(
502
436
  dbName: keyof SonamuDBConfig,
503
- _fixtures: FixtureRecord[]
504
- ) {
505
- const fixtures = _.uniqBy(_fixtures, (f) => f.fixtureId);
437
+ _fixtures: FixtureRecord[],
438
+ ): Promise<FixtureImportResult[]> {
439
+ const fixtures = unique(_fixtures, (f) => f.fixtureId);
440
+
441
+ // 초기화
442
+ this.builder = new UpsertBuilder();
443
+ this.fixtureRefMap = new Map();
444
+ this.uuidToFixtureId = new Map();
445
+ this.skippedFixtures = new Map();
506
446
 
507
- this.relationGraph.buildGraph(fixtures);
508
- const insertionOrder = this.relationGraph.getInsertionOrder();
509
447
  const db = knex(Sonamu.dbConfig[dbName]);
448
+ const results: FixtureImportResult[] = [];
510
449
 
511
- await db.transaction(async (trx) => {
512
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 0`);
450
+ try {
451
+ // 1. RelationGraph로 fixture 단위 삽입 순서 계산
452
+ this.relationGraph.buildGraph(fixtures);
453
+ const insertionOrder = this.relationGraph.getInsertionOrder();
513
454
 
455
+ // 2. 순서대로 UpsertBuilder에 등록 (override 체크)
514
456
  for (const fixtureId of insertionOrder) {
515
- const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
516
- const result = await this.insertFixture(trx as any, fixture);
517
- if (result.id !== fixture.id) {
518
- // ID가 변경된 경우, 다른 fixture에서 참조하는 경우가 찾아서 수정
457
+ const fixture = fixtures.find((f) => f.fixtureId === fixtureId);
458
+ if (!fixture) continue;
459
+
460
+ const hasTarget = !!fixture.target;
461
+ const hasUnique = !!fixture.unique;
462
+ const hasDuplicate = hasTarget || hasUnique;
463
+
464
+ // 중복이 있고 override=false인 경우: 스킵
465
+ if (hasDuplicate && !fixture.override) {
466
+ // 기존 레코드 ID 저장 (unique 우선, 없으면 target)
467
+ const existingId = fixture.unique?.id ?? fixture.target?.id;
468
+ assert(existingId);
469
+ this.skippedFixtures.set(fixtureId, {
470
+ entityId: fixture.entityId,
471
+ existingId,
472
+ });
473
+
519
474
  console.log(
520
475
  chalk.yellow(
521
- `Unique constraint violation: ${fixture.entityId}#${fixture.id} -> ${fixture.entityId}#${result.id}`
522
- )
476
+ `Skipped ${fixture.entityId}#${fixture.id} (existing: #${existingId}, override: false)`,
477
+ ),
523
478
  );
524
- fixtures.forEach((f) => {
525
- Object.values(f.columns).forEach((column) => {
526
- if (
527
- column.prop.type === "relation" &&
528
- column.prop.with === result.entityId &&
529
- column.value === fixture.id
530
- ) {
531
- column.value = result.id;
532
- }
533
- });
534
- });
535
- fixture.id = result.id;
479
+ continue;
536
480
  }
537
- }
538
481
 
539
- for (const fixtureId of insertionOrder) {
540
- const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
541
- await this.handleManyToManyRelations(trx as any, fixture, fixtures);
482
+ this.registerFixture(fixture);
483
+ console.log(
484
+ chalk.blue(
485
+ `Registered ${fixture.entityId}#${fixture.id}${fixture.override ? ` (override existing: #${fixture.target?.id})` : ""}`,
486
+ ),
487
+ );
542
488
  }
543
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
544
- });
545
489
 
546
- const records: FixtureImportResult[] = [];
490
+ // 3. 테이블별 upsert 실행
491
+ const tableOrder = this.getTableOrder(fixtures);
492
+
493
+ await db.transaction(async (trx) => {
494
+ const insertedIdsByTable = new Map<string, Map<string, number>>();
495
+
496
+ for (const tableName of tableOrder) {
497
+ if (!this.builder.hasTable(tableName)) continue;
498
+
499
+ // upsert 실행 전 uuid 목록 저장
500
+ const table = this.builder.getTable(tableName);
501
+ const uuids = table.rows.map((row) => row.uuid as string);
502
+
503
+ console.log(chalk.blue(`Upserting ${tableName} with ${uuids.length} rows`));
504
+ await this.builder.upsert(trx, tableName);
505
+
506
+ // upsert된 row들의 uuid -> id 매핑 구축
507
+ if (uuids.length > 0) {
508
+ const uuidToId = new Map<string, number>();
509
+ const rows = await trx(tableName).select("uuid", "id").whereIn("uuid", uuids);
510
+
511
+ for (const row of rows) {
512
+ uuidToId.set(row.uuid, row.id);
513
+ }
514
+
515
+ insertedIdsByTable.set(tableName, uuidToId);
516
+ }
517
+ }
518
+
519
+ // 4. ManyToMany 관계 처리
520
+ await this.processManyToManyRelations(trx, fixtures, insertedIdsByTable);
521
+
522
+ // 5. 결과 수집
523
+ for (const fixture of fixtures) {
524
+ const entity = EntityManager.get(fixture.entityId);
547
525
 
548
- for await (const r of fixtures) {
549
- const entity = EntityManager.get(r.entityId);
550
- const record = await db(entity.table).where("id", r.id).first();
551
- records.push({
552
- entityId: r.entityId,
553
- data: record,
526
+ // 스킵된 fixture는 기존 레코드 정보로 결과 추가
527
+ const skipped = this.skippedFixtures.get(fixture.fixtureId);
528
+ if (skipped) {
529
+ results.push({
530
+ entityId: fixture.entityId,
531
+ data: await trx(entity.table).where("id", skipped.existingId).first(),
532
+ });
533
+ continue;
534
+ }
535
+
536
+ const ref = this.fixtureRefMap.get(fixture.fixtureId);
537
+ if (ref) {
538
+ const uuidToId = insertedIdsByTable.get(entity.table);
539
+ const insertedId = uuidToId?.get(ref.uuid);
540
+
541
+ if (insertedId !== undefined) {
542
+ results.push({
543
+ entityId: fixture.entityId,
544
+ data: await trx(entity.table).where("id", insertedId).first(),
545
+ });
546
+
547
+ console.log(
548
+ chalk.green(`Inserted into ${entity.table}: #${fixture.id} -> #${insertedId}`),
549
+ );
550
+ }
551
+ }
552
+ }
554
553
  });
554
+ } finally {
555
+ await db.destroy();
555
556
  }
556
557
 
557
- return _.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
558
+ return unique(results, (r) => `${r.entityId}#${r.data.id}`);
558
559
  }
559
560
 
560
- private prepareInsertData(fixture: FixtureRecord) {
561
- const insertData: any = {};
561
+ /**
562
+ * FixtureRecord를 UpsertBuilder에 등록
563
+ */
564
+ private registerFixture(fixture: FixtureRecord): UBRef {
565
+ const entity = EntityManager.get(fixture.entityId);
566
+ const row: Record<string, unknown> = {};
567
+
568
+ // Override 모드 판단: target 또는 unique가 있고 override=true인 경우
569
+ const existingRecord = fixture.target ?? fixture.unique;
570
+ const isOverrideMode = fixture.override && existingRecord;
571
+
562
572
  for (const [propName, column] of Object.entries(fixture.columns)) {
563
- if (isVirtualProp(column.prop)) {
573
+ const prop = column.prop;
574
+
575
+ if (isVirtualProp(prop)) {
564
576
  continue;
565
577
  }
566
578
 
567
- const prop = column.prop as EntityProp;
568
- if (!isRelationProp(prop)) {
569
- if (prop.type === "json") {
570
- insertData[propName] = JSON.stringify(column.value);
571
- } else if (prop.type === "timestamp" || prop.type === "datetime") {
572
- insertData[propName] = new Date(column.value);
573
- } else {
574
- insertData[propName] = column.value;
579
+ // id/uuid 처리: Override 모드일 때만 기존 값 사용
580
+ if (propName === "id" || propName === "uuid") {
581
+ if (isOverrideMode && existingRecord) {
582
+ // Override: 기존 레코드의 값 사용 → UPDATE
583
+ row[propName] = existingRecord.columns[propName]?.value;
575
584
  }
576
- } else if (
577
- isBelongsToOneRelationProp(prop) ||
578
- (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
579
- ) {
580
- insertData[`${propName}_id`] = column.value;
585
+ // 레코드: 제외 → INSERT (DB/UpsertBuilder가 생성)
586
+ continue;
587
+ }
588
+
589
+ if (isRelationProp(prop)) {
590
+ if (
591
+ isBelongsToOneRelationProp(prop) ||
592
+ (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
593
+ ) {
594
+ const relatedId = column.value as number | null;
595
+ if (relatedId !== null && relatedId !== undefined) {
596
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
597
+
598
+ // 먼저 skip된 fixture인지 확인
599
+ const skippedExistingId = this.skippedFixtures.get(relatedFixtureId)?.existingId;
600
+ if (skippedExistingId !== undefined) {
601
+ // skip된 fixture → target DB의 기존 레코드 id 사용
602
+ row[`${propName}_id`] = skippedExistingId;
603
+ } else {
604
+ const relatedRef = this.fixtureRefMap.get(relatedFixtureId);
605
+ if (relatedRef) {
606
+ // 이미 등록된 fixture 참조 → UBRef 사용
607
+ row[`${propName}_id`] = relatedRef;
608
+ } else {
609
+ // fixtures에 포함되지 않은 레코드 → ID 그대로 사용
610
+ row[`${propName}_id`] = relatedId;
611
+ }
612
+ }
613
+ } else {
614
+ row[`${propName}_id`] = null;
615
+ }
616
+ }
617
+ // HasMany, ManyToMany는 별도 처리
618
+ } else {
619
+ // 일반 컬럼
620
+ row[propName] = this.convertColumnValue(prop as EntityProp, column.value);
581
621
  }
582
622
  }
583
- return insertData;
623
+
624
+ console.log(chalk.blue(`Registering ${entity.table} - ${inspect(row, false, null, true)}`));
625
+ const ref = this.builder.register(entity.table, row);
626
+ this.fixtureRefMap.set(fixture.fixtureId, ref);
627
+ this.uuidToFixtureId.set(ref.uuid, fixture.fixtureId);
628
+
629
+ return ref;
584
630
  }
585
631
 
586
- private async insertFixture(db: Knex, fixture: FixtureRecord) {
587
- const insertData = this.prepareInsertData(fixture);
588
- const entity = EntityManager.get(fixture.entityId);
632
+ /**
633
+ * 컬럼 변환
634
+ */
635
+ private convertColumnValue(prop: EntityProp, value: unknown): unknown {
636
+ if (value === null || value === undefined) {
637
+ return null;
638
+ }
589
639
 
590
- try {
591
- const uniqueFound = await this.checkUniqueViolation(db, entity, fixture);
592
- if (uniqueFound) {
593
- return {
594
- entityId: fixture.entityId,
595
- id: uniqueFound.id,
596
- };
597
- }
640
+ switch (prop.type) {
641
+ case "json":
642
+ // UpsertBuilder.register에서 JSON.stringify 처리하므로 object 그대로 전달
643
+ return value;
598
644
 
599
- const found = await db(entity.table).where("id", fixture.id).first();
600
- if (found && !fixture.override) {
601
- return {
602
- entityId: fixture.entityId,
603
- id: found.id,
604
- };
605
- }
645
+ case "date":
646
+ if (typeof value === "string" || typeof value === "number") {
647
+ return new Date(value);
648
+ }
649
+ return value;
606
650
 
607
- const q = db.insert(insertData).into(entity.table);
608
- await q.onDuplicateUpdate.apply(q, Object.keys(insertData));
609
- return {
610
- entityId: fixture.entityId,
611
- id: fixture.id,
612
- };
613
- } catch (err) {
614
- console.log(err);
615
- throw err;
651
+ default:
652
+ return value;
616
653
  }
617
654
  }
618
655
 
619
- private async handleManyToManyRelations(
620
- db: Knex,
621
- fixture: FixtureRecord,
622
- fixtures: FixtureRecord[]
623
- ) {
624
- for (const [, column] of Object.entries(fixture.columns)) {
625
- const prop = column.prop as EntityProp;
626
- if (isManyToManyRelationProp(prop)) {
627
- const joinTable = (prop as ManyToManyRelationProp).joinTable;
628
- const relatedIds = column.value as number[];
629
-
630
- for (const relatedId of relatedIds) {
631
- if (
632
- !fixtures.find((f) => f.fixtureId === `${prop.with}#${relatedId}`)
633
- ) {
634
- continue;
635
- }
636
-
637
- const entity = EntityManager.get(fixture.entityId);
638
- const relatedEntity = EntityManager.get(prop.with);
639
- if (!entity || !relatedEntity) {
640
- throw new Error(
641
- `Entity not found: ${fixture.entityId}, ${prop.with}`
642
- );
643
- }
656
+ /**
657
+ * 테이블 순서 추출 (fixtures에 포함된 테이블만)
658
+ */
659
+ private getTableOrder(fixtures: FixtureRecord[]): string[] {
660
+ const tables: string[] = [];
661
+ const seen = new Set<string>();
644
662
 
645
- const [found] = await db(joinTable)
646
- .where({
647
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
648
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
649
- })
650
- .limit(1);
651
- if (found) {
652
- continue;
653
- }
654
-
655
- const newIds = await db(joinTable).insert({
656
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
657
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
658
- });
659
- console.log(
660
- chalk.green(
661
- `Inserted into ${joinTable}: ${entity.table}(${fixture.id}) - ${relatedEntity.table}(${relatedId}) ID: ${newIds}`
662
- )
663
- );
664
- }
663
+ for (const fixture of fixtures) {
664
+ const entity = EntityManager.get(fixture.entityId);
665
+ if (!seen.has(entity.table)) {
666
+ seen.add(entity.table);
667
+ tables.push(entity.table);
665
668
  }
666
669
  }
670
+
671
+ return tables;
667
672
  }
668
673
 
669
- async addFixtureLoader(code: string) {
670
- const path = Sonamu.apiRootPath + "/src/testing/fixture.ts";
671
- let content = readFileSync(path).toString();
674
+ private async processManyToManyRelations(
675
+ trx: Knex.Transaction,
676
+ fixtures: FixtureRecord[],
677
+ insertedIdsByTable: Map<string, Map<string, number>>,
678
+ ): Promise<void> {
679
+ for (const fixture of fixtures) {
680
+ const entity = EntityManager.get(fixture.entityId);
681
+ const sourceRef = this.fixtureRefMap.get(fixture.fixtureId);
672
682
 
673
- const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
674
- const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
683
+ if (!sourceRef) continue;
675
684
 
676
- if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
677
- const newContent =
678
- content.slice(0, fixtureLoaderEnd) +
679
- " " +
680
- code +
681
- "\n" +
682
- content.slice(fixtureLoaderEnd);
685
+ const sourceUuidToId = insertedIdsByTable.get(entity.table);
686
+ const sourceId = sourceUuidToId?.get(sourceRef.uuid);
683
687
 
684
- writeFileSync(path, newContent);
685
- } else {
686
- throw new Error("Failed to find fixtureLoader in fixture.ts");
688
+ if (sourceId === undefined) continue;
689
+
690
+ for (const [, column] of Object.entries(fixture.columns)) {
691
+ const prop = column.prop;
692
+
693
+ if (isManyToManyRelationProp(prop) && Array.isArray(column.value)) {
694
+ // 선택되지 않은 ManyToMany 관계는 저장하지 않음
695
+ const targetTable = EntityManager.get(prop.with);
696
+ if (this.builder.hasTable(targetTable.table) === false) continue;
697
+
698
+ const relatedIds = column.value as number[];
699
+ if (relatedIds.length === 0) continue;
700
+
701
+ const joinTable = (prop as ManyToManyRelationProp).joinTable;
702
+ const relatedEntity = EntityManager.get(prop.with);
703
+
704
+ const sourceColumn = `${inflection.singularize(entity.table)}_id`;
705
+ const targetColumn = `${inflection.singularize(relatedEntity.table)}_id`;
706
+
707
+ for (const relatedId of relatedIds) {
708
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
709
+ const relatedRef = this.fixtureRefMap.get(relatedFixtureId);
710
+
711
+ let targetId: number;
712
+
713
+ if (relatedRef) {
714
+ const relatedUuidToId = insertedIdsByTable.get(relatedEntity.table);
715
+ const resolvedId = relatedUuidToId?.get(relatedRef.uuid);
716
+
717
+ if (resolvedId === undefined) {
718
+ console.warn(
719
+ `Related fixture ${relatedFixtureId} not found in insertedIds, skipping`,
720
+ );
721
+ continue;
722
+ }
723
+ targetId = resolvedId;
724
+ } else {
725
+ targetId = relatedId;
726
+ }
727
+
728
+ // JoinTable에 삽입
729
+ const [found] = await trx(joinTable)
730
+ .where({
731
+ [sourceColumn]: sourceId,
732
+ [targetColumn]: targetId,
733
+ })
734
+ .limit(1);
735
+
736
+ if (!found) {
737
+ await trx(joinTable).insert({
738
+ [sourceColumn]: sourceId,
739
+ [targetColumn]: targetId,
740
+ });
741
+
742
+ console.log(
743
+ chalk.green(
744
+ `Inserted into ${joinTable}: ${entity.table}(${sourceId}) - ${relatedEntity.table}(${targetId})`,
745
+ ),
746
+ );
747
+ }
748
+ }
749
+ }
750
+ }
687
751
  }
688
752
  }
689
753
 
690
- // 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
691
- private async checkUniqueViolation(
692
- db: Knex,
693
- entity: Entity,
694
- fixture: FixtureRecord
695
- ) {
696
- const _uniqueIndexes = entity.indexes.filter((i) => i.type === "unique");
754
+ private async checkUniqueViolation(db: Knex, entity: Entity, fixture: FixtureRecord) {
755
+ const _uniqueIndexes = entity.indexes?.filter((i) => i.type === "unique") ?? [];
697
756
 
698
- // ManyToMany 관계 테이블의 유니크 제약은 제외
699
757
  const uniqueIndexes = _uniqueIndexes.filter((index) =>
700
- index.columns.every((column) => !column.startsWith(`${entity.table}__`))
758
+ index.columns.every((column) => !column.startsWith(`${entity.table}__`)),
701
759
  );
702
760
  if (uniqueIndexes.length === 0) {
703
761
  return null;
704
762
  }
705
763
 
706
764
  let uniqueQuery = db(entity.table);
765
+ let hasCondition = false;
766
+
707
767
  for (const index of uniqueIndexes) {
708
768
  // 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
709
769
  const containsNull = index.columns.some((column) => {
710
- const field = column.split("_id")[0];
711
- return fixture.columns[field].value === null;
770
+ const field = column.replace(/_id$/, "");
771
+ return fixture.columns[field]?.value === null;
712
772
  });
713
773
  if (containsNull) {
714
774
  continue;
@@ -716,18 +776,71 @@ export class FixtureManagerClass {
716
776
 
717
777
  uniqueQuery = uniqueQuery.orWhere((qb) => {
718
778
  for (const column of index.columns) {
719
- const field = column.split("_id")[0];
779
+ const field = column.replace(/_id$/, "");
720
780
 
721
- if (Array.isArray(fixture.columns[field].value)) {
781
+ if (Array.isArray(fixture.columns[field]?.value)) {
722
782
  qb.whereIn(column, fixture.columns[field].value);
723
783
  } else {
724
- qb.andWhere(column, fixture.columns[field].value);
784
+ qb.andWhere(column, fixture.columns[field]?.value);
725
785
  }
726
786
  }
727
787
  });
788
+ hasCondition = true;
789
+ }
790
+
791
+ if (!hasCondition) {
792
+ return null;
728
793
  }
794
+
729
795
  const [uniqueFound] = await uniqueQuery;
730
796
  return uniqueFound;
731
797
  }
798
+
799
+ private async checkDuplicateByColumns(
800
+ db: Knex,
801
+ entity: Entity,
802
+ fixture: FixtureRecord,
803
+ columns: string[],
804
+ ) {
805
+ if (columns.length === 0) {
806
+ return null;
807
+ }
808
+
809
+ const whereClause: Record<string, unknown> = {};
810
+
811
+ for (const column of columns) {
812
+ // relation 필드인 경우 _id 붙이기
813
+ const prop = entity.props.find((p) => p.name === column);
814
+ const dbColumn = prop && isRelationProp(prop) ? `${column}_id` : column;
815
+ const value = fixture.columns[column]?.value;
816
+
817
+ // null 값이 포함된 경우 중복 확인 스킵
818
+ if (value === null || value === undefined) {
819
+ return null;
820
+ }
821
+
822
+ whereClause[dbColumn] = value;
823
+ }
824
+
825
+ const [found] = await db(entity.table).where(whereClause).limit(1);
826
+ return found;
827
+ }
828
+
829
+ async addFixtureLoader(code: string) {
830
+ const path = `${Sonamu.apiRootPath}/src/testing/fixture.ts`;
831
+ const content = readFileSync(path).toString();
832
+
833
+ const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
834
+ const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
835
+
836
+ if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
837
+ const newContent = `${content.slice(0, fixtureLoaderEnd)} ${code}\n${content.slice(fixtureLoaderEnd)}`;
838
+
839
+ writeFileSync(path, newContent);
840
+ } else {
841
+ throw new Error("Failed to find fixtureLoader in fixture.ts");
842
+ }
843
+ }
732
844
  }
845
+
733
846
  export const FixtureManager = new FixtureManagerClass();