sonamu 0.6.0 → 0.7.1

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 (406) 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 +2 -1
  39. package/dist/api/caster.d.ts.map +1 -1
  40. package/dist/api/caster.js +6 -1
  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 +178 -409
  44. package/dist/api/config.d.ts +27 -13
  45. package/dist/api/config.d.ts.map +1 -1
  46. package/dist/api/config.js +19 -26
  47. package/dist/api/context.d.ts +4 -3
  48. package/dist/api/context.d.ts.map +1 -1
  49. package/dist/api/context.js +1 -1
  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 +111 -18
  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 +3 -3
  56. package/dist/api/sonamu.d.ts +7 -7
  57. package/dist/api/sonamu.d.ts.map +1 -1
  58. package/dist/api/sonamu.js +83 -51
  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 +5 -1
  63. package/dist/bin/build-config.d.ts.map +1 -1
  64. package/dist/bin/build-config.js +5 -2
  65. package/dist/bin/cli.js +165 -64
  66. package/dist/bin/loader-register.d.ts +2 -0
  67. package/dist/bin/loader-register.d.ts.map +1 -0
  68. package/dist/bin/loader-register.js +34 -0
  69. package/dist/database/_batch_update.d.ts +5 -3
  70. package/dist/database/_batch_update.d.ts.map +1 -1
  71. package/dist/database/_batch_update.js +30 -13
  72. package/dist/database/base-model.d.ts +96 -10
  73. package/dist/database/base-model.d.ts.map +1 -1
  74. package/dist/database/base-model.js +232 -89
  75. package/dist/database/base-model.types.d.ts +93 -0
  76. package/dist/database/base-model.types.d.ts.map +1 -0
  77. package/dist/database/base-model.types.js +10 -0
  78. package/dist/database/code-generator.d.ts +1 -1
  79. package/dist/database/code-generator.d.ts.map +1 -1
  80. package/dist/database/code-generator.js +11 -10
  81. package/dist/database/db.d.ts +5 -6
  82. package/dist/database/db.d.ts.map +1 -1
  83. package/dist/database/db.js +22 -25
  84. package/dist/database/puri-subset.test-d.js +81 -0
  85. package/dist/database/puri-subset.types.d.ts +123 -0
  86. package/dist/database/puri-subset.types.d.ts.map +1 -0
  87. package/dist/database/puri-subset.types.js +16 -0
  88. package/dist/database/puri-wrapper.d.ts +13 -11
  89. package/dist/database/puri-wrapper.d.ts.map +1 -1
  90. package/dist/database/puri-wrapper.js +2 -2
  91. package/dist/database/puri.d.ts +25 -14
  92. package/dist/database/puri.d.ts.map +1 -1
  93. package/dist/database/puri.js +83 -21
  94. package/dist/database/puri.types.d.ts +21 -7
  95. package/dist/database/puri.types.d.ts.map +1 -1
  96. package/dist/database/puri.types.js +4 -1
  97. package/dist/database/transaction-context.d.ts +1 -1
  98. package/dist/database/transaction-context.d.ts.map +1 -1
  99. package/dist/database/transaction-context.js +1 -1
  100. package/dist/database/upsert-builder.d.ts +9 -3
  101. package/dist/database/upsert-builder.d.ts.map +1 -1
  102. package/dist/database/upsert-builder.js +227 -78
  103. package/dist/entity/entity-manager.d.ts +165 -2
  104. package/dist/entity/entity-manager.d.ts.map +1 -1
  105. package/dist/entity/entity-manager.js +26 -10
  106. package/dist/entity/entity.d.ts +5 -3
  107. package/dist/entity/entity.d.ts.map +1 -1
  108. package/dist/entity/entity.js +153 -54
  109. package/dist/exceptions/error-handler.d.ts +1 -1
  110. package/dist/exceptions/error-handler.d.ts.map +1 -1
  111. package/dist/exceptions/error-handler.js +1 -1
  112. package/dist/exceptions/so-exceptions.d.ts +1 -1
  113. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  114. package/dist/exceptions/so-exceptions.js +1 -1
  115. package/dist/file-storage/driver.d.ts +1 -1
  116. package/dist/file-storage/driver.d.ts.map +1 -1
  117. package/dist/file-storage/driver.js +1 -1
  118. package/dist/file-storage/file-storage.js +2 -2
  119. package/dist/index.d.ts +18 -11
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +19 -13
  122. package/dist/migration/code-generation.d.ts +1 -1
  123. package/dist/migration/code-generation.d.ts.map +1 -1
  124. package/dist/migration/code-generation.js +123 -67
  125. package/dist/migration/migration-set.d.ts +2 -10
  126. package/dist/migration/migration-set.d.ts.map +1 -1
  127. package/dist/migration/migration-set.js +67 -218
  128. package/dist/migration/migrator.d.ts +24 -73
  129. package/dist/migration/migrator.d.ts.map +1 -1
  130. package/dist/migration/migrator.js +121 -301
  131. package/dist/migration/postgresql-schema-reader.d.ts +51 -0
  132. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -0
  133. package/dist/migration/postgresql-schema-reader.js +245 -0
  134. package/dist/migration/types.d.ts +6 -38
  135. package/dist/migration/types.d.ts.map +1 -1
  136. package/dist/migration/types.js +1 -1
  137. package/dist/naite/messaging-types.d.ts +43 -0
  138. package/dist/naite/messaging-types.d.ts.map +1 -0
  139. package/dist/naite/messaging-types.js +7 -0
  140. package/dist/naite/naite-reporter.d.ts +41 -0
  141. package/dist/naite/naite-reporter.d.ts.map +1 -0
  142. package/dist/naite/naite-reporter.js +102 -0
  143. package/dist/naite/naite.d.ts +91 -8
  144. package/dist/naite/naite.d.ts.map +1 -1
  145. package/dist/naite/naite.js +285 -41
  146. package/dist/stream/sse.d.ts +2 -2
  147. package/dist/stream/sse.d.ts.map +1 -1
  148. package/dist/stream/sse.js +1 -1
  149. package/dist/syncer/api-parser.d.ts +3 -13
  150. package/dist/syncer/api-parser.d.ts.map +1 -1
  151. package/dist/syncer/api-parser.js +67 -56
  152. package/dist/syncer/checksum.d.ts +2 -2
  153. package/dist/syncer/checksum.d.ts.map +1 -1
  154. package/dist/syncer/checksum.js +11 -11
  155. package/dist/syncer/code-generator.d.ts +3 -3
  156. package/dist/syncer/code-generator.d.ts.map +1 -1
  157. package/dist/syncer/code-generator.js +37 -17
  158. package/dist/syncer/entity-operations.d.ts +2 -2
  159. package/dist/syncer/entity-operations.d.ts.map +1 -1
  160. package/dist/syncer/entity-operations.js +9 -8
  161. package/dist/syncer/file-patterns.d.ts +1 -1
  162. package/dist/syncer/file-patterns.d.ts.map +1 -1
  163. package/dist/syncer/file-patterns.js +1 -1
  164. package/dist/syncer/index.d.ts +4 -4
  165. package/dist/syncer/index.d.ts.map +1 -1
  166. package/dist/syncer/index.js +5 -5
  167. package/dist/syncer/module-loader.d.ts +4 -4
  168. package/dist/syncer/module-loader.d.ts.map +1 -1
  169. package/dist/syncer/module-loader.js +17 -12
  170. package/dist/syncer/syncer.d.ts +31 -24
  171. package/dist/syncer/syncer.d.ts.map +1 -1
  172. package/dist/syncer/syncer.js +92 -45
  173. package/dist/template/entity-converter.d.ts +1 -1
  174. package/dist/template/entity-converter.d.ts.map +1 -1
  175. package/dist/template/entity-converter.js +15 -8
  176. package/dist/template/helpers.d.ts +2 -2
  177. package/dist/template/helpers.d.ts.map +1 -1
  178. package/dist/template/helpers.js +3 -3
  179. package/dist/template/implementations/entity.template.d.ts +2 -2
  180. package/dist/template/implementations/entity.template.d.ts.map +1 -1
  181. package/dist/template/implementations/entity.template.js +4 -5
  182. package/dist/template/implementations/generated.template.d.ts +2 -3
  183. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  184. package/dist/template/implementations/generated.template.js +46 -29
  185. package/dist/template/implementations/generated_http.template.d.ts +2 -3
  186. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  187. package/dist/template/implementations/generated_http.template.js +9 -9
  188. package/dist/template/implementations/generated_sso.template.d.ts +3 -4
  189. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  190. package/dist/template/implementations/generated_sso.template.js +54 -25
  191. package/dist/template/implementations/init_types.template.d.ts +2 -2
  192. package/dist/template/implementations/init_types.template.d.ts.map +1 -1
  193. package/dist/template/implementations/init_types.template.js +2 -2
  194. package/dist/template/implementations/model.template.d.ts +2 -2
  195. package/dist/template/implementations/model.template.d.ts.map +1 -1
  196. package/dist/template/implementations/model.template.js +47 -37
  197. package/dist/template/implementations/model_test.template.d.ts +2 -2
  198. package/dist/template/implementations/model_test.template.d.ts.map +1 -1
  199. package/dist/template/implementations/model_test.template.js +2 -2
  200. package/dist/template/implementations/service.template.d.ts +4 -4
  201. package/dist/template/implementations/service.template.d.ts.map +1 -1
  202. package/dist/template/implementations/service.template.js +24 -16
  203. package/dist/template/implementations/view_enums_buttonset.template.d.ts +2 -2
  204. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +1 -1
  205. package/dist/template/implementations/view_enums_buttonset.template.js +1 -1
  206. package/dist/template/implementations/view_enums_dropdown.template.d.ts +2 -2
  207. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +1 -1
  208. package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
  209. package/dist/template/implementations/view_enums_select.template.d.ts +2 -2
  210. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
  211. package/dist/template/implementations/view_enums_select.template.js +2 -2
  212. package/dist/template/implementations/view_form.template.d.ts +2 -2
  213. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  214. package/dist/template/implementations/view_form.template.js +4 -4
  215. package/dist/template/implementations/view_id_all_select.template.d.ts +2 -2
  216. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -1
  217. package/dist/template/implementations/view_id_all_select.template.js +1 -1
  218. package/dist/template/implementations/view_id_async_select.template.d.ts +2 -2
  219. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
  220. package/dist/template/implementations/view_id_async_select.template.js +1 -1
  221. package/dist/template/implementations/view_list.template.d.ts +2 -2
  222. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  223. package/dist/template/implementations/view_list.template.js +29 -19
  224. package/dist/template/implementations/view_list_columns.template.d.ts +3 -3
  225. package/dist/template/implementations/view_list_columns.template.d.ts.map +1 -1
  226. package/dist/template/implementations/view_list_columns.template.js +1 -1
  227. package/dist/template/implementations/view_search_input.template.d.ts +2 -2
  228. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  229. package/dist/template/implementations/view_search_input.template.js +1 -1
  230. package/dist/template/index.d.ts +4 -2
  231. package/dist/template/index.d.ts.map +1 -1
  232. package/dist/template/index.js +5 -3
  233. package/dist/template/template-manager.d.ts +56 -0
  234. package/dist/template/template-manager.d.ts.map +1 -0
  235. package/dist/template/template-manager.js +125 -0
  236. package/dist/template/template-types.d.ts +16 -0
  237. package/dist/template/template-types.d.ts.map +1 -0
  238. package/dist/template/template-types.js +7 -0
  239. package/dist/template/template.d.ts +12 -2
  240. package/dist/template/template.d.ts.map +1 -1
  241. package/dist/template/template.js +19 -6
  242. package/dist/template/zod-converter.d.ts +40 -7
  243. package/dist/template/zod-converter.d.ts.map +1 -1
  244. package/dist/template/zod-converter.js +386 -58
  245. package/dist/testing/_relation-graph.d.ts +1 -1
  246. package/dist/testing/_relation-graph.d.ts.map +1 -1
  247. package/dist/testing/_relation-graph.js +12 -3
  248. package/dist/testing/fixture-manager.d.ts +42 -11
  249. package/dist/testing/fixture-manager.d.ts.map +1 -1
  250. package/dist/testing/fixture-manager.js +338 -236
  251. package/dist/types/types.d.ts +709 -104
  252. package/dist/types/types.d.ts.map +1 -1
  253. package/dist/types/types.js +309 -52
  254. package/dist/typings/knex.d.js +2 -2
  255. package/dist/utils/async-utils.d.ts.map +1 -1
  256. package/dist/utils/async-utils.js +3 -3
  257. package/dist/utils/console-util.js +1 -1
  258. package/dist/utils/controller.d.ts +1 -0
  259. package/dist/utils/controller.d.ts.map +1 -1
  260. package/dist/utils/controller.js +4 -1
  261. package/dist/utils/esm-utils.d.ts +0 -6
  262. package/dist/utils/esm-utils.d.ts.map +1 -1
  263. package/dist/utils/esm-utils.js +2 -9
  264. package/dist/utils/formatter.d.ts +3 -0
  265. package/dist/utils/formatter.d.ts.map +1 -0
  266. package/dist/utils/formatter.js +110 -0
  267. package/dist/utils/fs-utils.d.ts +1 -1
  268. package/dist/utils/fs-utils.d.ts.map +1 -1
  269. package/dist/utils/fs-utils.js +1 -1
  270. package/dist/utils/lodash-able.d.ts.map +1 -1
  271. package/dist/utils/lodash-able.js +1 -1
  272. package/dist/utils/object-utils.d.ts +44 -0
  273. package/dist/utils/object-utils.d.ts.map +1 -0
  274. package/dist/utils/object-utils.js +191 -0
  275. package/dist/utils/path-utils.d.ts +1 -1
  276. package/dist/utils/path-utils.d.ts.map +1 -1
  277. package/dist/utils/path-utils.js +3 -3
  278. package/dist/utils/process-utils.js +1 -1
  279. package/dist/utils/sql-parser.d.ts +5 -1
  280. package/dist/utils/sql-parser.d.ts.map +1 -1
  281. package/dist/utils/sql-parser.js +14 -3
  282. package/dist/utils/type-utils.d.ts +23 -0
  283. package/dist/utils/type-utils.d.ts.map +1 -0
  284. package/dist/utils/type-utils.js +45 -0
  285. package/dist/utils/utils.d.ts +7 -1
  286. package/dist/utils/utils.d.ts.map +1 -1
  287. package/dist/utils/utils.js +44 -5
  288. package/dist/utils/zod-error.d.ts +1 -1
  289. package/dist/utils/zod-error.d.ts.map +1 -1
  290. package/dist/utils/zod-error.js +1 -1
  291. package/package.json +55 -30
  292. package/src/ai/agents/agent.ts +87 -0
  293. package/src/ai/agents/index.ts +2 -0
  294. package/src/ai/agents/types.ts +47 -0
  295. package/src/ai/index.ts +1 -0
  296. package/src/ai/providers/rtzr/api.ts +37 -0
  297. package/src/ai/providers/rtzr/error.ts +34 -0
  298. package/src/ai/providers/rtzr/index.ts +4 -0
  299. package/src/ai/providers/rtzr/model.ts +201 -0
  300. package/src/ai/providers/rtzr/options.ts +49 -0
  301. package/src/ai/providers/rtzr/provider.ts +91 -0
  302. package/src/ai/providers/rtzr/utils.ts +127 -0
  303. package/src/api/base-frame.ts +4 -2
  304. package/src/api/caster.ts +17 -23
  305. package/src/api/code-converters.ts +176 -533
  306. package/src/api/config.ts +39 -56
  307. package/src/api/context.ts +7 -18
  308. package/src/api/decorators.ts +175 -46
  309. package/src/api/index.ts +2 -2
  310. package/src/api/sonamu.ts +133 -124
  311. package/src/api/validator.ts +83 -0
  312. package/src/bin/build-config.ts +7 -1
  313. package/src/bin/cli.ts +192 -110
  314. package/src/bin/loader-register.ts +38 -0
  315. package/src/database/_batch_update.ts +46 -31
  316. package/src/database/base-model.ts +390 -182
  317. package/src/database/base-model.types.ts +155 -0
  318. package/src/database/code-generator.ts +13 -32
  319. package/src/database/db.ts +36 -50
  320. package/src/database/puri-subset.test-d.ts +471 -0
  321. package/src/database/puri-subset.types.ts +195 -0
  322. package/src/database/puri-wrapper.ts +58 -67
  323. package/src/database/puri.ts +182 -126
  324. package/src/database/puri.types.ts +64 -31
  325. package/src/database/transaction-context.ts +1 -1
  326. package/src/database/upsert-builder.ts +261 -132
  327. package/src/entity/entity-manager.ts +36 -28
  328. package/src/entity/entity.ts +330 -249
  329. package/src/exceptions/error-handler.ts +3 -3
  330. package/src/exceptions/so-exceptions.ts +11 -11
  331. package/src/file-storage/driver.ts +5 -5
  332. package/src/file-storage/file-storage.ts +2 -2
  333. package/src/index.ts +18 -12
  334. package/src/migration/code-generation.ts +185 -172
  335. package/src/migration/migration-set.ts +80 -293
  336. package/src/migration/migrator.ts +182 -425
  337. package/src/migration/mysql-schema-reader.ts.txt +272 -0
  338. package/src/migration/postgresql-schema-reader.ts +310 -0
  339. package/src/migration/types.ts +6 -39
  340. package/src/naite/messaging-types.ts +51 -0
  341. package/src/naite/naite-reporter.ts +128 -0
  342. package/src/naite/naite.ts +378 -33
  343. package/src/shared/web.shared.ts.txt +20 -24
  344. package/src/stream/sse.ts +5 -5
  345. package/src/syncer/api-parser.ts +52 -69
  346. package/src/syncer/checksum.ts +25 -37
  347. package/src/syncer/code-generator.ts +58 -62
  348. package/src/syncer/entity-operations.ts +12 -15
  349. package/src/syncer/file-patterns.ts +2 -2
  350. package/src/syncer/index.ts +4 -4
  351. package/src/syncer/module-loader.ts +28 -25
  352. package/src/syncer/syncer.ts +155 -162
  353. package/src/template/entity-converter.ts +18 -27
  354. package/src/template/helpers.ts +8 -11
  355. package/src/template/implementations/entity.template.ts +6 -6
  356. package/src/template/implementations/generated.template.ts +99 -99
  357. package/src/template/implementations/generated_http.template.ts +21 -54
  358. package/src/template/implementations/generated_sso.template.ts +78 -65
  359. package/src/template/implementations/init_types.template.ts +4 -6
  360. package/src/template/implementations/model.template.ts +47 -38
  361. package/src/template/implementations/model_test.template.ts +3 -3
  362. package/src/template/implementations/service.template.ts +56 -80
  363. package/src/template/implementations/view_enums_buttonset.template.ts +2 -2
  364. package/src/template/implementations/view_enums_dropdown.template.ts +4 -4
  365. package/src/template/implementations/view_enums_select.template.ts +3 -3
  366. package/src/template/implementations/view_form.template.ts +34 -75
  367. package/src/template/implementations/view_id_all_select.template.ts +2 -2
  368. package/src/template/implementations/view_id_async_select.template.ts +9 -23
  369. package/src/template/implementations/view_list.template.ts +54 -95
  370. package/src/template/implementations/view_list_columns.template.ts +4 -10
  371. package/src/template/implementations/view_search_input.template.ts +2 -2
  372. package/src/template/index.ts +4 -2
  373. package/src/template/template-manager.ts +166 -0
  374. package/src/template/template-types.ts +16 -0
  375. package/src/template/template.ts +29 -10
  376. package/src/template/zod-converter.ts +459 -101
  377. package/src/testing/_relation-graph.ts +18 -11
  378. package/src/testing/fixture-manager.ts +468 -362
  379. package/src/types/types.ts +516 -248
  380. package/src/typings/knex.d.ts +7 -9
  381. package/src/utils/async-utils.ts +8 -12
  382. package/src/utils/console-util.ts +1 -1
  383. package/src/utils/controller.ts +3 -0
  384. package/src/utils/esm-utils.ts +8 -18
  385. package/src/utils/formatter.ts +109 -0
  386. package/src/utils/fs-utils.ts +1 -1
  387. package/src/utils/lodash-able.ts +1 -4
  388. package/src/utils/object-utils.ts +217 -0
  389. package/src/utils/path-utils.ts +3 -6
  390. package/src/utils/process-utils.ts +1 -1
  391. package/src/utils/sql-parser.ts +23 -5
  392. package/src/utils/type-utils.ts +83 -0
  393. package/src/utils/utils.ts +58 -9
  394. package/src/utils/zod-error.ts +3 -3
  395. package/dist/bin/cli-wrapper.d.ts +0 -3
  396. package/dist/bin/cli-wrapper.d.ts.map +0 -1
  397. package/dist/bin/cli-wrapper.js +0 -72
  398. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts +0 -2
  399. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts.map +0 -1
  400. package/dist/database/knex-plugins/knex-on-duplicate-update.js +0 -39
  401. package/dist/entity/entity-utils.d.ts +0 -61
  402. package/dist/entity/entity-utils.d.ts.map +0 -1
  403. package/dist/entity/entity-utils.js +0 -210
  404. package/src/bin/cli-wrapper.ts +0 -82
  405. package/src/database/knex-plugins/knex-on-duplicate-update.ts +0 -45
  406. package/src/entity/entity-utils.ts +0 -291
