sonamu 0.5.6 → 0.6.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 (365) hide show
  1. package/dist/api/base-frame.js +12 -2
  2. package/dist/api/caster.js +66 -2
  3. package/dist/api/code-converters.js +489 -2
  4. package/dist/api/config.d.ts +76 -0
  5. package/dist/api/config.d.ts.map +1 -0
  6. package/dist/api/config.js +32 -0
  7. package/dist/api/context.d.ts +1 -0
  8. package/dist/api/context.d.ts.map +1 -1
  9. package/dist/api/context.js +3 -2
  10. package/dist/api/decorators.d.ts +1 -0
  11. package/dist/api/decorators.d.ts.map +1 -1
  12. package/dist/api/decorators.js +142 -2
  13. package/dist/api/index.js +9 -2
  14. package/dist/api/sonamu.d.ts +8 -22
  15. package/dist/api/sonamu.d.ts.map +1 -1
  16. package/dist/api/sonamu.js +482 -2
  17. package/dist/bin/build-config.d.ts +2 -1
  18. package/dist/bin/build-config.d.ts.map +1 -1
  19. package/dist/bin/build-config.js +12 -2
  20. package/dist/bin/cli-wrapper.js +71 -2
  21. package/dist/bin/cli.js +418 -2
  22. package/dist/bin/hot-hook-register.d.ts +11 -0
  23. package/dist/bin/hot-hook-register.d.ts.map +1 -0
  24. package/dist/bin/hot-hook-register.js +21 -0
  25. package/dist/database/_batch_update.js +78 -2
  26. package/dist/database/base-model.js +247 -2
  27. package/dist/database/code-generator.js +53 -2
  28. package/dist/database/db.d.ts +5 -16
  29. package/dist/database/db.d.ts.map +1 -1
  30. package/dist/database/db.js +132 -2
  31. package/dist/database/knex-plugins/knex-on-duplicate-update.js +39 -2
  32. package/dist/database/puri-wrapper.d.ts +22 -10
  33. package/dist/database/puri-wrapper.d.ts.map +1 -1
  34. package/dist/database/puri-wrapper.js +109 -2
  35. package/dist/database/puri.d.ts +105 -73
  36. package/dist/database/puri.d.ts.map +1 -1
  37. package/dist/database/puri.js +539 -2
  38. package/dist/database/puri.types.d.ts +33 -42
  39. package/dist/database/puri.types.d.ts.map +1 -1
  40. package/dist/database/puri.types.js +3 -2
  41. package/dist/database/transaction-context.d.ts +3 -3
  42. package/dist/database/transaction-context.d.ts.map +1 -1
  43. package/dist/database/transaction-context.js +14 -2
  44. package/dist/database/upsert-builder.js +215 -2
  45. package/dist/entity/entity-manager.d.ts +3 -1
  46. package/dist/entity/entity-manager.d.ts.map +1 -1
  47. package/dist/entity/entity-manager.js +114 -2
  48. package/dist/entity/entity-utils.js +210 -2
  49. package/dist/entity/entity.d.ts.map +1 -1
  50. package/dist/entity/entity.js +651 -2
  51. package/dist/exceptions/error-handler.js +29 -2
  52. package/dist/exceptions/so-exceptions.js +85 -2
  53. package/dist/file-storage/driver.js +79 -2
  54. package/dist/file-storage/file-storage.js +75 -2
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +28 -2
  58. package/dist/migration/code-generation.js +558 -2
  59. package/dist/migration/migration-set.js +364 -2
  60. package/dist/migration/migrator.d.ts +0 -9
  61. package/dist/migration/migrator.d.ts.map +1 -1
  62. package/dist/migration/migrator.js +510 -2
  63. package/dist/migration/types.js +3 -2
  64. package/dist/naite/naite.d.ts +12 -0
  65. package/dist/naite/naite.d.ts.map +1 -0
  66. package/dist/naite/naite.js +72 -0
  67. package/dist/stream/index.js +3 -2
  68. package/dist/stream/sse.js +38 -2
  69. package/dist/syncer/api-parser.d.ts +20 -0
  70. package/dist/syncer/api-parser.d.ts.map +1 -0
  71. package/dist/syncer/api-parser.js +229 -0
  72. package/dist/syncer/checksum.d.ts +21 -0
  73. package/dist/syncer/checksum.d.ts.map +1 -0
  74. package/dist/syncer/checksum.js +98 -0
  75. package/dist/syncer/code-generator.d.ts +20 -0
  76. package/dist/syncer/code-generator.d.ts.map +1 -0
  77. package/dist/syncer/code-generator.js +141 -0
  78. package/dist/syncer/entity-operations.d.ts +17 -0
  79. package/dist/syncer/entity-operations.d.ts.map +1 -0
  80. package/dist/syncer/entity-operations.js +58 -0
  81. package/dist/syncer/file-patterns.d.ts +29 -0
  82. package/dist/syncer/file-patterns.d.ts.map +1 -0
  83. package/dist/syncer/file-patterns.js +38 -0
  84. package/dist/syncer/index.d.ts +6 -0
  85. package/dist/syncer/index.d.ts.map +1 -1
  86. package/dist/syncer/index.js +9 -2
  87. package/dist/syncer/module-loader.d.ts +35 -0
  88. package/dist/syncer/module-loader.d.ts.map +1 -0
  89. package/dist/syncer/module-loader.js +82 -0
  90. package/dist/syncer/syncer.d.ts +93 -108
  91. package/dist/syncer/syncer.d.ts.map +1 -1
  92. package/dist/syncer/syncer.js +375 -2
  93. package/dist/template/entity-converter.d.ts +14 -0
  94. package/dist/template/entity-converter.d.ts.map +1 -0
  95. package/dist/template/entity-converter.js +101 -0
  96. package/dist/template/helpers.d.ts +23 -0
  97. package/dist/template/helpers.d.ts.map +1 -0
  98. package/dist/template/helpers.js +64 -0
  99. package/dist/{templates → template/implementations}/entity.template.d.ts +3 -3
  100. package/dist/template/implementations/entity.template.d.ts.map +1 -0
  101. package/dist/template/implementations/entity.template.js +87 -0
  102. package/dist/{templates → template/implementations}/generated.template.d.ts +3 -3
  103. package/dist/template/implementations/generated.template.d.ts.map +1 -0
  104. package/dist/template/implementations/generated.template.js +232 -0
  105. package/dist/{templates → template/implementations}/generated_http.template.d.ts +3 -3
  106. package/dist/template/implementations/generated_http.template.d.ts.map +1 -0
  107. package/dist/template/implementations/generated_http.template.js +131 -0
  108. package/dist/{templates → template/implementations}/generated_sso.template.d.ts +3 -3
  109. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -0
  110. package/dist/template/implementations/generated_sso.template.js +105 -0
  111. package/dist/{templates → template/implementations}/init_types.template.d.ts +3 -3
  112. package/dist/template/implementations/init_types.template.d.ts.map +1 -0
  113. package/dist/template/implementations/init_types.template.js +38 -0
  114. package/dist/template/implementations/model.template.d.ts +17 -0
  115. package/dist/template/implementations/model.template.d.ts.map +1 -0
  116. package/dist/template/implementations/model.template.js +171 -0
  117. package/dist/{templates → template/implementations}/model_test.template.d.ts +3 -3
  118. package/dist/template/implementations/model_test.template.d.ts.map +1 -0
  119. package/dist/template/implementations/model_test.template.js +35 -0
  120. package/dist/{templates → template/implementations}/service.template.d.ts +6 -6
  121. package/dist/template/implementations/service.template.d.ts.map +1 -0
  122. package/dist/template/implementations/service.template.js +193 -0
  123. package/dist/{templates → template/implementations}/view_enums_buttonset.template.d.ts +3 -3
  124. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +1 -0
  125. package/dist/template/implementations/view_enums_buttonset.template.js +31 -0
  126. package/dist/{templates → template/implementations}/view_enums_dropdown.template.d.ts +3 -4
  127. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +1 -0
  128. package/dist/template/implementations/view_enums_dropdown.template.js +50 -0
  129. package/dist/{templates → template/implementations}/view_enums_select.template.d.ts +3 -3
  130. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -0
  131. package/dist/template/implementations/view_enums_select.template.js +55 -0
  132. package/dist/{templates → template/implementations}/view_form.template.d.ts +5 -5
  133. package/dist/template/implementations/view_form.template.d.ts.map +1 -0
  134. package/dist/template/implementations/view_form.template.js +337 -0
  135. package/dist/{templates → template/implementations}/view_id_all_select.template.d.ts +3 -3
  136. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -0
  137. package/dist/template/implementations/view_id_all_select.template.js +31 -0
  138. package/dist/{templates → template/implementations}/view_id_async_select.template.d.ts +3 -3
  139. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -0
  140. package/dist/template/implementations/view_id_async_select.template.js +105 -0
  141. package/dist/{templates → template/implementations}/view_list.template.d.ts +5 -13
  142. package/dist/template/implementations/view_list.template.d.ts.map +1 -0
  143. package/dist/template/implementations/view_list.template.js +465 -0
  144. package/dist/{templates → template/implementations}/view_list_columns.template.d.ts +3 -3
  145. package/dist/template/implementations/view_list_columns.template.d.ts.map +1 -0
  146. package/dist/template/implementations/view_list_columns.template.js +49 -0
  147. package/dist/{templates → template/implementations}/view_search_input.template.d.ts +3 -3
  148. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -0
  149. package/dist/template/implementations/view_search_input.template.js +64 -0
  150. package/dist/template/index.d.ts +5 -0
  151. package/dist/template/index.d.ts.map +1 -0
  152. package/dist/template/index.js +6 -0
  153. package/dist/template/template.d.ts +39 -0
  154. package/dist/template/template.d.ts.map +1 -0
  155. package/dist/template/template.js +47 -0
  156. package/dist/template/zod-converter.d.ts +18 -0
  157. package/dist/template/zod-converter.d.ts.map +1 -0
  158. package/dist/template/zod-converter.js +166 -0
  159. package/dist/testing/_relation-graph.js +80 -2
  160. package/dist/testing/fixture-manager.d.ts.map +1 -1
  161. package/dist/testing/fixture-manager.js +521 -2
  162. package/dist/types/types.d.ts +39 -40
  163. package/dist/types/types.d.ts.map +1 -1
  164. package/dist/types/types.js +289 -2
  165. package/dist/typings/knex.d.js +3 -2
  166. package/dist/utils/async-utils.d.ts +7 -0
  167. package/dist/utils/async-utils.d.ts.map +1 -1
  168. package/dist/utils/async-utils.js +57 -2
  169. package/dist/utils/console-util.d.ts +2 -0
  170. package/dist/utils/console-util.d.ts.map +1 -0
  171. package/dist/utils/console-util.js +6 -0
  172. package/dist/utils/controller.js +26 -2
  173. package/dist/utils/esm-utils.d.ts +45 -0
  174. package/dist/utils/esm-utils.d.ts.map +1 -0
  175. package/dist/utils/esm-utils.js +56 -0
  176. package/dist/utils/fs-utils.js +17 -2
  177. package/dist/utils/lodash-able.js +6 -2
  178. package/dist/utils/model.js +22 -2
  179. package/dist/utils/path-utils.d.ts +89 -0
  180. package/dist/utils/path-utils.d.ts.map +1 -0
  181. package/dist/utils/path-utils.js +60 -0
  182. package/dist/utils/process-utils.d.ts +13 -0
  183. package/dist/utils/process-utils.d.ts.map +1 -0
  184. package/dist/utils/process-utils.js +36 -0
  185. package/dist/utils/sql-parser.js +35 -2
  186. package/dist/utils/utils.d.ts +4 -7
  187. package/dist/utils/utils.d.ts.map +1 -1
  188. package/dist/utils/utils.js +33 -2
  189. package/dist/utils/zod-error.d.ts.map +1 -1
  190. package/dist/utils/zod-error.js +19 -2
  191. package/package.json +21 -9
  192. package/src/api/code-converters.ts +2 -2
  193. package/src/api/config.ts +142 -0
  194. package/src/api/context.ts +1 -0
  195. package/src/api/decorators.ts +15 -5
  196. package/src/api/sonamu.ts +102 -87
  197. package/src/bin/build-config.ts +2 -1
  198. package/src/bin/cli-wrapper.ts +10 -3
  199. package/src/bin/cli.ts +108 -56
  200. package/src/bin/hot-hook-register.ts +22 -0
  201. package/src/database/base-model.ts +1 -1
  202. package/src/database/code-generator.ts +1 -1
  203. package/src/database/db.ts +53 -60
  204. package/src/database/puri-wrapper.ts +104 -26
  205. package/src/database/puri.ts +477 -580
  206. package/src/database/puri.types.ts +111 -201
  207. package/src/database/transaction-context.ts +4 -4
  208. package/src/database/upsert-builder.ts +1 -1
  209. package/src/entity/entity-manager.ts +19 -15
  210. package/src/entity/entity.ts +4 -3
  211. package/src/index.ts +2 -0
  212. package/src/migration/code-generation.ts +1 -1
  213. package/src/migration/migration-set.ts +1 -1
  214. package/src/migration/migrator.ts +23 -152
  215. package/src/naite/naite.ts +70 -0
  216. package/src/syncer/api-parser.ts +299 -0
  217. package/src/syncer/checksum.ts +152 -0
  218. package/src/syncer/code-generator.ts +202 -0
  219. package/src/syncer/entity-operations.ts +68 -0
  220. package/src/syncer/file-patterns.ts +56 -0
  221. package/src/syncer/index.ts +6 -0
  222. package/src/syncer/module-loader.ts +125 -0
  223. package/src/syncer/syncer.ts +363 -1420
  224. package/src/template/entity-converter.ts +123 -0
  225. package/src/template/helpers.ts +84 -0
  226. package/src/{templates → template/implementations}/entity.template.ts +4 -4
  227. package/src/{templates → template/implementations}/generated.template.ts +9 -9
  228. package/src/{templates → template/implementations}/generated_http.template.ts +9 -6
  229. package/src/{templates → template/implementations}/generated_sso.template.ts +7 -7
  230. package/src/{templates → template/implementations}/init_types.template.ts +4 -4
  231. package/src/{templates → template/implementations}/model.template.ts +9 -9
  232. package/src/{templates → template/implementations}/model_test.template.ts +5 -5
  233. package/src/{templates → template/implementations}/service.template.ts +29 -12
  234. package/src/{templates → template/implementations}/view_enums_buttonset.template.ts +3 -3
  235. package/src/{templates → template/implementations}/view_enums_dropdown.template.ts +5 -21
  236. package/src/{templates → template/implementations}/view_enums_select.template.ts +4 -4
  237. package/src/{templates → template/implementations}/view_form.template.ts +11 -13
  238. package/src/{templates → template/implementations}/view_id_all_select.template.ts +3 -3
  239. package/src/{templates → template/implementations}/view_id_async_select.template.ts +3 -3
  240. package/src/{templates → template/implementations}/view_list.template.ts +13 -64
  241. package/src/{templates → template/implementations}/view_list_columns.template.ts +3 -3
  242. package/src/{templates → template/implementations}/view_search_input.template.ts +3 -3
  243. package/src/template/index.ts +4 -0
  244. package/src/template/template.ts +86 -0
  245. package/src/template/zod-converter.ts +219 -0
  246. package/src/testing/fixture-manager.ts +8 -1
  247. package/src/types/types.ts +39 -62
  248. package/src/utils/async-utils.ts +17 -0
  249. package/src/utils/console-util.ts +4 -0
  250. package/src/utils/esm-utils.ts +69 -0
  251. package/src/utils/path-utils.ts +102 -0
  252. package/src/utils/process-utils.ts +46 -0
  253. package/src/utils/sql-parser.ts +1 -1
  254. package/src/utils/utils.ts +14 -40
  255. package/src/utils/zod-error.ts +0 -1
  256. package/dist/api/base-frame.js.map +0 -1
  257. package/dist/api/caster.js.map +0 -1
  258. package/dist/api/code-converters.js.map +0 -1
  259. package/dist/api/context.js.map +0 -1
  260. package/dist/api/decorators.js.map +0 -1
  261. package/dist/api/index.js.map +0 -1
  262. package/dist/api/sonamu.js.map +0 -1
  263. package/dist/bin/build-config.js.map +0 -1
  264. package/dist/bin/cli-wrapper.js.map +0 -1
  265. package/dist/bin/cli.js.map +0 -1
  266. package/dist/database/_batch_update.js.map +0 -1
  267. package/dist/database/base-model.js.map +0 -1
  268. package/dist/database/code-generator.js.map +0 -1
  269. package/dist/database/db.js.map +0 -1
  270. package/dist/database/knex-plugins/knex-on-duplicate-update.js.map +0 -1
  271. package/dist/database/puri-wrapper.js.map +0 -1
  272. package/dist/database/puri.js.map +0 -1
  273. package/dist/database/puri.types.js.map +0 -1
  274. package/dist/database/transaction-context.js.map +0 -1
  275. package/dist/database/upsert-builder.js.map +0 -1
  276. package/dist/entity/entity-manager.js.map +0 -1
  277. package/dist/entity/entity-utils.js.map +0 -1
  278. package/dist/entity/entity.js.map +0 -1
  279. package/dist/exceptions/error-handler.js.map +0 -1
  280. package/dist/exceptions/so-exceptions.js.map +0 -1
  281. package/dist/file-storage/driver.js.map +0 -1
  282. package/dist/file-storage/file-storage.js.map +0 -1
  283. package/dist/index.js.map +0 -1
  284. package/dist/migration/code-generation.js.map +0 -1
  285. package/dist/migration/migration-set.js.map +0 -1
  286. package/dist/migration/migrator.js.map +0 -1
  287. package/dist/migration/types.js.map +0 -1
  288. package/dist/stream/index.js.map +0 -1
  289. package/dist/stream/sse.js.map +0 -1
  290. package/dist/syncer/index.js.map +0 -1
  291. package/dist/syncer/syncer.js.map +0 -1
  292. package/dist/templates/base-template.d.ts +0 -13
  293. package/dist/templates/base-template.d.ts.map +0 -1
  294. package/dist/templates/base-template.js +0 -2
  295. package/dist/templates/base-template.js.map +0 -1
  296. package/dist/templates/entity.template.d.ts.map +0 -1
  297. package/dist/templates/entity.template.js +0 -2
  298. package/dist/templates/entity.template.js.map +0 -1
  299. package/dist/templates/generated.template.d.ts.map +0 -1
  300. package/dist/templates/generated.template.js +0 -2
  301. package/dist/templates/generated.template.js.map +0 -1
  302. package/dist/templates/generated_http.template.d.ts.map +0 -1
  303. package/dist/templates/generated_http.template.js +0 -2
  304. package/dist/templates/generated_http.template.js.map +0 -1
  305. package/dist/templates/generated_sso.template.d.ts.map +0 -1
  306. package/dist/templates/generated_sso.template.js +0 -2
  307. package/dist/templates/generated_sso.template.js.map +0 -1
  308. package/dist/templates/index.d.ts +0 -2
  309. package/dist/templates/index.d.ts.map +0 -1
  310. package/dist/templates/index.js +0 -2
  311. package/dist/templates/index.js.map +0 -1
  312. package/dist/templates/init_types.template.d.ts.map +0 -1
  313. package/dist/templates/init_types.template.js +0 -2
  314. package/dist/templates/init_types.template.js.map +0 -1
  315. package/dist/templates/model.template.d.ts +0 -17
  316. package/dist/templates/model.template.d.ts.map +0 -1
  317. package/dist/templates/model.template.js +0 -2
  318. package/dist/templates/model.template.js.map +0 -1
  319. package/dist/templates/model_test.template.d.ts.map +0 -1
  320. package/dist/templates/model_test.template.js +0 -2
  321. package/dist/templates/model_test.template.js.map +0 -1
  322. package/dist/templates/service.template.d.ts.map +0 -1
  323. package/dist/templates/service.template.js +0 -2
  324. package/dist/templates/service.template.js.map +0 -1
  325. package/dist/templates/view_enums_buttonset.template.d.ts.map +0 -1
  326. package/dist/templates/view_enums_buttonset.template.js +0 -2
  327. package/dist/templates/view_enums_buttonset.template.js.map +0 -1
  328. package/dist/templates/view_enums_dropdown.template.d.ts.map +0 -1
  329. package/dist/templates/view_enums_dropdown.template.js +0 -2
  330. package/dist/templates/view_enums_dropdown.template.js.map +0 -1
  331. package/dist/templates/view_enums_select.template.d.ts.map +0 -1
  332. package/dist/templates/view_enums_select.template.js +0 -2
  333. package/dist/templates/view_enums_select.template.js.map +0 -1
  334. package/dist/templates/view_form.template.d.ts.map +0 -1
  335. package/dist/templates/view_form.template.js +0 -2
  336. package/dist/templates/view_form.template.js.map +0 -1
  337. package/dist/templates/view_id_all_select.template.d.ts.map +0 -1
  338. package/dist/templates/view_id_all_select.template.js +0 -2
  339. package/dist/templates/view_id_all_select.template.js.map +0 -1
  340. package/dist/templates/view_id_async_select.template.d.ts.map +0 -1
  341. package/dist/templates/view_id_async_select.template.js +0 -2
  342. package/dist/templates/view_id_async_select.template.js.map +0 -1
  343. package/dist/templates/view_list.template.d.ts.map +0 -1
  344. package/dist/templates/view_list.template.js +0 -2
  345. package/dist/templates/view_list.template.js.map +0 -1
  346. package/dist/templates/view_list_columns.template.d.ts.map +0 -1
  347. package/dist/templates/view_list_columns.template.js +0 -2
  348. package/dist/templates/view_list_columns.template.js.map +0 -1
  349. package/dist/templates/view_search_input.template.d.ts.map +0 -1
  350. package/dist/templates/view_search_input.template.js +0 -2
  351. package/dist/templates/view_search_input.template.js.map +0 -1
  352. package/dist/testing/_relation-graph.js.map +0 -1
  353. package/dist/testing/fixture-manager.js.map +0 -1
  354. package/dist/types/types.js.map +0 -1
  355. package/dist/typings/knex.d.js.map +0 -1
  356. package/dist/utils/async-utils.js.map +0 -1
  357. package/dist/utils/controller.js.map +0 -1
  358. package/dist/utils/fs-utils.js.map +0 -1
  359. package/dist/utils/lodash-able.js.map +0 -1
  360. package/dist/utils/model.js.map +0 -1
  361. package/dist/utils/sql-parser.js.map +0 -1
  362. package/dist/utils/utils.js.map +0 -1
  363. package/dist/utils/zod-error.js.map +0 -1
  364. package/src/templates/base-template.ts +0 -19
  365. package/src/templates/index.ts +0 -1