@@ -1,27 +1,38 @@
1
+ import assert from "assert";
1
2
  import chalk from "chalk";
2
- import * as _ from "lodash-es";
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,
@@ -407,21 +330,30 @@ export class FixtureManagerClass {
407
330
  await targetDB.destroy();
408
331
  await sourceDB.destroy();
409
332
 
410
- return _.uniqBy(fixtures, (f) => f.fixtureId);
333
+ return unique(fixtures, (f) => f.fixtureId);
411
334
  }
412
335
 
413
336
  async createFixtureRecord(
414
337
  entity: Entity,
415
- row: any,
338
+ row: {
339
+ id: number;
340
+ [key: string]: string | number | boolean | null;
341
+ },
416
342
  options?: {
417
343
  singleRecord?: boolean;
418
344
  _db?: Knex;
419
- }
345
+ },
420
346
  ): Promise<FixtureRecord[]> {
421
347
  const records: FixtureRecord[] = [];
422
348
  const visitedEntities = new Set<string>();
423
349
 
424
- 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
+ ) => {
425
357
  const fixtureId = `${entity.id}#${row.id}`;
426
358
  if (visitedEntities.has(fixtureId)) {
427
359
  return;
@@ -454,9 +386,7 @@ export class FixtureManagerClass {
454
386
  const fromColumn = `${inflection.singularize(entity.table)}_id`;
455
387
  const toColumn = `${inflection.singularize(relatedEntity.table)}_id`;
456
388
 
457
- const relatedIds = await db(throughTable)
458
- .where(fromColumn, row.id)
459
- .pluck(toColumn);
389
+ const relatedIds = await db(throughTable).where(fromColumn, row.id).pluck(toColumn);
460
390
  record.columns[prop.name].value = relatedIds;
461
391
  } else if (isHasManyRelationProp(prop)) {
462
392
  const relatedEntity = EntityManager.get(prop.with);
@@ -467,12 +397,10 @@ export class FixtureManagerClass {
467
397
  } else if (isOneToOneRelationProp(prop) && !prop.hasJoinColumn) {
468
398
  const relatedEntity = EntityManager.get(prop.with);
469
399
  const relatedProp = relatedEntity.props.find(
470
- (p) => isRelationProp(p) && p.with === entity.id
400
+ (p) => isRelationProp(p) && p.with === entity.id,
471
401
  );
472
402
  if (relatedProp) {
473
- const relatedRow = await db(relatedEntity.table)
474
- .where("id", row.id)
475
- .first();
403
+ const relatedRow = await db(relatedEntity.table).where("id", row.id).first();
476
404
  record.columns[prop.name].value = relatedRow?.id;
477
405
  }
478
406
  } else if (isRelationProp(prop)) {
@@ -483,9 +411,7 @@ export class FixtureManagerClass {
483
411
  }
484
412
  if (!options?.singleRecord && relatedId) {
485
413
  const relatedEntity = EntityManager.get(prop.with);
486
- const relatedRow = await db(relatedEntity.table)
487
- .where("id", relatedId)
488
- .first();
414
+ const relatedRow = await db(relatedEntity.table).where("id", relatedId).first();
489
415
  if (relatedRow) {
490
416
  await create(relatedEntity, relatedRow);
491
417
  }
@@ -501,221 +427,348 @@ export class FixtureManagerClass {
501
427
  return records;
502
428
  }
503
429
 
430
+ /**
431
+ * 1. RelationGraph로 fixture 단위 삽입 순서 계산 (self-reference 포함)
432
+ * 2. 순서대로 UpsertBuilder에 등록 (UBRef로 참조 관계 표현)
433
+ * 3. 테이블별 upsert 실행 (ID는 DB가 자동 할당)
434
+ */
504
435
  async insertFixtures(
505
436
  dbName: keyof SonamuDBConfig,
506
- _fixtures: FixtureRecord[]
507
- ) {
508
- 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();
509
446
 
510
- this.relationGraph.buildGraph(fixtures);
511
- const insertionOrder = this.relationGraph.getInsertionOrder();
512
447
  const db = knex(Sonamu.dbConfig[dbName]);
448
+ const results: FixtureImportResult[] = [];
513
449
 
514
- await db.transaction(async (trx) => {
515
- 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();
516
454
 
455
+ // 2. 순서대로 UpsertBuilder에 등록 (override 체크)
517
456
  for (const fixtureId of insertionOrder) {
518
- const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
519
- const result = await this.insertFixture(trx as any, fixture);
520
- if (result.id !== fixture.id) {
521
- // 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
+
522
474
  console.log(
523
475
  chalk.yellow(
524
- `Unique constraint violation: ${fixture.entityId}#${fixture.id} -> ${fixture.entityId}#${result.id}`
525
- )
476
+ `Skipped ${fixture.entityId}#${fixture.id} (existing: #${existingId}, override: false)`,
477
+ ),
526
478
  );
527
- fixtures.forEach((f) => {
528
- Object.values(f.columns).forEach((column) => {
529
- if (
530
- column.prop.type === "relation" &&
531
- column.prop.with === result.entityId &&
532
- column.value === fixture.id
533
- ) {
534
- column.value = result.id;
535
- }
536
- });
537
- });
538
- fixture.id = result.id;
479
+ continue;
539
480
  }
540
- }
541
481
 
542
- for (const fixtureId of insertionOrder) {
543
- const fixture = fixtures.find((f) => f.fixtureId === fixtureId)!;
544
- 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
+ );
545
488
  }
546
- await trx.raw(`SET FOREIGN_KEY_CHECKS = 1`);
547
- });
548
489
 
549
- 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;
550
498
 
551
- for await (const r of fixtures) {
552
- const entity = EntityManager.get(r.entityId);
553
- const record = await db(entity.table).where("id", r.id).first();
554
- records.push({
555
- entityId: r.entityId,
556
- data: record,
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);
525
+
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
+ }
557
553
  });
554
+ } finally {
555
+ await db.destroy();
558
556
  }
559
557
 
560
- await db.destroy();
561
-
562
- return _.uniqBy(records, (r) => `${r.entityId}#${r.data.id}`);
558
+ return unique(results, (r) => `${r.entityId}#${r.data.id}`);
563
559
  }
564
560
 
565
- private prepareInsertData(fixture: FixtureRecord) {
566
- 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
+
567
572
  for (const [propName, column] of Object.entries(fixture.columns)) {
568
- if (isVirtualProp(column.prop)) {
573
+ const prop = column.prop;
574
+
575
+ if (isVirtualProp(prop)) {
569
576
  continue;
570
577
  }
571
578
 
572
- const prop = column.prop as EntityProp;
573
- if (!isRelationProp(prop)) {
574
- if (prop.type === "json") {
575
- insertData[propName] = JSON.stringify(column.value);
576
- } else if (prop.type === "timestamp" || prop.type === "datetime") {
577
- insertData[propName] = new Date(column.value);
578
- } else {
579
- 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;
580
584
  }
581
- } else if (
582
- isBelongsToOneRelationProp(prop) ||
583
- (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
584
- ) {
585
- 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);
586
621
  }
587
622
  }
588
- 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;
589
630
  }
590
631
 
591
- private async insertFixture(db: Knex, fixture: FixtureRecord) {
592
- const insertData = this.prepareInsertData(fixture);
593
- 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
+ }
594
639
 
595
- try {
596
- const uniqueFound = await this.checkUniqueViolation(db, entity, fixture);
597
- if (uniqueFound) {
598
- return {
599
- entityId: fixture.entityId,
600
- id: uniqueFound.id,
601
- };
602
- }
640
+ switch (prop.type) {
641
+ case "json":
642
+ // UpsertBuilder.register에서 JSON.stringify 처리하므로 object 그대로 전달
643
+ return value;
603
644
 
604
- const found = await db(entity.table).where("id", fixture.id).first();
605
- if (found && !fixture.override) {
606
- return {
607
- entityId: fixture.entityId,
608
- id: found.id,
609
- };
610
- }
645
+ case "date":
646
+ if (typeof value === "string" || typeof value === "number") {
647
+ return new Date(value);
648
+ }
649
+ return value;
650
+
651
+ default:
652
+ return value;
653
+ }
654
+ }
611
655
 
612
- const q = db.insert(insertData).into(entity.table);
613
- await q.onDuplicateUpdate.apply(q, Object.keys(insertData));
614
- console.log(chalk.green(`Inserted into ${entity.table}: #${fixture.id}`));
656
+ /**
657
+ * 테이블 순서 추출 (fixtures에 포함된 테이블만)
658
+ */
659
+ private getTableOrder(fixtures: FixtureRecord[]): string[] {
660
+ const tables: string[] = [];
661
+ const seen = new Set<string>();
615
662
 
616
- return {
617
- entityId: fixture.entityId,
618
- id: fixture.id,
619
- };
620
- } catch (err) {
621
- console.log(err);
622
- throw err;
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);
668
+ }
623
669
  }