@@ -1,300 +1,188 @@
1
1
  import path, { dirname } from "path";
2
- import { globAsync, importMultiple } from "../utils/utils";
3
- import { createReadStream } from "fs";
4
- import { mkdir, readFile, rm, writeFile } from "fs/promises";
2
+ import { mkdir, readFile, writeFile } from "fs/promises";
5
3
  import { exists } from "../utils/fs-utils";
6
- import crypto from "crypto";
7
- import equal from "fast-deep-equal";
8
- import _, { chunk } from "lodash";
9
- import inflection from "inflection";
4
+ import * as _ from "lodash-es";
10
5
  import { EntityManager, EntityNamesRecord } from "../entity/entity-manager";
11
- import ts from "typescript";
12
- import {
13
- ApiParam,
14
- ApiParamType,
15
- EntityProp,
16
- EntityPropNode,
17
- isBelongsToOneRelationProp,
18
- isBigIntegerProp,
19
- isBooleanProp,
20
- isDateProp,
21
- isDateTimeProp,
22
- isDecimalProp,
23
- isDoubleProp,
24
- isEnumProp,
25
- isFloatProp,
26
- isIntegerProp,
27
- isJsonProp,
28
- isOneToOneRelationProp,
29
- isRelationProp,
30
- isStringProp,
31
- isTextProp,
32
- isTimeProp,
33
- isTimestampProp,
34
- isUuidProp,
35
- isVirtualProp,
36
- } from "../types/types";
37
- import {
38
- ApiDecoratorOptions,
39
- registeredApis,
40
- ExtendedApi,
41
- } from "../api/decorators";
42
- import { z } from "zod";
6
+ import { GenerateOptions } from "../types/types";
43
7
  import chalk from "chalk";
44
- import {
45
- TemplateKey,
46
- PathAndCode,
47
- TemplateOptions,
48
- GenerateOptions,
49
- RenderingNode,
50
- } from "../types/types";
51
- import {
52
- AlreadyProcessedException,
53
- BadRequestException,
54
- ServiceUnavailableException,
55
- } from "../exceptions/so-exceptions";
56
- import { wrapIf } from "../utils/lodash-able";
57
- import { getTextTypeLength } from "../api/code-converters";
58
- import { Template } from "../templates/base-template";
59
- import { Template__generated } from "../templates/generated.template";
60
- import { Template__init_types } from "../templates/init_types.template";
61
- import { Template__entity } from "../templates/entity.template";
62
- import { Template__model } from "../templates/model.template";
63
- import { Template__model_test } from "../templates/model_test.template";
64
- import { Template__service } from "../templates/service.template";
65
- import { Template__view_form } from "../templates/view_form.template";
66
- import { Template__view_list } from "../templates/view_list.template";
67
- import prettier from "prettier";
68
- import { Template__view_id_all_select } from "../templates/view_id_all_select.template";
69
- import { Template__view_id_async_select } from "../templates/view_id_async_select.template";
70
- import { Template__view_enums_dropdown } from "../templates/view_enums_dropdown.template";
71
- import { Template__view_enums_select } from "../templates/view_enums_select.template";
72
- import { Template__view_enums_buttonset } from "../templates/view_enums_buttonset.template";
73
- import { Template__view_search_input } from "../templates/view_search_input.template";
74
- import { Template__view_list_columns } from "../templates/view_list_columns.template";
75
- import { Template__generated_http } from "../templates/generated_http.template";
8
+ import { TemplateKey, TemplateOptions } from "../types/types";
76
9
  import { Sonamu } from "../api/sonamu";
77
- import { Template__generated_sso } from "../templates/generated_sso.template";
78
- import { setTimeout as setTimeoutPromises } from "timers/promises";
79
10
  import assert from "assert";
80
- import * as swc from "@swc/core";
81
11
  import { minimatch } from "minimatch";
12
+ import { mapAsync, reduceAsync } from "../utils/async-utils";
13
+ import { centerText } from "../utils/console-util";
14
+ import { runWithGracefulShutdown } from "../utils/process-utils";
15
+ import { AbsolutePath } from "../utils/path-utils";
16
+ import { generateTemplate, renderTemplate } from "./code-generator";
17
+ import { Template } from "../template";
82
18
  import {
83
- everyAsync,
84
- filterAsync,
85
- mapAsync,
86
- reduceAsync,
87
- } from "../utils/async-utils";
88
-
89
- type FileType =
90
- | "model"
91
- | "types"
92
- | "functions"
93
- | "generated"
94
- | "entity"
95
- | "frame";
96
- type GlobPattern = {
97
- [key in FileType]: string;
98
- };
99
- type PathAndChecksum = {
100
- path: string;
101
- checksum: string;
102
- };
19
+ FileType,
20
+ getChecksumPatternGroupInAbsolutePath,
21
+ } from "./file-patterns";
22
+ import {
23
+ findChangedFilesUsingChecksums,
24
+ renewChecksums,
25
+ areFilesSame,
26
+ } from "./checksum";
27
+ import {
28
+ loadApis,
29
+ loadModels,
30
+ loadTypes,
31
+ LoadedApis,
32
+ LoadedModels,
33
+ LoadedTypes,
34
+ } from "./module-loader";
35
+ import { createEntity, delEntity } from "./entity-operations";
36
+ import { z } from "zod";
37
+ import { hot } from "@sonamu-kit/hot-hook";
38
+
103
39
  type DiffGroups = {
104
- [key in FileType]: string[];
105
- };
106
- export type RenderedTemplate = {
107
- target: string;
108
- path: string;
109
- body: string;
110
- importKeys: string[];
111
- customHeaders?: string[];
112
- preTemplates?: {
113
- key: TemplateKey;
114
- options: TemplateOptions[TemplateKey];
115
- }[];
40
+ [key in FileType]: AbsolutePath[];
116
41
  };
117
42
 