670
+
671
+ return tables;
624
672
  }
625
673
 
626
- private async handleManyToManyRelations(
627
- db: Knex,
628
- fixture: FixtureRecord,
629
- fixtures: FixtureRecord[]
630
- ) {
631
- for (const [, column] of Object.entries(fixture.columns)) {
632
- const prop = column.prop as EntityProp;
633
- if (isManyToManyRelationProp(prop)) {
634
- const joinTable = (prop as ManyToManyRelationProp).joinTable;
635
- const relatedIds = column.value as number[];
636
-
637
- for (const relatedId of relatedIds) {
638
- if (
639
- !fixtures.find((f) => f.fixtureId === `${prop.with}#${relatedId}`)
640
- ) {
641
- continue;
642
- }
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);
643
682
 
644
- const entity = EntityManager.get(fixture.entityId);
683
+ if (!sourceRef) continue;
684
+
685
+ const sourceUuidToId = insertedIdsByTable.get(entity.table);
686
+ const sourceId = sourceUuidToId?.get(sourceRef.uuid);
687
+
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;
645
702
  const relatedEntity = EntityManager.get(prop.with);
646
- if (!entity || !relatedEntity) {
647
- throw new Error(
648
- `Entity not found: ${fixture.entityId}, ${prop.with}`
649
- );
650
- }
651
703
 
652
- const [found] = await db(joinTable)
653
- .where({
654
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
655
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
656
- })
657
- .limit(1);
658
- if (found) {
659
- continue;
660
- }
704
+ const sourceColumn = `${inflection.singularize(entity.table)}_id`;
705
+ const targetColumn = `${inflection.singularize(relatedEntity.table)}_id`;
661
706
 
662
- const newIds = await db(joinTable).insert({
663
- [`${inflection.singularize(entity.table)}_id`]: fixture.id,
664
- [`${inflection.singularize(relatedEntity.table)}_id`]: relatedId,
665
- });
666
- console.log(
667
- chalk.green(
668
- `Inserted into ${joinTable}: ${entity.table}(${fixture.id}) - ${relatedEntity.table}(${relatedId}) ID: ${newIds}`
669
- )
670
- );
671
- }
672
- }
673
- }
674
- }
707
+ for (const relatedId of relatedIds) {
708
+ const relatedFixtureId = `${prop.with}#${relatedId}`;
709
+ const relatedRef = this.fixtureRefMap.get(relatedFixtureId);
675
710
 
676
- async addFixtureLoader(code: string) {
677
- const path = Sonamu.apiRootPath + "/src/testing/fixture.ts";
678
- let content = readFileSync(path).toString();
711
+ let targetId: number;
679
712
 
680
- const fixtureLoaderStart = content.indexOf("const fixtureLoader = {");
681
- const fixtureLoaderEnd = content.indexOf("};", fixtureLoaderStart);
713
+ if (relatedRef) {
714
+ const relatedUuidToId = insertedIdsByTable.get(relatedEntity.table);
715
+ const resolvedId = relatedUuidToId?.get(relatedRef.uuid);
682
716
 
683
- if (fixtureLoaderStart !== -1 && fixtureLoaderEnd !== -1) {
684
- const newContent =
685
- content.slice(0, fixtureLoaderEnd) +
686
- " " +
687
- code +
688
- "\n" +
689
- content.slice(fixtureLoaderEnd);
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
+ }
690
727
 
691
- writeFileSync(path, newContent);
692
- } else {
693
- throw new Error("Failed to find fixtureLoader in fixture.ts");
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
+ }
694
751
  }
695
752
  }
696
753
 
697
- // 해당 픽스쳐의 값으로 유니크 제약에 위배되는 레코드가 있는지 확인
698
- private async checkUniqueViolation(
699
- db: Knex,
700
- entity: Entity,
701
- fixture: FixtureRecord
702
- ) {
703
- 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") ?? [];
704
756
 
705
- // ManyToMany 관계 테이블의 유니크 제약은 제외
706
757
  const uniqueIndexes = _uniqueIndexes.filter((index) =>
707
- index.columns.every((column) => !column.startsWith(`${entity.table}__`))
758
+ index.columns.every((column) => !column.startsWith(`${entity.table}__`)),
708
759
  );
709
760
  if (uniqueIndexes.length === 0) {
710
761
  return null;
711
762
  }
712
763
 
713
764
  let uniqueQuery = db(entity.table);
765
+ let hasCondition = false;
766
+
714
767
  for (const index of uniqueIndexes) {
715
768
  // 컬럼 중 하나라도 null이면 유니크 제약을 위반하지 않기 때문에 해당 인덱스는 무시
716
769
  const containsNull = index.columns.some((column) => {
717
- const field = column.split("_id")[0];
718
- return fixture.columns[field].value === null;
770
+ const field = column.replace(/_id$/, "");
771
+ return fixture.columns[field]?.value === null;
719
772
  });
720
773
  if (containsNull) {
721
774
  continue;
@@ -723,18 +776,71 @@ export class FixtureManagerClass {
723
776
 
724
777
  uniqueQuery = uniqueQuery.orWhere((qb) => {
725
778
  for (const column of index.columns) {
726
- const field = column.split("_id")[0];
779
+ const field = column.replace(/_id$/, "");
727
780
 
728
- if (Array.isArray(fixture.columns[field].value)) {
781
+ if (Array.isArray(fixture.columns[field]?.value)) {
729
782
  qb.whereIn(column, fixture.columns[field].value);
730
783
  } else {
731
- qb.andWhere(column, fixture.columns[field].value);
784
+ qb.andWhere(column, fixture.columns[field]?.value);
732
785
  }
733
786
  }
734
787
  });
788
+ hasCondition = true;
789
+ }
790
+
791
+ if (!hasCondition) {
792
+ return null;
735
793
  }
794
+
736
795
  const [uniqueFound] = await uniqueQuery;
737
796
  return uniqueFound;
738
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
+ }
739
844
  }
845
+
740
846
  export const FixtureManager = new FixtureManagerClass();