118
43
  export class Syncer {
119
- apis: {
120
- typeParameters: ApiParamType.TypeParam[];
121
- parameters: ApiParam[];
122
- returnType: ApiParamType;
123
- modelName: string;
124
- methodName: string;
125
- path: string;
126
- options: ApiDecoratorOptions;
127
- }[] = [];
128
- types: { [typeName: string]: z.ZodObject<any> } = {};
129
- models: { [modelName: string]: unknown } = {};
44
+ apis: LoadedApis = [];
45
+ types: LoadedTypes = {};
46
+ models: LoadedModels = {};
130
47
  isSyncing: boolean = false;
131
48
 
132
- public checksumPatternGroup: GlobPattern = {
133
- /* 원본 체크 */
134
- entity: Sonamu.apiRootPath + "/src/application/**/*.entity.json",
135
- types: Sonamu.apiRootPath + "/src/application/**/*.types.ts",
136
- generated: Sonamu.apiRootPath + "/src/application/sonamu.generated.ts",
137
- functions: Sonamu.apiRootPath + "/src/application/**/*.functions.ts",
138
- /* compiled-JS 체크 */
139
- model: Sonamu.apiRootPath + "/dist/application/**/*.model.js",
140
- frame: Sonamu.apiRootPath + "/dist/application/**/*.frame.js",
141
- };
142
-
143
- get checksumsPath(): string {
144
- return path.join(Sonamu.apiRootPath, "/sonamu.lock");
145
- }
146
- public constructor() {}
147
-
49
+ /**
50
+ * 체크섬이 변경된 부분에 대해 싱크를 진행합니다.
51
+ * 다만 sonamu.shared.ts는 체크섬 비교 없이 무조건 싱크(복사)합니다.
52
+ * @returns
53
+ */
148
54
  async sync(): Promise<void> {
149
55
  const { targets } = Sonamu.config.sync;
150
56
 
151
- // 번들러 여부에 따라 현재 디렉토리가 바뀌므로
152
- const currentDirname = __dirname.endsWith("/syncer")
153
- ? __dirname
154
- : path.join(__dirname, "./syncer");
155
-
156
- // 트리거와 무관하게 shared 분배
157
- await Promise.all(
158
- targets.map(async (target) => {
159
- const srcCodePath = path
160
- .join(currentDirname, `../shared/${target}.shared.ts.txt`)
161
- .replace("/dist/", "/src/");
162
- if (!(await exists(srcCodePath))) {
163
- return;
164
- }
57
+ // sonamu.shared.ts는 무조건 싱크(복사)합니다.
58
+ await this.copySharedToTargets(targets);
165
59
 
166
- const dstCodePath = path.join(
167
- Sonamu.appRootPath,
168
- target,
169
- "src/services/sonamu.shared.ts"
170
- );
60
+ // 다음부터는 변경된 파일을 찾아서 동기화 작업을 실행합니다.
61
+ const changedFiles = await findChangedFilesUsingChecksums();
62
+ if (changedFiles.length === 0) {
63
+ console.log(chalk.black.bgGreen(centerText("All files are synced!")));
64
+ return;
65
+ }
171
66
 
172
- const srcChecksum = await this.getChecksumOfFile(srcCodePath);
173
- const dstChecksum = await (async () => {
174
- if (!(await exists(dstCodePath))) {
175
- return "";
176
- }
177
- return this.getChecksumOfFile(dstCodePath);
178
- })();
67
+ // 만약 싱크 중에 프로세스가 죽으면 꼬여버리기 때문에,
68
+ // 시그널에도 잠시 버틸 있는 환경 속에서 싱크를 실행합니다.
69
+ await runWithGracefulShutdown(
70
+ async () => {
71
+ // 얘가 싱크 작업 수행하는 본체입니다.
72
+ await this.doSyncActions(changedFiles);
179
73
 
180
- if (srcChecksum === dstChecksum) {
181
- return;
182
- }
183
- await writeFile(dstCodePath, await readFile(srcCodePath));
184
- console.log(chalk.blue("shared.ts is synced"));
185
- })
74
+ // 싱크 액션이 끝나면 항상 체크섬을 다시 갱신합니다.
75
+ await renewChecksums();
76
+ },
77
+ { whenThisHappens: "SIGUSR2", waitForUpTo: 20000 }
186
78
  );
79
+ }
187
80
 
188
- // 현재 checksums
189
- let currentChecksums = await this.getCurrentChecksums();
190
- // 이전 checksums
191
- const previousChecksums = await this.getPreviousChecksums();
81
+ /**
82
+ * Watcher가 감지한 파일 변경 사항에 대해 싱크를 진행합니다.
83
+ * 주어진 변경 파일들 중 체크섬 관리 대상인 것들만 가져다가 싱크를 진행합니다.
84
+ * 체크섬 파일 업데이트는 여기에서 하지 않습니다. 호출자가 합니다.
85
+ * @param diffFilePath - 변경 파일들. 프로젝트 루트부터 "src/" 또는 "dist/"로 시작하는 상대 경로입니다. 예시: "src/application/user/user.model.ts"
86
+ */
87
+ async syncFromWatcher(
88
+ event: string,
89
+ diffFilePath: AbsolutePath
90
+ ): Promise<void> {
91
+ if (event !== "change" && event !== "add" && event !== "unlink") {
92
+ return;
93
+ }
192
94
 
193
- // 비교
194
- const isSame = equal(currentChecksums, previousChecksums);
195
- if (isSame) {
196
- const msg = "All files are synced!";
197
- const margin = (process.stdout.columns - msg.length) / 2;
95
+ // 일단 변경된 파일과 dependent 파일들을 invalidate 합니다.
96
+ // 이상 import된 친구들에 대해서만 실제 작업이 일어납니다.
97
+ // 그러니 안심하고 invalidate 해도 됩니다.
98
+ const invalidatedPaths = await hot.invalidateFile(diffFilePath, event);
99
+ if (invalidatedPaths.length > 0) {
198
100
  console.log(
199
- chalk.black.bgGreen(" ".repeat(margin) + msg + " ".repeat(margin))
101
+ chalk.bold(
102
+ `🔄 Invalidated:\n${chalk.blue(invalidatedPaths.map((p) => `- ${path.relative(Sonamu.apiRootPath, p)}`).join("\n"))}`
103
+ )
200
104
  );
201
- return;
202
105
  }
203
106
 
204
- const abc = new AbortController();
205
- this.isSyncing = true;
206
- const onSIGUSR2 = async () => {
207
- if (this.isSyncing === false) {
208
- process.exit(0);
107
+ const isInCheckPatternGroup = Object.values(
108
+ getChecksumPatternGroupInAbsolutePath()
109
+ ).some((pattern) => minimatch(diffFilePath, pattern));
110
+
111
+ // 할 일(sync)이 있으면 합니다.
112
+ if (isInCheckPatternGroup) {
113
+ await this.doSyncActions([diffFilePath]);
114
+ }
115
+
116
+ // 싱크 작업이 끝나면 모든 모듈을 로드합니다.
117
+ // hot-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
118
+ await this.autoloadTypes();
119
+ await this.autoloadModels();
120
+ await this.autoloadApis();
121
+
122
+ this.syncUI();
123
+ }
124
+
125
+ private async copySharedToTargets(targets: string[]): Promise<void> {
126
+ for (const target of targets) {
127
+ // 지금 가져가려는 이 파일은 Sonamu 코드베이스의 일부입니다.
128
+ // 그런데 dist 속 빌드된 소스 코드 파일이 필요한 것이 아니고, src에만 있는 텍스트 파일이 필요합니다.
129
+ // 따라서 /src/에서 찾습니다.
130
+ const srcPath = path.join(
131
+ import.meta.dirname.replace("/dist/", "/src/"),
132
+ `../shared/${target}.shared.ts.txt`
133
+ );
134
+ if (!(await exists(srcPath))) {
135
+ return;
209
136
  }
210
- console.log(chalk.magentaBright(`wait for syncing done....`));
211
-
212
- // 싱크 완료 대기
213
- try {
214
- await setTimeoutPromises(20000, "waiting-sync", { signal: abc.signal });
215
- } catch {}
216
- console.log(chalk.magentaBright(`Syncing DONE!`));
217
- process.exit(0);
218
- };
219
- process.on("SIGUSR2", onSIGUSR2);
220
137
 
221
- // 변경된 파일 찾기
222
- const diff = _.differenceWith(
223
- currentChecksums,
224
- previousChecksums,
225
- _.isEqual
226
- );
227
- const diffFiles = diff.map((r) => r.path);
228
- console.log("Changed Files: ", diffFiles);
138
+ // 이건 프로젝트에 .ts 소스 코드 파일을 생성하는 것이므로 src의 .ts 경로로 갑니다.
139
+ const destPath = path.join(
140
+ Sonamu.appRootPath,
141
+ target,
142
+ "src/services/sonamu.shared.ts"
143
+ );
229
144
 
230
- const { changedChecksums } = await this.doSyncActions(
231
- diffFiles,
232
- currentChecksums
233
- );
234
- // checksum 오버라이드 (액션 실행 과정 중간에 체크섬이 바뀐 경우)
235
- currentChecksums = changedChecksums ?? currentChecksums;
145
+ if (await areFilesSame(srcPath, destPath)) {
146
+ return;
147
+ }
236
148
 
237
- // 저장
238
- await this.saveChecksums(currentChecksums);
149
+ await writeFile(destPath, await readFile(srcPath));
239
150
 
240
- // 싱크 종료
241
- this.isSyncing = false;
242
- abc.abort();
243
- process.off("SIGUSR2", onSIGUSR2);
151
+ console.log(
152
+ chalk.bold("Copied: ") +
153
+ chalk.blue(path.relative(Sonamu.appRootPath, destPath))
154
+ );
155
+ }
244
156
  }
245
157
 
246
- async doSyncActions(
247
- diffFiles: string[],
248
- currentChecksums?: PathAndChecksum[]
249
- ): Promise<{
250
- diffTypes: string[];
251
- changedChecksums?: PathAndChecksum[];
252
- }> {
253
- // 다른 부분 찾아 액션
254
- const diffGroups = _.groupBy(diffFiles, (r) => {
255
- const matched = r.match(
256
- /\.(model|types|functions|entity|generated|frame)\.[tj]s/
257
- );
258
- return matched?.[1] ?? "unknown";
259
- }) as unknown as DiffGroups;
158
+ async autoloadTypes() {
159
+ this.types = await loadTypes();
160
+ }
161
+
162
+ async autoloadModels() {
163
+ this.models = await loadModels();
164
+ }
260
165
 
261
- // 변경된 파일들을 타입별로 분리하여 각 타입별 액션 처리
166
+ async autoloadApis() {
167
+ this.apis = await loadApis();
168
+ }
169
+
170
+ /**
171
+ * 실제 싱크를 수행하는 본체입니다.
172
+ * 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
173
+ * @param diffFilePaths - 변경된 파일들의 절대 경로 목록
174
+ * @returns diffTypes - 변경된 파일의 타입 목록 (entity, types, model 등)
175
+ */
176
+ private async doSyncActions(
177
+ diffFilePaths: AbsolutePath[]
178
+ ): Promise<{ diffTypes: string[] }> {
179
+ const diffGroups = this.calculateDiffGroups(diffFilePaths);
262
180
  const diffTypes = Object.keys(diffGroups);
263
181
 
264
182
  // 트리거: entity, types
265
183
  // 액션: 스키마 생성
266
- if (diffTypes.includes("entity") || diffTypes.includes("types")) {
267
- await EntityManager.reload();
268
-
269
- await this.actionGenerateSchemas();
270
-
271
- // types 생성(entity 새로 추가된 경우)
272
- // parentId가 없고, types가 없는 경우에만 생성
273
- const entityId = this.getEntityIdFromPath([
274
- ...(diffGroups["entity"] ?? []),
275
- ])[0];
276
- if (entityId) {
277
- const entity = EntityManager.get(entityId);
278
- const typeFilePath = path.join(
279
- Sonamu.apiRootPath,
280
- `src/application/${entity.names.fs}/${entity.names.fs}.types.ts`
281
- );
282
- if (entity.parentId === undefined && !(await exists(typeFilePath))) {
283
- await this.generateTemplate("init_types", { entityId });
284
- }
285
- }
286
-
287
- // generated 싱크까지 동시에 처리 후 체크섬 갱신
288
- diffGroups["generated"] = _.uniq([
289
- ...(diffGroups["generated"] ?? []),
290
- "/src/application/sonamu.generated.ts",
291
- ]);
292
- diffTypes.push("generated");
293
-
294
- // fullSync인 경우만 실행
295
- if (currentChecksums) {
296
- currentChecksums = await this.getCurrentChecksums();
297
- }
184
+ if (diffTypes.includes("entity")) {
185
+ await this.handleEntityChange(diffGroups, diffTypes);
298
186
  }
299
187
 
300
188
  // 트리거: types, enums, generated 변경시
@@ -304,202 +192,183 @@ export class Syncer {
304
192
  diffTypes.includes("functions") ||
305
193
  diffTypes.includes("generated")
306
194
  ) {
307
- const tsPaths = _.uniq(
308
- [
309
- ...(diffGroups["types"] ?? []),
310
- ...(diffGroups["functions"] ?? []),
311
- ...(diffGroups["generated"] ?? []),
312
- ].map((p) => p.replace("/dist/", "/src/").replace(".js", ".ts"))
313
- );
314
- await this.actionSyncFilesToTargets(tsPaths);
195
+ await this.handleTypesOrFunctionsOrGeneratedChange(diffGroups);
315
196
  }
316
197
 
317
198
  // 트리거: model
318
199
  if (diffTypes.includes("model") || diffTypes.includes("frame")) {
319
- const mergedGroup = [
320
- ...(diffGroups["model"] ?? []),
321
- ...(diffGroups["frame"] ?? []),
322
- ];
323
-
324
- // registeredApis 초기화
325
- await this.autoloadModels();
326
-
327
- // Syncer.apis 초기화
328
- await this.autoloadApis();
329
-
330
- const params: { namesRecord: EntityNamesRecord; modelTsPath: string }[] =
331
- mergedGroup.map((modelPath) => {
332
- if (modelPath.endsWith(".model.js")) {
333
- const entityId = this.getEntityIdFromPath([modelPath])[0];
334
- assert(entityId);
335
- return {
336
- namesRecord: EntityManager.getNamesFromId(entityId),
337
- modelTsPath: path.join(
338
- Sonamu.apiRootPath,
339
- modelPath
340
- .replace("/dist/", "/src/")
341
- .replace(".model.js", ".model.ts")
342
- ),
343
- };
344
- }
345
- if (modelPath.endsWith("frame.js")) {
346
- const [, frameName] = modelPath.match(/.+\/(.+)\.frame.js$/) ?? [];
347
- assert(frameName);
348
- return {
349
- namesRecord: EntityManager.getNamesFromId(frameName),
350
- modelTsPath: path.join(
351
- Sonamu.apiRootPath,
352
- modelPath
353
- .replace("/dist/", "/src/")
354
- .replace(".frame.js", ".frame.ts")
355
- ),
356
- };
357
- }
358
- throw new Error("not reachable");
359
- });
360
- await this.actionGenerateServices(params);
361
-
362
- await this.actionGenerateHttps();
200
+ await this.handleModelOrFrameChange(diffGroups);
201
+ }
202
+
203
+ // 트리거: config
204
+ if (diffTypes.includes("config")) {
205
+ await this.actionSyncConfig();
363
206
  }
364
207
 
365
208
  return {
366
209
  diffTypes,
367
- changedChecksums: currentChecksums,
368
210
  };
369
211
  }
370
212
 
371
- async syncFromWatcher(diffFiles: string[]): Promise<void> {
372
- const tsFiles = diffFiles.filter((file) => file.endsWith(".ts"));
373
- const jsonFiles = diffFiles.filter((file) => file.endsWith(".json"));
374
-
375
- // transpile (성능 이슈를 고려하여 5개 동시 실행)
376
- const chunks = chunk(tsFiles, 5);
377
- let transpiledFilePaths: string[] = [];
378
- for (const chunk of chunks) {
379
- const _transpiledFilePaths = await Promise.all(
380
- chunk.map(async (diffFile) => {
381
- const { code, map } = await swc.transformFile(diffFile, {
382
- module: {
383
- type: "commonjs",
384
- },
385
- jsc: {
386
- parser: {
387
- syntax: "typescript",
388
- decorators: true,
389
- },
390
- target: "es5",
391
- },
392
- sourceMaps: true,
393
- });
213
+ private calculateDiffGroups(diffFiles: AbsolutePath[]): DiffGroups {
214
+ return _.groupBy(diffFiles, (r) => {
215
+ const matched = r.match(
216
+ /\.(model|types|functions|entity|generated|frame|config)\.[tj]s/
217
+ );
218
+ return matched?.[1] ?? "unknown";
219
+ }) as unknown as DiffGroups;
220
+ }
394
221
 
395
- const jsPath = diffFile
396
- .replace("/src/", "/dist/")
397
- .replace(".ts", ".js");
398
- await mkdir(path.dirname(jsPath), { recursive: true }); // 파일 새로 추가된 경우 디렉토리 생성
399
- await writeFile(jsPath, code);
400
-
401
- if (map) {
402
- const mapPath = jsPath + ".map";
403
- await mkdir(path.dirname(mapPath), { recursive: true });
404
- await writeFile(mapPath, map);
405
-
406
- const sourceMapComment =
407
- "\n//# sourceMappingURL=" + path.basename(mapPath);
408
- await writeFile(jsPath, sourceMapComment, {
409
- flag: "a" /*파일 끝에 붙이기만 해요*/,
410
- });
411
- }
412
-
413
- console.log(
414
- chalk.bold("Transpiled: ") +
415
- chalk.blue(`${jsPath.replace(Sonamu.apiRootPath, "api")}`)
416
- );
417
- return jsPath;
418
- })
222
+ private async handleEntityChange(
223
+ diffGroups: DiffGroups,
224
+ diffTypes: string[]
225
+ ): Promise<void> {
226
+ // console.log(
227
+ // chalk.gray(
228
+ // `[Processing] Handling entity changes: ${diffGroups["entity"]?.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
229
+ // )
230
+ // );
231
+
232
+ await EntityManager.reload();
233
+
234
+ // types 생성(entity 새로 추가된 경우)
235
+ // parentId가 없고, types가 없는 경우에만 생성
236
+ const entityId = EntityManager.getEntityIdFromPath(
237
+ diffGroups["entity"]?.[0]
238
+ );
239
+
240
+ if (entityId) {
241
+ const entity = EntityManager.get(entityId);
242
+ // 프로젝트에 생성되어야 하는 .ts 파일의 경로입니다.
243
+ const typeFilePath = path.join(
244
+ Sonamu.apiRootPath,
245
+ `src/application/${entity.names.fs}/${entity.names.fs}.types.ts`
419
246
  );
420
- transpiledFilePaths.push(..._transpiledFilePaths);
247
+ if (entity.parentId === undefined && !(await exists(typeFilePath))) {
248
+ await generateTemplate("init_types", { entityId });
249
+ }
421
250
  }
422
251
 
423
- // module reload - doSyncActions 전에 캐시 삭제
424
- function clearModuleAndDependents(filePath: string) {
425
- const resolved = require.resolve(filePath);
426
- const toDelete = new Set([resolved]);
252
+ await this.actionGenerateSchemas();
427
253
 
428
- // 파일을 children으로 가진 모듈 찾기
429
- Object.keys(require.cache).forEach((key) => {
430
- const mod = require.cache[key];
431
- if (mod?.children?.some((child) => child.id === resolved)) {
432
- toDelete.add(key);
433
- }
434
- });
254
+ diffGroups["generated"] = _.uniq([
255
+ ...(diffGroups["generated"] ?? []),
256
+ path.join(
257
+ Sonamu.apiRootPath,
258
+ "src/application/sonamu.generated.ts"
259
+ ) as AbsolutePath,
260
+ ]);
261
+ diffTypes.push("generated");
262
+ }
435
263
 
436
- toDelete.forEach((key) => {
437
- if (key.includes("dist/index.js")) {
438
- process.kill(process.pid, "SIGUSR2");
439
- }
440
- delete require.cache[key];
441
- // console.debug(
442
- // chalk.bold("ModuleCleared: ") +
443
- // chalk.blue(`${key.replace(Sonamu.apiRootPath, "api")}`)
444
- // );
445
- });
446
- }
447
- transpiledFilePaths.map((filePath) => {
448
- clearModuleAndDependents(filePath);
449
- });
264
+ private async handleTypesOrFunctionsOrGeneratedChange(
265
+ diffGroups: DiffGroups
266
+ ): Promise<FileType[]> {
267
+ const tsPaths = _.uniq([
268
+ ...(diffGroups["types"] ?? []),
269
+ ...(diffGroups["functions"] ?? []),
270
+ ...(diffGroups["generated"] ?? []),
271
+ ]);
450
272
 
451
- // doSyncActions
452
- const allFilePaths = [...tsFiles, ...transpiledFilePaths, ...jsonFiles];
453
- const targetFilePaths = allFilePaths
454
- .filter((filePath) => {
455
- return Object.values(this.checksumPatternGroup).some((pattern) =>
456
- minimatch(filePath, pattern)
457
- );
458
- })
459
- .map((filePath) => "/" + path.relative(Sonamu.apiRootPath, filePath));
460
- await this.doSyncActions(targetFilePaths);
273
+ // console.log(
274
+ // chalk.gray(
275
+ // `[Processing] Handling types/functions/generated changes: ${tsPaths.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
276
+ // )
277
+ // );
461
278
 
462
- this.apis = [];
463
- this.types = {};
464
- this.models = {};
465
- await this.autoloadTypes();
279
+ await this.actionSyncFilesToTargets(tsPaths);
280
+
281
+ return [];
282
+ }
283
+
284
+ private async handleModelOrFrameChange(
285
+ diffGroups: DiffGroups
286
+ ): Promise<void> {
287
+ const mergedGroup = [
288
+ ...(diffGroups["model"] ?? []),
289
+ ...(diffGroups["frame"] ?? []),
290
+ ];
291
+
292
+ // console.log(
293
+ // chalk.gray(
294
+ // `[Processing] Handling model/frame changes: ${mergedGroup.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
295
+ // )
296
+ // );
297
+
298
+ // generated_http.template.ts에서 syncer.types를 씁니다.
299
+ // service.template.ts에서 syncer.apis를 씁니다.
466
300
  await this.autoloadModels();
301
+ await this.autoloadTypes();
467
302
  await this.autoloadApis();
468
303
 
469
- this.syncUI();
304
+ const params: {
305
+ namesRecord: EntityNamesRecord;
306
+ }[] = mergedGroup.map((modelPath) => {
307
+ if (modelPath.endsWith(".model.ts")) {
308
+ const entityId = EntityManager.getEntityIdFromPath(modelPath);
309
+ assert(entityId);
310
+ return {
311
+ namesRecord: EntityManager.getNamesFromId(entityId),
312
+ };
313
+ }
314
+ if (modelPath.endsWith("frame.ts")) {
315
+ const [, frameName] = modelPath.match(/.+\/(.+)\.frame.js$/) ?? [];
316
+ assert(frameName);
317
+ return {
318
+ namesRecord: EntityManager.getNamesFromId(frameName),
319
+ };
320
+ }
321
+ throw new Error("not reachable");
322
+ });
323
+
324
+ await this.actionGenerateServices(params);
325
+ await this.actionGenerateHttps();
470
326
  }
471
327
 
472
- getEntityIdFromPath(filePaths: string[]): string[] {
473
- return _.uniq(
474
- filePaths.map((p) => {
475
- const matched = p.match(/application\/(.+)\//);
476
- assert(matched && matched[1]);
477
- return inflection.camelize(matched[1].replace(/\-/g, "_"));
328
+ // web/.sonamu.env 현재 설정값 저장
329
+ async actionSyncConfig() {
330
+ const { host, port } = Sonamu.config.server.listen ?? {};
331
+ const content = `API_HOST=${host ?? "localhost"}\nAPI_PORT=${port ?? 3000}`;
332
+
333
+ await Promise.all(
334
+ Sonamu.config.sync.targets.map(async (target) => {
335
+ await writeFile(
336
+ path.join(Sonamu.appRootPath, target, ".sonamu.env"),
337
+ content
338
+ );
478
339
  })
479
340
  );
480
341
  }
481
342
 
482
- async actionGenerateSchemas(): Promise<string[]> {
343
+ /**
344
+ * sonamu.generated.ts와 sonamu.generated.sso.ts를 생성합니다.
345
+ * @returns 생성된 파일 경로 배열.
346
+ */
347
+ private async actionGenerateSchemas(): Promise<AbsolutePath[]> {
483
348
  return (
484
349
  await Promise.all([
485
- this.generateTemplate("generated_sso", {}, { overwrite: true }),
486
- this.generateTemplate("generated", {}, { overwrite: true }),
350
+ generateTemplate("generated_sso", {}, { overwrite: true }),
351
+ generateTemplate("generated", {}, { overwrite: true }),
487
352
  ])
488
353
  )
489
354
  .flat()
490
355
  .flat();
491
356
  }
492
357
 
493
- async actionGenerateServices(
358
+ /**
359
+ * *.service.ts를 생성합니다.
360
+ * @param paramsArray
361
+ * @returns 생성된 파일 경로 배열.
362
+ */
363
+ private async actionGenerateServices(
494
364
  paramsArray: {
495
365
  namesRecord: EntityNamesRecord;
496
- modelTsPath: string;
497
366
  }[]
498
367
  ): Promise<string[]> {
499
368
  return (
500
369
  await Promise.all(
501
370
  paramsArray.map(async (params) =>
502
- this.generateTemplate("service", params, {
371
+ generateTemplate("service", params, {
503
372
  overwrite: true,
504
373
  })
505
374
  )
@@ -509,8 +378,12 @@ export class Syncer {
509
378
  .flat();
510
379
  }
511
380
 
512
- async actionGenerateHttps(): Promise<string[]> {
513
- const [res] = await this.generateTemplate(
381
+ /**
382
+ * sonamu.generated.http를 생성합니다.
383
+ * @returns 생성된 파일 경로.
384
+ */
385
+ private async actionGenerateHttps(): Promise<AbsolutePath> {
386
+ const [res] = await generateTemplate(
514
387
  "generated_http",
515
388
  {},
516
389
  { overwrite: true }
@@ -519,29 +392,14 @@ export class Syncer {
519
392
  return res;
520
393
  }
521
394
 
522
- async copyFileWithReplaceCoreToShared(fromPath: string, toPath: string) {
523
- if (!(await exists(fromPath))) {
524
- return;
525
- }
526
-
527
- const oldFileContent = (await readFile(fromPath)).toString();
528
-
529
- const newFileContent = (() => {
530
- const nfc = oldFileContent.replace(
531
- /from "sonamu"/g,
532
- `from "src/services/sonamu.shared"`
533
- );
534
-
535
- if (toPath.includes("/web/")) {
536
- return nfc.replace(/from "lodash";/g, `from "lodash-es";`);
537
- } else {
538
- return nfc;
539
- }
540
- })();
541
- return writeFile(toPath, newFileContent);
542
- }
543
-
544
- async actionSyncFilesToTargets(tsPaths: string[]): Promise<string[]> {
395
+ /**
396
+ * *.types.ts, *.functions.ts, *.generated.ts를 타겟 디렉토리에 복사합니다.
397
+ * @param tsPaths
398
+ * @returns 복사된 파일 경로 배열.
399
+ */
400
+ private async actionSyncFilesToTargets(
401
+ tsPaths: AbsolutePath[]
402
+ ): Promise<string[]> {
545
403
  const { targets } = Sonamu.config.sync;
546
404
  const { dir: apiDir } = Sonamu.config.api;
547
405
 
@@ -549,8 +407,7 @@ export class Syncer {
549
407
  await Promise.all(
550
408
  targets.map(async (target) =>
551
409
  Promise.all(
552
- tsPaths.map(async (src) => {
553
- const realSrc = Sonamu.apiRootPath + src;
410
+ tsPaths.map(async (realSrc) => {
554
411
  const dst = realSrc
555
412
  .replace(`/${apiDir}/`, `/${target}/`)
556
413
  .replace("/application/", "/services/");
@@ -560,9 +417,7 @@ export class Syncer {
560
417
  }
561
418
  console.log(
562
419
  chalk.bold("Copied: ") +
563
- chalk.blue(
564
- `Copied: ${dst.replace(Sonamu.appRootPath + "/", "")}`
565
- )
420
+ chalk.blue(dst.replace(Sonamu.appRootPath + "/", ""))
566
421
  );
567
422
  await this.copyFileWithReplaceCoreToShared(realSrc, dst);
568
423
  return dst;
@@ -573,671 +428,49 @@ export class Syncer {
573
428
  ).flat();
574
429
  }
575
430
 
576
- async getCurrentChecksums(): Promise<PathAndChecksum[]> {
577
- const filePaths = (
578
- await Promise.all(
579
- Object.entries(this.checksumPatternGroup).map(
580
- async ([_fileType, pattern]) => {
581
- return globAsync(pattern);
582
- }
583
- )
584
- )
585
- )
586
- .flat()
587
- .sort();
588
-
589
- const fileChecksums: {
590
- path: string;
591
- checksum: string;
592
- }[] = await Promise.all(
593
- filePaths.map(async (filePath) => {
594
- return {
595
- path: filePath.substring(Sonamu.apiRootPath.length),
596
- checksum: await this.getChecksumOfFile(filePath),
597
- };
598
- })
599
- );
600
- return fileChecksums;
601
- }
602
-
603
- async getPreviousChecksums(): Promise<PathAndChecksum[]> {
604
- if (!(await exists(this.checksumsPath))) {
605
- return [];
606
- }
607
-
608
- const previousChecksums = JSON.parse(
609
- (await readFile(this.checksumsPath, "utf-8"))
610
- ) as PathAndChecksum[];
611
- return previousChecksums;
612
- }
613
-
614
- async saveChecksums(checksums: PathAndChecksum[]): Promise<void> {
615
- await writeFile(
616
- this.checksumsPath,
617
- JSON.stringify(checksums, null, 2),
618
- "utf-8"
619
- );
620
- console.log("checksum saved", this.checksumsPath);
621
- }
622
-
623
- async getChecksumOfFile(filePath: string): Promise<string> {
624
- return new Promise<string>((resolve, reject) => {
625
- const hash = crypto.createHash("sha1");
626
- const input = createReadStream(filePath);
627
- input.on("error", reject);
628
- input.on("data", function (chunk: any) {
629
- hash.update(chunk);
630
- });
631
- input.on("close", function () {
632
- resolve(hash.digest("hex"));
633
- });
634
- });
635
- }
636
-
637
- async readApisFromFile(filePath: string) {
638
- const sourceFile = ts.createSourceFile(
639
- filePath,
640
- (await readFile(filePath)).toString(),
641
- ts.ScriptTarget.Latest
642
- );
643
-
644
- const methods: Omit<ExtendedApi, "path" | "options">[] = [];
645
- let modelName: string = "UnknownModel";
646
- let methodName: string = "unknownMethod";
647
- const visitor = (node: ts.Node) => {
648
- if (ts.isClassDeclaration(node)) {
649
- if (node.name && ts.isIdentifier(node.name)) {
650
- modelName = node.name.escapedText.toString().replace(/Class$/, "");
651
- }
652
- }
653
- if (ts.isMethodDeclaration(node)) {
654
- if (ts.isIdentifier(node.name)) {
655
- methodName = node.name.escapedText.toString();
656
- }
657
-
658
- const typeParameters: ApiParamType.TypeParam[] = (
659
- node.typeParameters ?? []
660
- ).map((typeParam) => {
661
- const tp = typeParam as ts.TypeParameterDeclaration;
662
-
663
- return {
664
- t: "type-param",
665
- id: tp.name.escapedText.toString(),
666
- constraint: tp.constraint
667
- ? this.resolveTypeNode(tp.constraint)
668
- : undefined,
669
- };
670
- });
671
- const parameters: ApiParam[] = node.parameters.map(
672
- (paramDec, index) => {
673
- const defaultDef = this.printNode(paramDec.initializer, sourceFile);
674
-
675
- // 기본값이 있는 경우 paramDec.type가 undefined로 나옴
676
-
677
- return this.resolveParamDec(
678
- {
679
- name: paramDec.name,
680
- type: paramDec.type as ts.TypeNode,
681
- optional:
682
- paramDec.questionToken !== undefined ||
683
- paramDec.initializer !== undefined,
684
- defaultDef,
685
- },
686
- index
687
- );
688
- }
689
- );
690
- if (node.type === undefined) {
691
- throw new Error(
692
- `리턴 타입이 기재되지 않은 메소드 ${modelName}.${methodName}`
693
- );
694
- }
695
- const returnType = this.resolveTypeNode(node.type!);
696
-
697
- methods.push({
698
- modelName,
699
- methodName,
700
- typeParameters,
701
- parameters,
702
- returnType,
703
- });
704
- }
705
- ts.forEachChild(node, visitor);
706
- };
707
- visitor(sourceFile);
708
-
709
- if (methods.length === 0) {
710
- return [];
711
- }
712
-
713
- // 현재 파일의 등록된 API 필터
714
- const currentModelApis = registeredApis.filter((api) => {
715
- return methods.find(
716
- (method) =>
717
- method.modelName === api.modelName &&
718
- method.methodName === api.methodName
719
- );
720
- });
721
- if (currentModelApis.length === 0) {
722
- // const p = path.join(tmpdir(), "sonamu-syncer-error.json");
723
- // writeFileSync(p, JSON.stringify(registeredApis, null, 2));
724
- // execSync(`open ${p}`);
725
- throw new Error(`현재 파일에 사전 등록된 API가 없습니다. ${filePath}`);
726
- }
727
-
728
- // 등록된 API에 현재 메소드 타입 정보 확장
729
- const extendedApis = currentModelApis.map((api) => {
730
- const foundMethod = methods.find(
731
- (method) =>
732
- method.modelName === api.modelName &&
733
- method.methodName === api.methodName
734
- );
735
- return {
736
- ...api,
737
- typeParameters: foundMethod!.typeParameters,
738
- parameters: foundMethod!.parameters,
739
- returnType: foundMethod!.returnType,
740
- };
741
- });
742
- return extendedApis;
743
- }
744
-
745
- resolveTypeNode(typeNode: ts.TypeNode): ApiParamType {
746
- switch (typeNode?.kind) {
747
- case ts.SyntaxKind.AnyKeyword:
748
- return "any";
749
- case ts.SyntaxKind.UnknownKeyword:
750
- return "unknown";
751
- case ts.SyntaxKind.StringKeyword:
752
- return "string";
753
- case ts.SyntaxKind.NumberKeyword:
754
- return "number";
755
- case ts.SyntaxKind.BooleanKeyword:
756
- return "boolean";
757
- case ts.SyntaxKind.UndefinedKeyword:
758
- return "undefined";
759
- case ts.SyntaxKind.NullKeyword:
760
- return "null";
761
- case ts.SyntaxKind.VoidKeyword:
762
- return "void";
763
- case ts.SyntaxKind.LiteralType:
764
- const literal = (typeNode as ts.LiteralTypeNode).literal;
765
- if (ts.isStringLiteral(literal)) {
766
- return {
767
- t: "string-literal",
768
- value: literal.text,
769
- };
770
- } else if (ts.isNumericLiteral(literal)) {
771
- return {
772
- t: "numeric-literal",
773
- value: Number(literal.text),
774
- };
775
- } else {
776
- if (literal.kind === ts.SyntaxKind.NullKeyword) {
777
- return "null";
778
- } else if (literal.kind === ts.SyntaxKind.UndefinedKeyword) {
779
- return "undefined";
780
- } else if (literal.kind === ts.SyntaxKind.TrueKeyword) {
781
- return "true";
782
- } else if (literal.kind === ts.SyntaxKind.FalseKeyword) {
783
- return "false";
784
- }
785
- throw new Error("알 수 없는 리터럴");
786
- }
787
- case ts.SyntaxKind.ArrayType:
788
- const arrNode = typeNode as ts.ArrayTypeNode;
789
- return {
790
- t: "array",
791
- elementsType: this.resolveTypeNode(arrNode.elementType),
792
- };
793
- case ts.SyntaxKind.TypeLiteral:
794
- const literalNode = typeNode as ts.TypeLiteralNode;
795
- return {
796
- t: "object",
797
- props: literalNode.members.map((member) => {
798
- if (ts.isIndexSignatureDeclaration(member)) {
799
- assert(member.parameters[0]);
800
- const res = this.resolveParamDec({
801
- name: member.parameters[0].name as ts.Identifier,
802
- type: member.parameters[0].type as ts.TypeNode,
803
- });
804
-
805
- return this.resolveParamDec({
806
- name: {
807
- escapedText: `[${res.name}${res.optional ? "?" : ""}: ${
808
- res.type
809
- }]`,
810
- } as ts.Identifier,
811
- type: member.type as ts.TypeNode,
812
- });
813
- } else {
814
- return this.resolveParamDec({
815
- name: (member as ts.PropertySignature).name as ts.Identifier,
816
- type: (member as ts.PropertySignature).type as ts.TypeNode,
817
- optional:
818
- (member as ts.PropertySignature).questionToken !== undefined,
819
- });
820
- }
821
- }),
822
- };
823
- case ts.SyntaxKind.TypeReference:
824
- return {
825
- t: "ref",
826
- id: (
827
- (typeNode as ts.TypeReferenceNode).typeName as ts.Identifier
828
- ).escapedText.toString(),
829
- args: (typeNode as ts.TypeReferenceNode).typeArguments?.map(
830
- (typeArg) => this.resolveTypeNode(typeArg)
831
- ),
832
- };
833
- case ts.SyntaxKind.UnionType:
834
- return {
835
- t: "union",
836
- types: (typeNode as ts.UnionTypeNode).types.map((type) =>
837
- this.resolveTypeNode(type)
838
- ),
839
- };
840
- case ts.SyntaxKind.IntersectionType:
841
- return {
842
- t: "intersection",
843
- types: (typeNode as ts.IntersectionTypeNode).types.map((type) =>
844
- this.resolveTypeNode(type)
845
- ),
846
- };
847
- case ts.SyntaxKind.IndexedAccessType:
848
- return {
849
- t: "indexed-access",
850
- object: this.resolveTypeNode(
851
- (typeNode as ts.IndexedAccessTypeNode).objectType
852
- ),
853
- index: this.resolveTypeNode(
854
- (typeNode as ts.IndexedAccessTypeNode).indexType
855
- ),
856
- };
857
- case ts.SyntaxKind.TupleType:
858
- if (ts.isTupleTypeNode(typeNode)) {
859
- return {
860
- t: "tuple-type",
861
- elements: typeNode.elements.map((elem) =>
862
- this.resolveTypeNode(elem)
863
- ),
864
- };
865
- }
866
- break;
867
- case undefined:
868
- throw new Error(`typeNode undefined`);
869
- }
870
-
871
- console.debug(typeNode);
872
- throw new Error(`알 수 없는 SyntaxKind ${typeNode.kind}`);
873
- }
874
-
875
- resolveParamDec = (
876
- paramDec: {
877
- name: ts.BindingName;
878
- type: ts.TypeNode;
879
- optional?: boolean;
880
- defaultDef?: string;
881
- },
882
- index: number = 0
883
- ): ApiParam => {
884
- const name = paramDec.name as ts.Identifier;
885
- const type = this.resolveTypeNode(paramDec.type);
886
-
887
- if (name === undefined) {
888
- console.debug({ name, type, paramDec });
889
- }
890
-
891
- const result: ApiParam = {
892
- name: name.escapedText ? name.escapedText.toString() : `nonameAt${index}`,
893
- type,
894
- optional: paramDec.optional === true,
895
- defaultDef: paramDec?.defaultDef,
896
- };
897
-
898
- // 구조분해할당의 경우 타입이름 사용
899
- if (
900
- ts.isObjectBindingPattern(name) &&
901
- ts.isTypeReferenceNode(paramDec.type) &&
902
- ts.isIdentifier(paramDec.type.typeName)
903
- ) {
904
- result.name = inflection.camelize(paramDec.type.typeName.text, true);
905
- }
906
-
907
- return result;
908
- };
909
-
910
- printNode(
911
- node: ts.Node | undefined,
912
- sourceFile: ts.SourceFile
913
- ): string | undefined {
914
- if (node === undefined) {
915
- return undefined;
916
- }
917
-
918
- const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
919
- return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
920
- }
921
-
922
- async autoloadApis() {
923
- const pathPattern = path.join(
924
- Sonamu.apiRootPath,
925
- "/src/application/**/*.{model,frame}.ts"
926
- );
927
- // console.debug(chalk.yellow(`autoload:APIs @ ${pathPattern}`));
928
-
929
- const filePaths = await globAsync(pathPattern);
930
- const result = await Promise.all(
931
- filePaths.map((filePath) => this.readApisFromFile(filePath))
932
- );
933
- this.apis = result.flat();
934
- return this.apis;
935
- }
936
-
937
- async autoloadModels(): Promise<{ [modelName: string]: unknown }> {
938
- const pathPattern = path.join(
939
- Sonamu.apiRootPath,
940
- "dist/application/**/*.{model,frame}.js"
941
- );
942
- // console.debug(chalk.yellow(`autoload:models @ ${pathPattern}`));
943
-
944
- const filePaths = await filterAsync(
945
- await globAsync(pathPattern),
946
- async (path) => {
947
- // src 디렉터리 내에 있는 해당 파일이 존재할 경우에만 로드
948
- // 삭제된 파일이지만 dist에 남아있는 경우 BaseSchema undefined 에러 방지
949
- const srcPath = path.replace("/dist/", "/src/").replace(".js", ".ts");
950
- return await exists(srcPath);
951
- }
952
- );
953
- const modules = await importMultiple(filePaths);
954
- const functions = modules
955
- .map(({ imported }) => Object.entries(imported))
956
- .flat();
957
- this.models = Object.fromEntries(
958
- functions.filter(
959
- ([name]) => name.endsWith("Model") || name.endsWith("Frame")
960
- )
961
- );
962
- return this.models;
963
- }
964
-
965
- async autoloadTypes(
966
- doRefresh: boolean = false
967
- ): Promise<{ [typeName: string]: z.ZodObject<any> }> {
968
- if (!doRefresh && Object.keys(this.types).length > 0) {
969
- return this.types;
970
- }
971
-
972
- const pathPatterns = [
973
- path.join(Sonamu.apiRootPath, "/dist/application/**/*.types.js"),
974
- path.join(Sonamu.apiRootPath, "/dist/application/**/*.generated.js"),
975
- ];
976
- // console.debug(chalk.magenta(`autoload:types @ ${pathPatterns.join("\n")}`));
977
-
978
- const filePaths = await filterAsync(
979
- (await mapAsync(pathPatterns, globAsync)).flat(),
980
- async (path) => {
981
- // src 디렉터리 내에 있는 해당 파일이 존재할 경우에만 로드
982
- // 삭제된 파일이지만 dist에 남아있는 경우 BaseSchema undefined 에러 방지
983
- const srcPath = path.replace("/dist/", "/src/").replace(".js", ".ts");
984
- return await exists(srcPath);
985
- }
986
- );
987
- const modules = await importMultiple(filePaths, doRefresh);
988
- const functions = modules
989
- .map(({ imported }) => Object.entries(imported))
990
- .flat();
991
- this.types = Object.fromEntries(
992
- functions.filter(([, f]) => f instanceof z.ZodType)
993
- ) as typeof this.types;
994
- return this.types;
995
- }
996
-
997
- getTemplate(key: TemplateKey): Template {
998
- if (key === "entity") {
999
- return new Template__entity();
1000
- } else if (key === "init_types") {
1001
- return new Template__init_types();
1002
- } else if (key === "generated") {
1003
- return new Template__generated();
1004
- } else if (key === "generated_sso") {
1005
- return new Template__generated_sso();
1006
- } else if (key === "generated_http") {
1007
- return new Template__generated_http();
1008
- } else if (key === "model") {
1009
- return new Template__model();
1010
- } else if (key === "model_test") {
1011
- return new Template__model_test();
1012
- } else if (key === "service") {
1013
- return new Template__service();
1014
- } else if (key === "view_list") {
1015
- return new Template__view_list();
1016
- } else if (key === "view_list_columns") {
1017
- return new Template__view_list_columns();
1018
- } else if (key === "view_search_input") {
1019
- return new Template__view_search_input();
1020
- } else if (key === "view_form") {
1021
- return new Template__view_form();
1022
- } else if (key === "view_id_all_select") {
1023
- return new Template__view_id_all_select();
1024
- } else if (key === "view_id_async_select") {
1025
- return new Template__view_id_async_select();
1026
- } else if (key === "view_enums_select") {
1027
- return new Template__view_enums_select();
1028
- } else if (key === "view_enums_dropdown") {
1029
- return new Template__view_enums_dropdown();
1030
- } else if (key === "view_enums_buttonset") {
1031
- return new Template__view_enums_buttonset();
1032
- } else {
1033
- throw new BadRequestException(`잘못된 템플릿 키 ${key}`);
1034
- }
1035
- }
1036
-
1037
- async renderTemplate<T extends keyof TemplateOptions>(
1038
- key: T,
1039
- options: TemplateOptions[T]
1040
- ): Promise<PathAndCode[]> {
1041
- const template: Template = this.getTemplate(key);
1042
-
1043
- let extra: unknown[] = [];
1044
- if (key === "service") {
1045
- // service 필요 정보 (API 리스트)
1046
- const { modelTsPath } = options as TemplateOptions["service"];
1047
- extra = [await this.readApisFromFile(modelTsPath)];
1048
- } else if (["model", "view_list", "view_form"].includes(key)) {
1049
- const entityId = (options as TemplateOptions["model"]).entityId;
1050
- if (key === "view_list" || key === "model") {
1051
- // view_list 필요 정보 (컬럼 노드, 리스트파라미터 노드)
1052
- const columnsNode = await this.getColumnsNode(entityId, "A");
1053
- const listParamsZodType = await this.getZodTypeById(
1054
- `${entityId}ListParams`
1055
- );
1056
- const listParamsNode = this.zodTypeToRenderingNode(listParamsZodType);
1057
- extra = [columnsNode, listParamsNode];
1058
- } else if (key === "view_form") {
1059
- // view_form 필요 정보 (세이브파라미터 노드)
1060
- const saveParamsZodType = await this.getZodTypeById(
1061
- `${entityId}SaveParams`
1062
- );
1063
- const saveParamsNode = this.zodTypeToRenderingNode(saveParamsZodType);
1064
- extra = [saveParamsNode];
1065
- }
1066
- }
1067
-
1068
- const rendered = await template.render(options, ...extra);
1069
- const resolved = await this.resolveRenderedTemplate(key, rendered);
1070
-
1071
- let preTemplateResolved: PathAndCode[] = [];
1072
- if (rendered.preTemplates) {
1073
- preTemplateResolved = (
1074
- await Promise.all(
1075
- rendered.preTemplates.map(({ key, options }) => {
1076
- return this.renderTemplate(key, options);
1077
- })
1078
- )
1079
- ).flat();
431
+ private async copyFileWithReplaceCoreToShared(
432
+ fromPath: string,
433
+ toPath: string
434
+ ) {
435
+ if (!(await exists(fromPath))) {
436
+ return;
1080
437
  }
1081
438
 
1082
- return [resolved, ...preTemplateResolved];
1083
- }
439
+ const oldFileContent = (await readFile(fromPath)).toString();
1084
440
 
1085
- async resolveRenderedTemplate(
1086
- key: TemplateKey,
1087
- result: RenderedTemplate
1088
- ): Promise<PathAndCode> {
1089
- const { target, path: filePath, body, importKeys, customHeaders } = result;
1090
-
1091
- // import 할 대상의 대상 path 추출
1092
- const importDefs = importKeys
1093
- .reduce(
1094
- (r, importKey) => {
1095
- const modulePath = EntityManager.getModulePath(importKey);
1096
- let importPath = modulePath;
1097
- if (modulePath.includes("/") || modulePath.includes(".")) {
1098
- importPath = wrapIf(
1099
- path.relative(path.dirname(filePath), modulePath),
1100
- (p) => [p.startsWith(".") === false, "./" + p]
1101
- );
1102
- }
1103
-
1104
- // 같은 파일에서 import 하는 경우 keys 로 나열 처리
1105
- const existsOne = r.find(
1106
- (importDef) => importDef.from === importPath
1107
- );
1108
- if (existsOne) {
1109
- existsOne.keys = _.uniq(existsOne.keys.concat(importKey));
1110
- } else {
1111
- r.push({
1112
- keys: [importKey],
1113
- from: importPath,
1114
- });
1115
- }
1116
- return r;
1117
- },
1118
- [] as {
1119
- keys: string[];
1120
- from: string;
1121
- }[]
1122
- )
1123
- // 셀프 참조 방지
1124
- .filter(
1125
- (importDef) =>
1126
- filePath.endsWith(importDef.from.replace("./", "") + ".ts") === false
441
+ const newFileContent = (() => {
442
+ const nfc = oldFileContent.replace(
443
+ /from "sonamu"/g,
444
+ `from "src/services/sonamu.shared"`
1127
445
  );
1128
446
 
1129
- // 커스텀 헤더 포함하여 헤더 생성
1130
- const header = [
1131
- ...(customHeaders ?? []),
1132
- ...importDefs.map(
1133
- (importDef) =>
1134
- `import { ${importDef.keys.join(", ")} } from '${importDef.from}'`
1135
- ),
1136
- ].join("\n");
1137
-
1138
- const formatted = await (async () => {
1139
- if (key === "generated_http") {
1140
- return [header, body].join("\n\n");
1141
- } else {
1142
- return prettier.format([header, body].join("\n\n"), {
1143
- parser: key === "entity" ? "json" : "typescript",
1144
- });
1145
- }
1146
- })();
1147
-
1148
- return {
1149
- path: target + "/" + filePath,
1150
- code: formatted,
1151
- };
1152
- }
1153
-
1154
- async writeCodeToPath(pathAndCode: PathAndCode): Promise<string[]> {
1155
- const { targets } = Sonamu.config.sync;
1156
- const { appRootPath } = Sonamu;
1157
- const filePath = `${Sonamu.appRootPath}/${pathAndCode.path}`;
1158
-
1159
- const dstFilePaths = _.uniq(
1160
- targets.map((target) => filePath.replace("/:target/", `/${target}/`))
1161
- );
1162
- return await Promise.all(
1163
- dstFilePaths.map(async (dstFilePath) => {
1164
- const dir = path.dirname(dstFilePath);
1165
- if (!(await exists(dir))) {
1166
- await mkdir(dir, { recursive: true });
1167
- }
1168
- await writeFile(dstFilePath, pathAndCode.code);
1169
- console.log(
1170
- chalk.bold("Generated: ") +
1171
- chalk.blue(`${dstFilePath.replace(appRootPath + "/", "")}`)
1172
- );
1173
- return dstFilePath;
1174
- })
1175
- );
1176
- }
1177
-
1178
- async generateTemplate(
1179
- key: TemplateKey,
1180
- templateOptions: any,
1181
- _generateOptions?: GenerateOptions
1182
- ) {
1183
- const generateOptions = {
1184
- overwrite: false,
1185
- ..._generateOptions,
1186
- };
1187
-
1188
- // 키 children
1189
- const keys: TemplateKey[] = [key];
1190
-
1191
- // 템플릿 렌더
1192
- const pathAndCodes = (
1193
- await Promise.all(
1194
- keys.map(async (key) => {
1195
- return await this.renderTemplate(key, templateOptions);
1196
- })
1197
- )
1198
- ).flat();
1199
-
1200
- const filteredPathAndCodes: PathAndCode[] = await (async () => {
1201
- if (generateOptions.overwrite === true) {
1202
- return pathAndCodes;
447
+ if (toPath.includes("/web/")) {
448
+ return nfc; // .replace(/from "lodash";/g, `from "lodash-es";`); // TODO 흠? 필요없을듯.
1203
449
  } else {
1204
- return await filterAsync(pathAndCodes, async (pathAndCode) => {
1205
- const { targets } = Sonamu.config.sync;
1206
- const filePath = `${Sonamu.appRootPath}/${pathAndCode.path}`;
1207
- const dstFilePaths = targets.map((target) =>
1208
- filePath.replace("/:target/", `/${target}/`)
1209
- );
1210
- return await everyAsync(
1211
- dstFilePaths,
1212
- async (dstPath) => !(await exists(dstPath))
1213
- );
1214
- });
450
+ return nfc;
1215
451
  }
1216
452
  })();
1217
- if (filteredPathAndCodes.length === 0) {
1218
- throw new AlreadyProcessedException(
1219
- "이미 경로에 모든 파일이 존재합니다."
1220
- );
1221
- }
1222
-
1223
- return Promise.all(
1224
- filteredPathAndCodes.map((pathAndCode) =>
1225
- this.writeCodeToPath(pathAndCode)
1226
- )
1227
- );
453
+ return writeFile(toPath, newFileContent);
1228
454
  }
1229
455
 
456
+ /**
457
+ * 주어진 엔티티와 템플릿 키에 대해, 생성된 코드가 존재하는지 확인합니다.
458
+ * @param entityId 엔티티 ID
459
+ * @param templateKey 템플릿 키
460
+ * @param enumId 열거형 ID
461
+ * @returns 생성된 코드가 존재하는지 여부
462
+ */
1230
463
  async checkExistsGenCode(
1231
464
  entityId: string,
1232
465
  templateKey: TemplateKey,
1233
466
  enumId?: string
1234
467
  ): Promise<{ subPath: string; fullPath: string; isExists: boolean }> {
1235
- const { target, path: genPath } = this.getTemplate(
468
+ const { target, path: genPath } = Template.find(
1236
469
  templateKey
1237
470
  ).getTargetAndPath(EntityManager.getNamesFromId(entityId), enumId);
1238
471
 
1239
- const fullPath = path.join(Sonamu.appRootPath, target, genPath);
1240
472
  const subPath = path.join(target, genPath);
473
+ const fullPath = path.join(Sonamu.appRootPath, subPath);
1241
474
  return {
1242
475
  subPath,
1243
476
  fullPath,
@@ -1245,6 +478,12 @@ export class Syncer {
1245
478
  };
1246
479
  }
1247
480
 
481
+ /**
482
+ * 주어진 엔티티와 열거형에 대해, 생성된 코드가 존재하는지 확인합니다.
483
+ * @param entityId 엔티티 ID
484
+ * @param enums 열거형 레이블
485
+ * @returns 생성된 코드가 존재하는지 여부
486
+ */
1248
487
  async checkExists(
1249
488
  entityId: string,
1250
489
  enums: {
@@ -1260,7 +499,7 @@ export class Syncer {
1260
499
  return await reduceAsync(
1261
500
  keys,
1262
501
  async (result, key) => {
1263
- const tpl = this.getTemplate(key);
502
+ const tpl = Template.find(key);
1264
503
  if (key.startsWith("view_enums")) {
1265
504
  await mapAsync(enumsKeys, async (componentId) => {
1266
505
  const { target, path: p } = tpl.getTargetAndPath(
@@ -1292,350 +531,54 @@ export class Syncer {
1292
531
  );
1293
532
  }
1294
533
 
1295
- async getZodTypeById(zodTypeId: string): Promise<z.ZodTypeAny> {
1296
- const modulePath = EntityManager.getModulePath(zodTypeId);
1297
- const moduleAbsPath = path.join(
1298
- Sonamu.apiRootPath,
1299
- "dist",
1300
- "application",
1301
- modulePath + ".js"
1302
- );
1303
- const importPath = "./" + path.relative(__dirname, moduleAbsPath);
1304
- const imported = await import(importPath);
1305
-
1306
- if (!imported[zodTypeId]) {
1307
- throw new Error(`존재하지 않는 zodTypeId ${zodTypeId}`);
1308
- }
1309
- return imported[zodTypeId].describe(zodTypeId);
1310
- }
1311
-
1312
- async propNodeToZodType(propNode: EntityPropNode): Promise<z.ZodTypeAny> {
1313
- if (propNode.nodeType === "plain") {
1314
- return this.propToZodType(propNode.prop);
1315
- } else if (propNode.nodeType === "array") {
1316
- if (propNode.prop === undefined) {
1317
- throw new Error();
1318
- } else if (propNode.children.length > 0) {
1319
- return (
1320
- await this.propNodeToZodType({
1321
- ...propNode,
1322
- nodeType: "object",
1323
- })
1324
- ).array();
1325
- } else {
1326
- const innerType = await this.propToZodType(propNode.prop);
1327
- if (propNode.prop.nullable === true) {
1328
- return z.array(innerType).nullable();
1329
- } else {
1330
- return z.array(innerType);
1331
- }
1332
- }
1333
- } else if (propNode.nodeType === "object") {
1334
- const obj = await propNode.children.reduce(
1335
- async (promise, childPropNode) => {
1336
- const result = await promise;
1337
- result[childPropNode.prop!.name] =
1338
- await this.propNodeToZodType(childPropNode);
1339
- return result;
1340
- },
1341
- {} as any
1342
- );
1343
-
1344
- if (propNode.prop?.nullable === true) {
1345
- return z.object(obj).nullable();
1346
- } else {
1347
- return z.object(obj);
1348
- }
1349
- } else {
1350
- throw Error;
1351
- }
1352
- }
1353
- async propToZodType(prop: EntityProp): Promise<z.ZodTypeAny> {
1354
- let zodType: z.ZodTypeAny = z.unknown();
1355
- if (isIntegerProp(prop)) {
1356
- zodType = z.number().int();
1357
- } else if (isBigIntegerProp(prop)) {
1358
- zodType = z.bigint();
1359
- } else if (isTextProp(prop)) {
1360
- zodType = z.string().max(getTextTypeLength(prop.textType));
1361
- } else if (isEnumProp(prop)) {
1362
- zodType = await this.getZodTypeById(prop.id);
1363
- } else if (isStringProp(prop)) {
1364
- zodType = z.string().max(prop.length);
1365
- } else if (isFloatProp(prop) || isDoubleProp(prop)) {
1366
- zodType = z.number();
1367
- } else if (isDecimalProp(prop)) {
1368
- zodType = z.string();
1369
- } else if (isBooleanProp(prop)) {
1370
- zodType = z.boolean();
1371
- } else if (isDateProp(prop)) {
1372
- zodType = z.string().length(10);
1373
- } else if (isTimeProp(prop)) {
1374
- zodType = z.string().length(8);
1375
- } else if (isDateTimeProp(prop)) {
1376
- zodType = z.date();
1377
- } else if (isTimestampProp(prop)) {
1378
- zodType = z.date();
1379
- } else if (isJsonProp(prop)) {
1380
- zodType = await this.getZodTypeById(prop.id);
1381
- } else if (isUuidProp(prop)) {
1382
- zodType = z.uuid();
1383
- } else if (isVirtualProp(prop)) {
1384
- zodType = await this.getZodTypeById(prop.id);
1385
- } else if (isRelationProp(prop)) {
1386
- if (
1387
- isBelongsToOneRelationProp(prop) ||
1388
- (isOneToOneRelationProp(prop) && prop.hasJoinColumn)
1389
- ) {
1390
- zodType = z.number().int();
1391
- }
1392
- } else {
1393
- throw new Error(`prop을 zodType으로 변환하는데 실패 ${prop}}`);
1394
- }
1395
-
1396
- if ((prop as { unsigned?: boolean }).unsigned) {
1397
- zodType = (zodType as z.ZodNumber).nonnegative();
1398
- }
1399
- if (prop.nullable) {
1400
- zodType = zodType.nullable();
1401
- }
1402
-
1403
- return zodType;
1404
- }
1405
-
1406
- resolveRenderType(
1407
- key: string,
1408
- zodType: z.ZodTypeAny
1409
- ): RenderingNode["renderType"] {
1410
- if (zodType instanceof z.ZodDate) {
1411
- return "datetime";
1412
- } else if (zodType instanceof z.ZodString) {
1413
- if (key.includes("img") || key.includes("image")) {
1414
- return "string-image";
1415
- } else if (zodType.description === "SQLDateTimeString") {
1416
- return "string-datetime";
1417
- } else if (key.endsWith("date")) {
1418
- return "string-date";
1419
- } else {
1420
- return "string-plain";
1421
- }
1422
- } else if (zodType instanceof z.ZodNumber) {
1423
- if (key === "id") {
1424
- return "number-id";
1425
- } else if (key.endsWith("_id")) {
1426
- return "number-fk_id";
1427
- } else {
1428
- return "number-plain";
1429
- }
1430
- } else if (zodType instanceof z.ZodBoolean) {
1431
- return "boolean";
1432
- } else if (zodType instanceof z.ZodEnum) {
1433
- return "enums";
1434
- } else if (zodType instanceof z.ZodRecord) {
1435
- return "record";
1436
- } else if (zodType instanceof z.ZodAny || zodType instanceof z.ZodUnknown) {
1437
- return "string-plain";
1438
- } else if (zodType instanceof z.ZodUnion) {
1439
- return "string-plain";
1440
- } else if (zodType instanceof z.ZodLiteral) {
1441
- return "string-plain";
1442
- } else {
1443
- throw new Error(`타입 파싱 불가 ${key} ${zodType.def.type}`);
1444
- }
1445
- }
1446
-
1447
- zodTypeToRenderingNode(
1448
- zodType: z.ZodType<any>,
1449
- baseKey: string = "root"
1450
- ): RenderingNode {
1451
- const def = {
1452
- name: baseKey,
1453
- label: inflection.camelize(baseKey, false),
1454
- zodType,
1455
- };
1456
- if (zodType instanceof z.ZodObject) {
1457
- const columnKeys = Object.keys(zodType.shape);
1458
- const children = columnKeys.map((key) => {
1459
- const innerType = zodType.shape[key];
1460
- return this.zodTypeToRenderingNode(innerType, key);
1461
- });
1462
- return {
1463
- ...def,
1464
- renderType: "object",
1465
- children,
1466
- };
1467
- } else if (zodType instanceof z.ZodArray) {
1468
- const innerType = (zodType as z.ZodArray<z.ZodType<any>>).def.element;
1469
- if (innerType instanceof z.ZodString && baseKey.includes("images")) {
1470
- return {
1471
- ...def,
1472
- renderType: "array-images",
1473
- };
1474
- }
1475
- return {
1476
- ...def,
1477
- renderType: "array",
1478
- element: this.zodTypeToRenderingNode(innerType, baseKey),
1479
- };
1480
- } else if (zodType instanceof z.ZodUnion) {
1481
- const optionNodes = (zodType as z.ZodUnion<z.ZodType[]>).def.options.map((opt) =>
1482
- this.zodTypeToRenderingNode(opt, baseKey)
1483
- );
1484
- // TODO: ZodUnion이 들어있는 경우 핸들링
1485
- return optionNodes[0];
1486
- } else if (zodType instanceof z.ZodOptional) {
1487
- return {
1488
- ...this.zodTypeToRenderingNode((zodType as z.ZodOptional<z.ZodType>).def.innerType, baseKey),
1489
- optional: true,
1490
- };
1491
- } else if (zodType instanceof z.ZodNullable) {
1492
- return {
1493
- ...this.zodTypeToRenderingNode((zodType as z.ZodNullable<z.ZodType>).def.innerType, baseKey),
1494
- nullable: true,
1495
- };
1496
- } else {
1497
- return {
1498
- ...def,
1499
- renderType: this.resolveRenderType(baseKey, zodType),
1500
- };
1501
- }
1502
- }
1503
-
1504
- async getColumnsNode(
1505
- entityId: string,
1506
- subsetKey: string
1507
- ): Promise<RenderingNode> {
1508
- const entity = EntityManager.get(entityId);
1509
- const subsetA = entity.subsets[subsetKey];
1510
- if (subsetA === undefined) {
1511
- throw new ServiceUnavailableException("SubsetA 가 없습니다.");
1512
- }
1513
- const propNodes = entity.fieldExprsToPropNodes(subsetA);
1514
- const rootPropNode: EntityPropNode = {
1515
- nodeType: "object",
1516
- children: propNodes,
1517
- };
1518
-
1519
- const columnsZodType = (await this.propNodeToZodType(
1520
- rootPropNode
1521
- )) as z.ZodObject<any>;
1522
-
1523
- const columnsNode = this.zodTypeToRenderingNode(columnsZodType);
1524
- columnsNode.children = columnsNode.children!.map((child) => {
1525
- if (child.renderType === "object") {
1526
- const pickedCol = child.children!.find((cc) =>
1527
- ["title", "name"].includes(cc.name)
1528
- );
1529
- if (pickedCol) {
1530
- return {
1531
- ...child,
1532
- renderType: "object-pick",
1533
- config: {
1534
- picked: pickedCol.name,
1535
- },
1536
- };
1537
- } else {
1538
- return child;
1539
- }
1540
- } else if (
1541
- child.renderType === "array" &&
1542
- child.element &&
1543
- child.element.renderType === "object"
1544
- ) {
1545
- const pickedCol = child.element!.children!.find((cc) =>
1546
- ["title", "name"].includes(cc.name)
1547
- );
1548
- if (pickedCol) {
1549
- return {
1550
- ...child,
1551
- element: {
1552
- ...child.element,
1553
- renderType: "object-pick",
1554
- config: {
1555
- picked: pickedCol.name,
1556
- },
1557
- },
1558
- };
1559
- } else {
1560
- return child;
1561
- }
1562
- }
1563
- return child;
1564
- });
534
+ syncUI() {
535
+ const uiPort = Sonamu.config.ui?.port ?? 57000;
1565
536
 
1566
- return columnsNode;
537
+ fetch(`http://127.0.0.1:${uiPort}/api/reload`, {
538
+ method: "GET",
539
+ }).catch((e) =>
540
+ console.log(chalk.dim(`Failed to reload Sonamu UI: ${e.message}`))
541
+ );
1567
542
  }
1568
543
 
544
+ /**
545
+ * 하위호환용 프록시 메소드입니다.
546
+ */
1569
547
  async createEntity(
1570
548
  form: Omit<TemplateOptions["entity"], "title"> & { title?: string }
1571
549
  ) {
1572
- if (!/^[A-Z][a-zA-Z0-9]*$/.test(form.entityId)) {
1573
- throw new BadRequestException("entityId는 CamelCase 형식이어야 합니다.");
1574
- }
1575
-
1576
- await this.generateTemplate("entity", form);
1577
-
1578
- // reload entities
1579
- await EntityManager.reload();
1580
-
1581
- // syncFromWatcher에서 처리하므로 주석처리
1582
- // this.actionGenerateSchemas();
1583
-
1584
- // // generate schemas, types
1585
- // await Promise.all([
1586
- // ...(form.parentId === undefined
1587
- // ? [
1588
- // this.generateTemplate("init_types", {
1589
- // entityId: form.entityId,
1590
- // }),
1591
- // ]
1592
- // : []),
1593
- // ]);
550
+ return await createEntity(form);
1594
551
  }
1595
552
 
553
+ /**
554
+ * 하위호환용 프록시 메소드입니다.
555
+ */
1596
556
  async delEntity(entityId: string): Promise<{ delPaths: string[] }> {
1597
- const entity = EntityManager.get(entityId);
1598
-
1599
- const delPaths = (() => {
1600
- if (entity.parentId) {
1601
- return [
1602
- `${Sonamu.apiRootPath}/src/application/${entity.names.parentFs}/${entity.names.fs}.entity.json`,
1603
- ];
1604
- } else {
1605
- return [
1606
- `${Sonamu.apiRootPath}/src/application/${entity.names.fs}`,
1607
- `${Sonamu.apiRootPath}/dist/application/${entity.names.fs}`,
1608
- ...Sonamu.config.sync.targets
1609
- .map((target) => [
1610
- `${Sonamu.appRootPath}/${target}/src/services/${entity.names.fs}`,
1611
- ])
1612
- .flat(),
1613
- ];
1614
- }
1615
- })(); // iife
1616
-
1617
- for await (const delPath of delPaths) {
1618
- if (await exists(delPath)) {
1619
- console.log(chalk.red(`DELETE ${delPath}`));
1620
- await rm(delPath, { recursive: true, force: true });
1621
- } else {
1622
- console.log(chalk.yellow(`NOT_EXISTS ${delPath}`));
1623
- }
1624
- }
1625
-
1626
- // reload entities
1627
- await EntityManager.reload();
557
+ return await delEntity(entityId);
558
+ }
1628
559
 
1629
- return { delPaths };
560
+ /**
561
+ * 하위호환용 프록시 메소드입니다.
562
+ */
563
+ async generateTemplate(
564
+ key: TemplateKey,
565
+ templateOptions: any,
566
+ _generateOptions?: GenerateOptions
567
+ ) {
568
+ return await generateTemplate(key, templateOptions, _generateOptions);
1630
569
  }
1631
570
 
1632
- syncUI() {
1633
- const uiPort = Sonamu.config.ui?.port ?? 57000;
571
+ /**
572
+ * 하위호환용 프록시 메소드입니다.
573
+ */
574
+ async renderTemplate(key: TemplateKey, templateOptions: any) {
575
+ return await renderTemplate(key, templateOptions);
576
+ }
1634
577
 
1635
- fetch(`http://127.0.0.1:${uiPort}/api/reload`, {
1636
- method: "GET",
1637
- }).catch((e) =>
1638
- console.log(chalk.dim(`Failed to reload Sonamu UI: ${e.message}`))
1639
- );
578
+ /**
579
+ * 하위호환용 프록시 메소드입니다.
580
+ */
581
+ async renewChecksums(): Promise<void> {
582
+ return await renewChecksums();
1640
583
  }
1641
584
  }