sonamu 0.5.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.swcrc.project-default +18 -0
  2. package/bin/cli.js +24 -0
  3. package/dist/ai/agents/agent.d.ts +11 -0
  4. package/dist/ai/agents/agent.d.ts.map +1 -0
  5. package/dist/ai/agents/agent.js +65 -0
  6. package/dist/ai/agents/index.d.ts +3 -0
  7. package/dist/ai/agents/index.d.ts.map +1 -0
  8. package/dist/ai/agents/index.js +4 -0
  9. package/dist/ai/agents/types.d.ts +43 -0
  10. package/dist/ai/agents/types.d.ts.map +1 -0
  11. package/dist/ai/agents/types.js +3 -0
  12. package/dist/ai/index.d.ts +2 -0
  13. package/dist/ai/index.d.ts.map +1 -0
  14. package/dist/ai/index.js +3 -0
  15. package/dist/ai/providers/rtzr/api.d.ts +22 -0
  16. package/dist/ai/providers/rtzr/api.d.ts.map +1 -0
  17. package/dist/ai/providers/rtzr/api.js +28 -0
  18. package/dist/ai/providers/rtzr/error.d.ts +18 -0
  19. package/dist/ai/providers/rtzr/error.d.ts.map +1 -0
  20. package/dist/ai/providers/rtzr/error.js +29 -0
  21. package/dist/ai/providers/rtzr/index.d.ts +5 -0
  22. package/dist/ai/providers/rtzr/index.d.ts.map +1 -0
  23. package/dist/ai/providers/rtzr/index.js +6 -0
  24. package/dist/ai/providers/rtzr/model.d.ts +52 -0
  25. package/dist/ai/providers/rtzr/model.d.ts.map +1 -0
  26. package/dist/ai/providers/rtzr/model.js +137 -0
  27. package/dist/ai/providers/rtzr/options.d.ts +7 -0
  28. package/dist/ai/providers/rtzr/options.d.ts.map +1 -0
  29. package/dist/ai/providers/rtzr/options.js +47 -0
  30. package/dist/ai/providers/rtzr/provider.d.ts +18 -0
  31. package/dist/ai/providers/rtzr/provider.d.ts.map +1 -0
  32. package/dist/ai/providers/rtzr/provider.js +54 -0
  33. package/dist/ai/providers/rtzr/utils.d.ts +19 -0
  34. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -0
  35. package/dist/ai/providers/rtzr/utils.js +88 -0
  36. package/dist/api/base-frame.d.ts +2 -2
  37. package/dist/api/base-frame.d.ts.map +1 -1
  38. package/dist/api/base-frame.js +13 -2
  39. package/dist/api/caster.d.ts.map +1 -1
  40. package/dist/api/caster.js +71 -2
  41. package/dist/api/code-converters.d.ts +58 -14
  42. package/dist/api/code-converters.d.ts.map +1 -1
  43. package/dist/api/code-converters.js +258 -2
  44. package/dist/api/config.d.ts +90 -0
  45. package/dist/api/config.d.ts.map +1 -0
  46. package/dist/api/config.js +25 -0
  47. package/dist/api/context.d.ts +4 -2
  48. package/dist/api/context.d.ts.map +1 -1
  49. package/dist/api/context.js +3 -2
  50. package/dist/api/decorators.d.ts +20 -6
  51. package/dist/api/decorators.d.ts.map +1 -1
  52. package/dist/api/decorators.js +235 -2
  53. package/dist/api/index.d.ts +2 -2
  54. package/dist/api/index.d.ts.map +1 -1
  55. package/dist/api/index.js +9 -2
  56. package/dist/api/sonamu.d.ts +10 -24
  57. package/dist/api/sonamu.d.ts.map +1 -1
  58. package/dist/api/sonamu.js +514 -2
  59. package/dist/api/validator.d.ts +6 -0
  60. package/dist/api/validator.d.ts.map +1 -0
  61. package/dist/api/validator.js +81 -0
  62. package/dist/bin/build-config.d.ts +6 -1
  63. package/dist/bin/build-config.d.ts.map +1 -1
  64. package/dist/bin/build-config.js +15 -2
  65. package/dist/bin/cli.js +519 -2
  66. package/dist/bin/hot-hook-register.d.ts +11 -0
  67. package/dist/bin/hot-hook-register.d.ts.map +1 -0
  68. package/dist/bin/hot-hook-register.js +21 -0
  69. package/dist/bin/loader-register.d.ts +2 -0
  70. package/dist/bin/loader-register.d.ts.map +1 -0
  71. package/dist/bin/loader-register.js +34 -0
  72. package/dist/database/_batch_update.d.ts +5 -3
  73. package/dist/database/_batch_update.d.ts.map +1 -1
  74. package/dist/database/_batch_update.js +95 -2
  75. package/dist/database/base-model.d.ts +96 -10
  76. package/dist/database/base-model.d.ts.map +1 -1
  77. package/dist/database/base-model.js +390 -2
  78. package/dist/database/base-model.types.d.ts +93 -0
  79. package/dist/database/base-model.types.d.ts.map +1 -0
  80. package/dist/database/base-model.types.js +10 -0
  81. package/dist/database/code-generator.d.ts +1 -1
  82. package/dist/database/code-generator.d.ts.map +1 -1
  83. package/dist/database/code-generator.js +54 -2
  84. package/dist/database/db.d.ts +6 -21
  85. package/dist/database/db.d.ts.map +1 -1
  86. package/dist/database/db.js +129 -2
  87. package/dist/database/puri-subset.test-d.js +81 -0
  88. package/dist/database/puri-subset.types.d.ts +123 -0
  89. package/dist/database/puri-subset.types.d.ts.map +1 -0
  90. package/dist/database/puri-subset.types.js +16 -0
  91. package/dist/database/puri-wrapper.d.ts +13 -11
  92. package/dist/database/puri-wrapper.d.ts.map +1 -1
  93. package/dist/database/puri-wrapper.js +109 -2
  94. package/dist/database/puri.d.ts +41 -23
  95. package/dist/database/puri.d.ts.map +1 -1
  96. package/dist/database/puri.js +601 -2
  97. package/dist/database/puri.types.d.ts +25 -6
  98. package/dist/database/puri.types.d.ts.map +1 -1
  99. package/dist/database/puri.types.js +6 -2
  100. package/dist/database/transaction-context.d.ts +1 -1
  101. package/dist/database/transaction-context.d.ts.map +1 -1
  102. package/dist/database/transaction-context.js +14 -2
  103. package/dist/database/upsert-builder.d.ts +9 -3
  104. package/dist/database/upsert-builder.d.ts.map +1 -1
  105. package/dist/database/upsert-builder.js +365 -2
  106. package/dist/entity/entity-manager.d.ts +167 -2
  107. package/dist/entity/entity-manager.d.ts.map +1 -1
  108. package/dist/entity/entity-manager.js +130 -2
  109. package/dist/entity/entity.d.ts +5 -3
  110. package/dist/entity/entity.d.ts.map +1 -1
  111. package/dist/entity/entity.js +750 -2
  112. package/dist/exceptions/error-handler.d.ts +1 -1
  113. package/dist/exceptions/error-handler.d.ts.map +1 -1
  114. package/dist/exceptions/error-handler.js +29 -2
  115. package/dist/exceptions/so-exceptions.d.ts +1 -1
  116. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  117. package/dist/exceptions/so-exceptions.js +85 -2
  118. package/dist/file-storage/driver.d.ts +1 -1
  119. package/dist/file-storage/driver.d.ts.map +1 -1
  120. package/dist/file-storage/driver.js +79 -2
  121. package/dist/file-storage/file-storage.js +75 -2
  122. package/dist/index.d.ts +18 -9
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +34 -2
  125. package/dist/migration/code-generation.d.ts +1 -1
  126. package/dist/migration/code-generation.d.ts.map +1 -1
  127. package/dist/migration/code-generation.js +614 -2
  128. package/dist/migration/migration-set.d.ts +2 -10
  129. package/dist/migration/migration-set.d.ts.map +1 -1
  130. package/dist/migration/migration-set.js +213 -2
  131. package/dist/migration/migrator.d.ts +24 -82
  132. package/dist/migration/migrator.d.ts.map +1 -1
  133. package/dist/migration/migrator.js +330 -2
  134. package/dist/migration/postgresql-schema-reader.d.ts +51 -0
  135. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -0
  136. package/dist/migration/postgresql-schema-reader.js +245 -0
  137. package/dist/migration/types.d.ts +6 -38
  138. package/dist/migration/types.d.ts.map +1 -1
  139. package/dist/migration/types.js +3 -2
  140. package/dist/naite/messaging-types.d.ts +43 -0
  141. package/dist/naite/messaging-types.d.ts.map +1 -0
  142. package/dist/naite/messaging-types.js +7 -0
  143. package/dist/naite/naite-reporter.d.ts +41 -0
  144. package/dist/naite/naite-reporter.d.ts.map +1 -0
  145. package/dist/naite/naite-reporter.js +102 -0
  146. package/dist/naite/naite.d.ts +95 -0
  147. package/dist/naite/naite.d.ts.map +1 -0
  148. package/dist/naite/naite.js +316 -0
  149. package/dist/stream/index.js +3 -2
  150. package/dist/stream/sse.d.ts +2 -2
  151. package/dist/stream/sse.d.ts.map +1 -1
  152. package/dist/stream/sse.js +38 -2
  153. package/dist/syncer/api-parser.d.ts +10 -0
  154. package/dist/syncer/api-parser.d.ts.map +1 -0
  155. package/dist/syncer/api-parser.js +240 -0
  156. package/dist/syncer/checksum.d.ts +21 -0
  157. package/dist/syncer/checksum.d.ts.map +1 -0
  158. package/dist/syncer/checksum.js +98 -0
  159. package/dist/syncer/code-generator.d.ts +20 -0
  160. package/dist/syncer/code-generator.d.ts.map +1 -0
  161. package/dist/syncer/code-generator.js +161 -0
  162. package/dist/syncer/entity-operations.d.ts +17 -0
  163. package/dist/syncer/entity-operations.d.ts.map +1 -0
  164. package/dist/syncer/entity-operations.js +59 -0
  165. package/dist/syncer/file-patterns.d.ts +29 -0
  166. package/dist/syncer/file-patterns.d.ts.map +1 -0
  167. package/dist/syncer/file-patterns.js +38 -0
  168. package/dist/syncer/index.d.ts +6 -0
  169. package/dist/syncer/index.d.ts.map +1 -1
  170. package/dist/syncer/index.js +9 -2
  171. package/dist/syncer/module-loader.d.ts +35 -0
  172. package/dist/syncer/module-loader.d.ts.map +1 -0
  173. package/dist/syncer/module-loader.js +87 -0
  174. package/dist/syncer/syncer.d.ts +98 -106
  175. package/dist/syncer/syncer.d.ts.map +1 -1
  176. package/dist/syncer/syncer.js +422 -2
  177. package/dist/template/entity-converter.d.ts +14 -0
  178. package/dist/template/entity-converter.d.ts.map +1 -0
  179. package/dist/template/entity-converter.js +108 -0
  180. package/dist/template/helpers.d.ts +23 -0
  181. package/dist/template/helpers.d.ts.map +1 -0
  182. package/dist/template/helpers.js +64 -0
  183. package/dist/{templates → template/implementations}/entity.template.d.ts +3 -3
  184. package/dist/template/implementations/entity.template.d.ts.map +1 -0
  185. package/dist/template/implementations/entity.template.js +86 -0
  186. package/dist/{templates → template/implementations}/generated.template.d.ts +3 -4
  187. package/dist/template/implementations/generated.template.d.ts.map +1 -0
  188. package/dist/template/implementations/generated.template.js +249 -0
  189. package/dist/{templates → template/implementations}/generated_http.template.d.ts +3 -4
  190. package/dist/template/implementations/generated_http.template.d.ts.map +1 -0
  191. package/dist/template/implementations/generated_http.template.js +131 -0
  192. package/dist/{templates → template/implementations}/generated_sso.template.d.ts +4 -5
  193. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -0
  194. package/dist/template/implementations/generated_sso.template.js +134 -0
  195. package/dist/{templates → template/implementations}/init_types.template.d.ts +3 -3
  196. package/dist/template/implementations/init_types.template.d.ts.map +1 -0
  197. package/dist/template/implementations/init_types.template.js +38 -0
  198. package/dist/template/implementations/model.template.d.ts +17 -0
  199. package/dist/template/implementations/model.template.d.ts.map +1 -0
  200. package/dist/template/implementations/model.template.js +181 -0
  201. package/dist/{templates → template/implementations}/model_test.template.d.ts +3 -3
  202. package/dist/template/implementations/model_test.template.d.ts.map +1 -0
  203. package/dist/template/implementations/model_test.template.js +35 -0
  204. package/dist/{templates → template/implementations}/service.template.d.ts +6 -6
  205. package/dist/template/implementations/service.template.d.ts.map +1 -0
  206. package/dist/template/implementations/service.template.js +201 -0
  207. package/dist/{templates → template/implementations}/view_enums_buttonset.template.d.ts +3 -3
  208. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +1 -0
  209. package/dist/template/implementations/view_enums_buttonset.template.js +31 -0
  210. package/dist/{templates → template/implementations}/view_enums_dropdown.template.d.ts +3 -4
  211. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +1 -0
  212. package/dist/template/implementations/view_enums_dropdown.template.js +50 -0
  213. package/dist/{templates → template/implementations}/view_enums_select.template.d.ts +3 -3
  214. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -0
  215. package/dist/template/implementations/view_enums_select.template.js +55 -0
  216. package/dist/{templates → template/implementations}/view_form.template.d.ts +5 -5
  217. package/dist/template/implementations/view_form.template.d.ts.map +1 -0
  218. package/dist/template/implementations/view_form.template.js +337 -0
  219. package/dist/{templates → template/implementations}/view_id_all_select.template.d.ts +3 -3
  220. package/dist/template/implementations/view_id_all_select.template.d.ts.map +1 -0
  221. package/dist/template/implementations/view_id_all_select.template.js +31 -0
  222. package/dist/{templates → template/implementations}/view_id_async_select.template.d.ts +3 -3
  223. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -0
  224. package/dist/template/implementations/view_id_async_select.template.js +105 -0
  225. package/dist/{templates → template/implementations}/view_list.template.d.ts +5 -13
  226. package/dist/template/implementations/view_list.template.d.ts.map +1 -0
  227. package/dist/template/implementations/view_list.template.js +475 -0
  228. package/dist/template/implementations/view_list_columns.template.d.ts +17 -0
  229. package/dist/template/implementations/view_list_columns.template.d.ts.map +1 -0
  230. package/dist/template/implementations/view_list_columns.template.js +49 -0
  231. package/dist/{templates → template/implementations}/view_search_input.template.d.ts +3 -3
  232. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -0
  233. package/dist/template/implementations/view_search_input.template.js +64 -0
  234. package/dist/template/index.d.ts +7 -0
  235. package/dist/template/index.d.ts.map +1 -0
  236. package/dist/template/index.js +8 -0
  237. package/dist/template/template-manager.d.ts +56 -0
  238. package/dist/template/template-manager.d.ts.map +1 -0
  239. package/dist/template/template-manager.js +125 -0
  240. package/dist/template/template-types.d.ts +16 -0
  241. package/dist/template/template-types.d.ts.map +1 -0
  242. package/dist/template/template-types.js +7 -0
  243. package/dist/template/template.d.ts +49 -0
  244. package/dist/template/template.d.ts.map +1 -0
  245. package/dist/template/template.js +60 -0
  246. package/dist/template/zod-converter.d.ts +51 -0
  247. package/dist/template/zod-converter.d.ts.map +1 -0
  248. package/dist/template/zod-converter.js +449 -0
  249. package/dist/testing/_relation-graph.d.ts +1 -1
  250. package/dist/testing/_relation-graph.d.ts.map +1 -1
  251. package/dist/testing/_relation-graph.js +89 -2
  252. package/dist/testing/fixture-manager.d.ts +42 -11
  253. package/dist/testing/fixture-manager.d.ts.map +1 -1
  254. package/dist/testing/fixture-manager.js +623 -2
  255. package/dist/types/types.d.ts +747 -143
  256. package/dist/types/types.d.ts.map +1 -1
  257. package/dist/types/types.js +546 -2
  258. package/dist/typings/knex.d.js +3 -2
  259. package/dist/utils/async-utils.d.ts +7 -0
  260. package/dist/utils/async-utils.d.ts.map +1 -1
  261. package/dist/utils/async-utils.js +57 -2
  262. package/dist/utils/console-util.d.ts +2 -0
  263. package/dist/utils/console-util.d.ts.map +1 -0
  264. package/dist/utils/console-util.js +6 -0
  265. package/dist/utils/controller.d.ts +1 -0
  266. package/dist/utils/controller.d.ts.map +1 -1
  267. package/dist/utils/controller.js +29 -2
  268. package/dist/utils/esm-utils.d.ts +39 -0
  269. package/dist/utils/esm-utils.d.ts.map +1 -0
  270. package/dist/utils/esm-utils.js +49 -0
  271. package/dist/utils/formatter.d.ts +3 -0
  272. package/dist/utils/formatter.d.ts.map +1 -0
  273. package/dist/utils/formatter.js +110 -0
  274. package/dist/utils/fs-utils.d.ts +1 -1
  275. package/dist/utils/fs-utils.d.ts.map +1 -1
  276. package/dist/utils/fs-utils.js +17 -2
  277. package/dist/utils/lodash-able.d.ts.map +1 -1
  278. package/dist/utils/lodash-able.js +6 -2
  279. package/dist/utils/model.js +22 -2
  280. package/dist/utils/object-utils.d.ts +44 -0
  281. package/dist/utils/object-utils.d.ts.map +1 -0
  282. package/dist/utils/object-utils.js +191 -0
  283. package/dist/utils/path-utils.d.ts +89 -0
  284. package/dist/utils/path-utils.d.ts.map +1 -0
  285. package/dist/utils/path-utils.js +60 -0
  286. package/dist/utils/process-utils.d.ts +13 -0
  287. package/dist/utils/process-utils.d.ts.map +1 -0
  288. package/dist/utils/process-utils.js +36 -0
  289. package/dist/utils/sql-parser.d.ts +5 -1
  290. package/dist/utils/sql-parser.d.ts.map +1 -1
  291. package/dist/utils/sql-parser.js +46 -2
  292. package/dist/utils/type-utils.d.ts +23 -0
  293. package/dist/utils/type-utils.d.ts.map +1 -0
  294. package/dist/utils/type-utils.js +45 -0
  295. package/dist/utils/utils.d.ts +10 -7
  296. package/dist/utils/utils.d.ts.map +1 -1
  297. package/dist/utils/utils.js +72 -2
  298. package/dist/utils/zod-error.d.ts +1 -1
  299. package/dist/utils/zod-error.d.ts.map +1 -1
  300. package/dist/utils/zod-error.js +19 -2
  301. package/package.json +65 -27
  302. package/src/ai/agents/agent.ts +87 -0
  303. package/src/ai/agents/index.ts +2 -0
  304. package/src/ai/agents/types.ts +47 -0
  305. package/src/ai/index.ts +1 -0
  306. package/src/ai/providers/rtzr/api.ts +37 -0
  307. package/src/ai/providers/rtzr/error.ts +34 -0
  308. package/src/ai/providers/rtzr/index.ts +4 -0
  309. package/src/ai/providers/rtzr/model.ts +201 -0
  310. package/src/ai/providers/rtzr/options.ts +49 -0
  311. package/src/ai/providers/rtzr/provider.ts +91 -0
  312. package/src/ai/providers/rtzr/utils.ts +127 -0
  313. package/src/api/base-frame.ts +4 -2
  314. package/src/api/caster.ts +17 -23
  315. package/src/api/code-converters.ts +178 -535
  316. package/src/api/config.ts +125 -0
  317. package/src/api/context.ts +7 -17
  318. package/src/api/decorators.ts +176 -46
  319. package/src/api/index.ts +2 -2
  320. package/src/api/sonamu.ts +190 -167
  321. package/src/api/validator.ts +83 -0
  322. package/src/bin/build-config.ts +8 -1
  323. package/src/bin/cli.ts +258 -124
  324. package/src/bin/hot-hook-register.ts +22 -0
  325. package/src/bin/loader-register.ts +38 -0
  326. package/src/database/_batch_update.ts +46 -31
  327. package/src/database/base-model.ts +390 -182
  328. package/src/database/base-model.types.ts +155 -0
  329. package/src/database/code-generator.ts +13 -32
  330. package/src/database/db.ts +40 -96
  331. package/src/database/puri-subset.test-d.ts +471 -0
  332. package/src/database/puri-subset.types.ts +195 -0
  333. package/src/database/puri-wrapper.ts +58 -67
  334. package/src/database/puri.ts +229 -148
  335. package/src/database/puri.types.ts +76 -30
  336. package/src/database/transaction-context.ts +1 -1
  337. package/src/database/upsert-builder.ts +262 -132
  338. package/src/entity/entity-manager.ts +48 -36
  339. package/src/entity/entity.ts +330 -248
  340. package/src/exceptions/error-handler.ts +3 -3
  341. package/src/exceptions/so-exceptions.ts +11 -11
  342. package/src/file-storage/driver.ts +5 -5
  343. package/src/file-storage/file-storage.ts +2 -2
  344. package/src/index.ts +18 -10
  345. package/src/migration/code-generation.ts +185 -172
  346. package/src/migration/migration-set.ts +80 -293
  347. package/src/migration/migrator.ts +199 -571
  348. package/src/migration/mysql-schema-reader.ts.txt +272 -0
  349. package/src/migration/postgresql-schema-reader.ts +310 -0
  350. package/src/migration/types.ts +6 -39
  351. package/src/naite/messaging-types.ts +51 -0
  352. package/src/naite/naite-reporter.ts +128 -0
  353. package/src/naite/naite.ts +415 -0
  354. package/src/shared/web.shared.ts.txt +20 -24
  355. package/src/stream/sse.ts +5 -5
  356. package/src/syncer/api-parser.ts +282 -0
  357. package/src/syncer/checksum.ts +140 -0
  358. package/src/syncer/code-generator.ts +198 -0
  359. package/src/syncer/entity-operations.ts +65 -0
  360. package/src/syncer/file-patterns.ts +56 -0
  361. package/src/syncer/index.ts +6 -0
  362. package/src/syncer/module-loader.ts +128 -0
  363. package/src/syncer/syncer.ts +389 -1453
  364. package/src/template/entity-converter.ts +114 -0
  365. package/src/template/helpers.ts +81 -0
  366. package/src/{templates → template/implementations}/entity.template.ts +7 -7
  367. package/src/{templates → template/implementations}/generated.template.ts +101 -101
  368. package/src/{templates → template/implementations}/generated_http.template.ts +27 -57
  369. package/src/template/implementations/generated_sso.template.ts +151 -0
  370. package/src/{templates → template/implementations}/init_types.template.ts +5 -7
  371. package/src/{templates → template/implementations}/model.template.ts +52 -43
  372. package/src/{templates → template/implementations}/model_test.template.ts +5 -5
  373. package/src/{templates → template/implementations}/service.template.ts +66 -82
  374. package/src/{templates → template/implementations}/view_enums_buttonset.template.ts +3 -3
  375. package/src/{templates → template/implementations}/view_enums_dropdown.template.ts +4 -20
  376. package/src/{templates → template/implementations}/view_enums_select.template.ts +4 -4
  377. package/src/{templates → template/implementations}/view_form.template.ts +40 -83
  378. package/src/{templates → template/implementations}/view_id_all_select.template.ts +3 -3
  379. package/src/{templates → template/implementations}/view_id_async_select.template.ts +10 -24
  380. package/src/{templates → template/implementations}/view_list.template.ts +60 -152
  381. package/src/{templates → template/implementations}/view_list_columns.template.ts +5 -11
  382. package/src/{templates → template/implementations}/view_search_input.template.ts +3 -3
  383. package/src/template/index.ts +6 -0
  384. package/src/template/template-manager.ts +166 -0
  385. package/src/template/template-types.ts +16 -0
  386. package/src/template/template.ts +105 -0
  387. package/src/template/zod-converter.ts +525 -0
  388. package/src/testing/_relation-graph.ts +18 -11
  389. package/src/testing/fixture-manager.ts +472 -359
  390. package/src/types/types.ts +553 -308
  391. package/src/typings/knex.d.ts +7 -9
  392. package/src/utils/async-utils.ts +23 -10
  393. package/src/utils/console-util.ts +4 -0
  394. package/src/utils/controller.ts +3 -0
  395. package/src/utils/esm-utils.ts +59 -0
  396. package/src/utils/formatter.ts +109 -0
  397. package/src/utils/fs-utils.ts +1 -1
  398. package/src/utils/lodash-able.ts +1 -4
  399. package/src/utils/object-utils.ts +217 -0
  400. package/src/utils/path-utils.ts +99 -0
  401. package/src/utils/process-utils.ts +46 -0
  402. package/src/utils/sql-parser.ts +23 -5
  403. package/src/utils/type-utils.ts +83 -0
  404. package/src/utils/utils.ts +66 -43
  405. package/src/utils/zod-error.ts +3 -4
  406. package/dist/api/base-frame.js.map +0 -1
  407. package/dist/api/caster.js.map +0 -1
  408. package/dist/api/code-converters.js.map +0 -1
  409. package/dist/api/context.js.map +0 -1
  410. package/dist/api/decorators.js.map +0 -1
  411. package/dist/api/index.js.map +0 -1
  412. package/dist/api/sonamu.js.map +0 -1
  413. package/dist/bin/build-config.js.map +0 -1
  414. package/dist/bin/cli-wrapper.d.ts +0 -3
  415. package/dist/bin/cli-wrapper.d.ts.map +0 -1
  416. package/dist/bin/cli-wrapper.js +0 -3
  417. package/dist/bin/cli-wrapper.js.map +0 -1
  418. package/dist/bin/cli.js.map +0 -1
  419. package/dist/database/_batch_update.js.map +0 -1
  420. package/dist/database/base-model.js.map +0 -1
  421. package/dist/database/code-generator.js.map +0 -1
  422. package/dist/database/db.js.map +0 -1
  423. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts +0 -2
  424. package/dist/database/knex-plugins/knex-on-duplicate-update.d.ts.map +0 -1
  425. package/dist/database/knex-plugins/knex-on-duplicate-update.js +0 -2
  426. package/dist/database/knex-plugins/knex-on-duplicate-update.js.map +0 -1
  427. package/dist/database/puri-wrapper.js.map +0 -1
  428. package/dist/database/puri.js.map +0 -1
  429. package/dist/database/puri.types.js.map +0 -1
  430. package/dist/database/transaction-context.js.map +0 -1
  431. package/dist/database/upsert-builder.js.map +0 -1
  432. package/dist/entity/entity-manager.js.map +0 -1
  433. package/dist/entity/entity-utils.d.ts +0 -61
  434. package/dist/entity/entity-utils.d.ts.map +0 -1
  435. package/dist/entity/entity-utils.js +0 -2
  436. package/dist/entity/entity-utils.js.map +0 -1
  437. package/dist/entity/entity.js.map +0 -1
  438. package/dist/exceptions/error-handler.js.map +0 -1
  439. package/dist/exceptions/so-exceptions.js.map +0 -1
  440. package/dist/file-storage/driver.js.map +0 -1
  441. package/dist/file-storage/file-storage.js.map +0 -1
  442. package/dist/index.js.map +0 -1
  443. package/dist/migration/code-generation.js.map +0 -1
  444. package/dist/migration/migration-set.js.map +0 -1
  445. package/dist/migration/migrator.js.map +0 -1
  446. package/dist/migration/types.js.map +0 -1
  447. package/dist/stream/index.js.map +0 -1
  448. package/dist/stream/sse.js.map +0 -1
  449. package/dist/syncer/index.js.map +0 -1
  450. package/dist/syncer/syncer.js.map +0 -1
  451. package/dist/templates/base-template.d.ts +0 -13
  452. package/dist/templates/base-template.d.ts.map +0 -1
  453. package/dist/templates/base-template.js +0 -2
  454. package/dist/templates/base-template.js.map +0 -1
  455. package/dist/templates/entity.template.d.ts.map +0 -1
  456. package/dist/templates/entity.template.js +0 -2
  457. package/dist/templates/entity.template.js.map +0 -1
  458. package/dist/templates/generated.template.d.ts.map +0 -1
  459. package/dist/templates/generated.template.js +0 -2
  460. package/dist/templates/generated.template.js.map +0 -1
  461. package/dist/templates/generated_http.template.d.ts.map +0 -1
  462. package/dist/templates/generated_http.template.js +0 -2
  463. package/dist/templates/generated_http.template.js.map +0 -1
  464. package/dist/templates/generated_sso.template.d.ts.map +0 -1
  465. package/dist/templates/generated_sso.template.js +0 -2
  466. package/dist/templates/generated_sso.template.js.map +0 -1
  467. package/dist/templates/index.d.ts +0 -2
  468. package/dist/templates/index.d.ts.map +0 -1
  469. package/dist/templates/index.js +0 -2
  470. package/dist/templates/index.js.map +0 -1
  471. package/dist/templates/init_types.template.d.ts.map +0 -1
  472. package/dist/templates/init_types.template.js +0 -2
  473. package/dist/templates/init_types.template.js.map +0 -1
  474. package/dist/templates/model.template.d.ts +0 -17
  475. package/dist/templates/model.template.d.ts.map +0 -1
  476. package/dist/templates/model.template.js +0 -2
  477. package/dist/templates/model.template.js.map +0 -1
  478. package/dist/templates/model_test.template.d.ts.map +0 -1
  479. package/dist/templates/model_test.template.js +0 -2
  480. package/dist/templates/model_test.template.js.map +0 -1
  481. package/dist/templates/service.template.d.ts.map +0 -1
  482. package/dist/templates/service.template.js +0 -2
  483. package/dist/templates/service.template.js.map +0 -1
  484. package/dist/templates/view_enums_buttonset.template.d.ts.map +0 -1
  485. package/dist/templates/view_enums_buttonset.template.js +0 -2
  486. package/dist/templates/view_enums_buttonset.template.js.map +0 -1
  487. package/dist/templates/view_enums_dropdown.template.d.ts.map +0 -1
  488. package/dist/templates/view_enums_dropdown.template.js +0 -2
  489. package/dist/templates/view_enums_dropdown.template.js.map +0 -1
  490. package/dist/templates/view_enums_select.template.d.ts.map +0 -1
  491. package/dist/templates/view_enums_select.template.js +0 -2
  492. package/dist/templates/view_enums_select.template.js.map +0 -1
  493. package/dist/templates/view_form.template.d.ts.map +0 -1
  494. package/dist/templates/view_form.template.js +0 -2
  495. package/dist/templates/view_form.template.js.map +0 -1
  496. package/dist/templates/view_id_all_select.template.d.ts.map +0 -1
  497. package/dist/templates/view_id_all_select.template.js +0 -2
  498. package/dist/templates/view_id_all_select.template.js.map +0 -1
  499. package/dist/templates/view_id_async_select.template.d.ts.map +0 -1
  500. package/dist/templates/view_id_async_select.template.js +0 -2
  501. package/dist/templates/view_id_async_select.template.js.map +0 -1
  502. package/dist/templates/view_list.template.d.ts.map +0 -1
  503. package/dist/templates/view_list.template.js +0 -2
  504. package/dist/templates/view_list.template.js.map +0 -1
  505. package/dist/templates/view_list_columns.template.d.ts +0 -17
  506. package/dist/templates/view_list_columns.template.d.ts.map +0 -1
  507. package/dist/templates/view_list_columns.template.js +0 -2
  508. package/dist/templates/view_list_columns.template.js.map +0 -1
  509. package/dist/templates/view_search_input.template.d.ts.map +0 -1
  510. package/dist/templates/view_search_input.template.js +0 -2
  511. package/dist/templates/view_search_input.template.js.map +0 -1
  512. package/dist/testing/_relation-graph.js.map +0 -1
  513. package/dist/testing/fixture-manager.js.map +0 -1
  514. package/dist/types/types.js.map +0 -1
  515. package/dist/typings/knex.d.js.map +0 -1
  516. package/dist/utils/async-utils.js.map +0 -1
  517. package/dist/utils/controller.js.map +0 -1
  518. package/dist/utils/fs-utils.js.map +0 -1
  519. package/dist/utils/lodash-able.js.map +0 -1
  520. package/dist/utils/model.js.map +0 -1
  521. package/dist/utils/sql-parser.js.map +0 -1
  522. package/dist/utils/utils.js.map +0 -1
  523. package/dist/utils/zod-error.js.map +0 -1
  524. package/src/bin/cli-wrapper.ts +0 -75
  525. package/src/database/knex-plugins/knex-on-duplicate-update.ts +0 -45
  526. package/src/entity/entity-utils.ts +0 -291
  527. package/src/templates/base-template.ts +0 -19
  528. package/src/templates/generated_sso.template.ts +0 -138
  529. package/src/templates/index.ts +0 -1
@@ -1,300 +1,217 @@
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";
5
- 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";
10
- 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";
43
- 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";
76
- import { Sonamu } from "../api/sonamu";
77
- import { Template__generated_sso } from "../templates/generated_sso.template";
78
- import { setTimeout as setTimeoutPromises } from "timers/promises";
1
+ import { hot } from "@sonamu-kit/hot-hook";
79
2
  import assert from "assert";
80
- import * as swc from "@swc/core";
3
+ import chalk from "chalk";
4
+ import { mkdir, readFile, writeFile } from "fs/promises";
81
5
  import { minimatch } from "minimatch";
6
+ import path, { dirname } from "path";
7
+ import { group, unique } from "radashi";
8
+ import type { z } from "zod";
9
+ import { registeredApis } from "../api/decorators";
10
+ import { Sonamu } from "../api/sonamu";
11
+ import { EntityManager, type EntityNamesRecord } from "../entity/entity-manager";
12
+ import { Naite } from "../naite/naite";
13
+ import { TemplateManager } from "../template/template-manager";
14
+ import type { GenerateOptions, PathAndCode } from "../types/types";
15
+ import { TemplateKey, type TemplateOptions } from "../types/types";
16
+ import { mapAsync, reduceAsync } from "../utils/async-utils";
17
+ import { centerText } from "../utils/console-util";
18
+ import { isTest } from "../utils/controller";
19
+ import { exists } from "../utils/fs-utils";
20
+ import type { AbsolutePath } from "../utils/path-utils";
21
+ import { runWithGracefulShutdown } from "../utils/process-utils";
22
+ import { areFilesSame, findChangedFilesUsingChecksums, renewChecksums } from "./checksum";
23
+ import { generateTemplate, renderTemplate } from "./code-generator";
24
+ import { createEntity, delEntity } from "./entity-operations";
25
+ import { type FileType, getChecksumPatternGroupInAbsolutePath } from "./file-patterns";
82
26
  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
- };
27
+ type LoadedApis,
28
+ type LoadedModels,
29
+ type LoadedTypes,
30
+ loadApis,
31
+ loadModels,
32
+ loadTypes,
33
+ } from "./module-loader";
34
+
103
35
  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
- }[];
36
+ [key in FileType]: AbsolutePath[];
116
37
  };
117
38
 
118
39
  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 } = {};
40
+ apis: LoadedApis = [];
41
+ types: LoadedTypes = {};
42
+ models: LoadedModels = {};
130
43
  isSyncing: boolean = false;
131
44
 
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
-
45
+ /**
46
+ * 체크섬이 변경된 부분에 대해 싱크를 진행합니다.
47
+ * 다만 sonamu.shared.ts는 체크섬 비교 없이 무조건 싱크(복사)합니다.
48
+ * @returns
49
+ */
148
50
  async sync(): Promise<void> {
149
51
  const { targets } = Sonamu.config.sync;
150
52
 
151
- // 번들러 여부에 따라 현재 디렉토리가 바뀌므로
152
- const currentDirname = __dirname.endsWith("/syncer")
153
- ? __dirname
154
- : path.join(__dirname, "./syncer");
53
+ // sonamu.shared.ts는 무조건 싱크(복사)합니다.
54
+ await this.copySharedToTargets(targets);
155
55
 
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
- }
165
-
166
- const dstCodePath = path.join(
167
- Sonamu.appRootPath,
168
- target,
169
- "src/services/sonamu.shared.ts"
170
- );
56
+ // 다음부터는 변경된 파일을 찾아서 동기화 작업을 실행합니다.
57
+ const changedFiles = await findChangedFilesUsingChecksums();
58
+ if (changedFiles.length === 0) {
59
+ console.log(chalk.black.bgGreen(centerText("All files are synced!")));
60
+ return;
61
+ }
171
62
 
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
- })();
63
+ // 만약 싱크 중에 프로세스가 죽으면 꼬여버리기 때문에,
64
+ // 시그널에도 잠시 버틸 있는 환경 속에서 싱크를 실행합니다.
65
+ await runWithGracefulShutdown(
66
+ async () => {
67
+ // 얘가 싱크 작업 수행하는 본체입니다.
68
+ await this.doSyncActions(changedFiles);
179
69
 
180
- if (srcChecksum === dstChecksum) {
181
- return;
182
- }
183
- await writeFile(dstCodePath, await readFile(srcCodePath));
184
- console.log(chalk.blue("shared.ts is synced"));
185
- })
70
+ // 싱크 액션이 끝나면 항상 체크섬을 다시 갱신합니다.
71
+ await renewChecksums();
72
+ },
73
+ { whenThisHappens: "SIGUSR2", waitForUpTo: 20000 },
186
74
  );
75
+ }
187
76
 
188
- // 현재 checksums
189
- let currentChecksums = await this.getCurrentChecksums();
190
- // 이전 checksums
191
- const previousChecksums = await this.getPreviousChecksums();
192
-
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;
198
- console.log(
199
- chalk.black.bgGreen(" ".repeat(margin) + msg + " ".repeat(margin))
200
- );
77
+ /**
78
+ * Watcher가 감지한 파일 변경 사항에 대해 싱크를 진행합니다.
79
+ * 주어진 변경 파일들 중 체크섬 관리 대상인 것들만 가져다가 싱크를 진행합니다.
80
+ * 체크섬 파일 업데이트는 여기에서 하지 않습니다. 호출자가 합니다.
81
+ * @param diffFilePath - 변경 파일들. 프로젝트 루트부터 "src/" 또는 "dist/"로 시작하는 상대 경로입니다. 예시: "src/application/user/user.model.ts"
82
+ */
83
+ async syncFromWatcher(event: string, diffFilePath: AbsolutePath): Promise<void> {
84
+ if (event !== "change" && event !== "add" && event !== "unlink") {
201
85
  return;
202
86
  }
203
87
 
204
- const abc = new AbortController();
205
- this.isSyncing = true;
206
- const onSIGUSR2 = async () => {
207
- if (this.isSyncing === false) {
208
- process.exit(0);
88
+ // 일단 변경된 파일과 dependent 파일들을 invalidate 합니다.
89
+ // 번 이상 import된 친구들에 대해서만 실제 작업이 일어납니다.
90
+ // 그러니 안심하고 invalidate 해도 됩니다.
91
+ // 테스트 환경에서는 hot.invalidateFile시 초기 에러가 발생하기 때문에 invalidate 하지 않습니다.
92
+ if (!isTest()) {
93
+ const invalidatedPaths = (await hot.invalidateFile(diffFilePath, event)) as AbsolutePath[];
94
+
95
+ if (invalidatedPaths.length > 0) {
96
+ console.log(chalk.bold(`🔄 Invalidated:`));
97
+
98
+ for (const invalidatedPath of invalidatedPaths) {
99
+ // 만약 model.ts 파일이 변경(invalidate)되었다? 그러면 registeredApis 중에서 이 모델에 해당하는 api들은 지워줘요.
100
+ // registeredApis는 통으로 다 날려버릴 수 없습니다. registeredApis에 올라오는 친구들은 초기 로드시 또는 HMR시에만 등록되기 때문입니다.
101
+ // 따라서 model.ts 파일의 변경으로 다음번 새로운 eval이 예상되는 이 시점에서만, 이 모델에서 나온 registeredApis들을 지워줄 수 있습니다.
102
+ const removedApis = this.removeInvalidatedRegisteredApis(invalidatedPath);
103
+ if (removedApis.length > 0) {
104
+ console.log(
105
+ chalk.blue(`- ${path.relative(Sonamu.apiRootPath, invalidatedPath)}`),
106
+ chalk.gray(`(with ${removedApis.length} APIs)`),
107
+ );
108
+ } else {
109
+ console.log(chalk.blue(`- ${path.relative(Sonamu.apiRootPath, invalidatedPath)}`));
110
+ }
111
+ }
209
112
  }
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);
113
+ }
220
114
 
221
- // 변경된 파일 찾기
222
- const diff = _.differenceWith(
223
- currentChecksums,
224
- previousChecksums,
225
- _.isEqual
115
+ const isInCheckPatternGroup = Object.values(getChecksumPatternGroupInAbsolutePath()).some(
116
+ (pattern) => minimatch(diffFilePath, pattern),
226
117
  );
227
- const diffFiles = diff.map((r) => r.path);
228
- console.log("Changed Files: ", diffFiles);
229
118
 
230
- const { changedChecksums } = await this.doSyncActions(
231
- diffFiles,
232
- currentChecksums
233
- );
234
- // checksum 오버라이드 (액션 실행 과정 중간에 체크섬이 바뀐 경우)
235
- currentChecksums = changedChecksums ?? currentChecksums;
119
+ // 일(sync)이 있으면 합니다.
120
+ if (isInCheckPatternGroup) {
121
+ await this.doSyncActions([diffFilePath]);
122
+ }
236
123
 
237
- // 저장
238
- await this.saveChecksums(currentChecksums);
124
+ // 싱크 작업이 끝나면 모든 모듈을 로드합니다.
125
+ // hot-hook에 의해 invalidate된 부분들이 아니라면 캐시 그대로 유지합니다.
126
+ await this.autoloadTypes();
127
+ await this.autoloadModels();
128
+ await this.autoloadApis();
239
129
 
240
- // 싱크 종료
241
- this.isSyncing = false;
242
- abc.abort();
243
- process.off("SIGUSR2", onSIGUSR2);
130
+ this.syncUI();
244
131
  }
245
132
 
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;
133
+ removeInvalidatedRegisteredApis(
134
+ invalidatedPath: AbsolutePath,
135
+ ): (typeof registeredApis)[number][] {
136
+ if (!invalidatedPath.endsWith(".model.ts" /*소스 코드를 다루는 상황이니 .ts 경로로 봅니다.*/)) {
137
+ return [];
138
+ }
260
139
 
261
- // 변경된 파일들을 타입별로 분리하여 각 타입별 액션 처리
262
- const diffTypes = Object.keys(diffGroups);
140
+ const entityId = EntityManager.getEntityIdFromPath(invalidatedPath);
141
+ const toRemove = registeredApis.filter((api) => api.modelName === `${entityId}Model`);
142
+ for (const api of toRemove) {
143
+ registeredApis.splice(registeredApis.indexOf(api), 1);
144
+ }
263
145
 
264
- // 트리거: entity, types
265
- // 액션: 스키마 생성
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`
146
+ return toRemove;
147
+ }
148
+
149
+ async copySharedToTargets(targets: string[]): Promise<void> {
150
+ for (const target of targets) {
151
+ // 지금 가져가려는 이 파일은 Sonamu 코드베이스의 일부입니다.
152
+ // 그런데 dist 속 빌드된 소스 코드 파일이 필요한 것이 아니고, src에만 있는 텍스트 파일이 필요합니다.
153
+ // 따라서 /src/에서 찾습니다.
154
+ const srcPath = path.join(
155
+ import.meta.dirname.replace("/dist/", "/src/"),
156
+ `../shared/${target}.shared.ts.txt`,
157
+ );
158
+ if (!(await exists(srcPath))) {
159
+ return;
160
+ }
161
+ if (!(await exists(path.join(Sonamu.appRootPath, target)))) {
162
+ throw new Error(
163
+ `Tried to copy sonamu.shared.ts to target '${target}' but the target directory does not exist. Please check your project directory structure.`,
281
164
  );
282
- if (entity.parentId === undefined && !(await exists(typeFilePath))) {
283
- await this.generateTemplate("init_types", { entityId });
284
- }
285
165
  }
286
166
 
287
- // generated 싱크까지 동시에 처리 체크섬 갱신
288
- diffGroups["generated"] = _.uniq([
289
- ...(diffGroups["generated"] ?? []),
290
- "/src/application/sonamu.generated.ts",
291
- ]);
292
- diffTypes.push("generated");
167
+ // 이건 프로젝트에 .ts 소스 코드 파일을 생성하는 것이므로 src의 .ts 경로로 갑니다.
168
+ const destPath = path.join(Sonamu.appRootPath, target, "./sonamu.shared.ts");
293
169
 
294
- // fullSync인 경우만 실행
295
- if (currentChecksums) {
296
- currentChecksums = await this.getCurrentChecksums();
170
+ // 정말 혹시나지만 target 디렉토리는 있어도 src/services 디렉토리는 없을 수 있으므로 미리 생성해줍니다.
171
+ if (!(await exists(path.dirname(destPath)))) {
172
+ await mkdir(path.dirname(destPath), { recursive: true });
173
+ console.warn(`Created directory '${path.dirname(destPath)}' because it did not exist.`);
297
174
  }
175
+
176
+ if (await areFilesSame(srcPath, destPath)) {
177
+ return;
178
+ }
179
+
180
+ await writeFile(destPath, await readFile(srcPath));
181
+
182
+ !isTest() &&
183
+ console.log(
184
+ chalk.bold("Copied: ") + chalk.blue(path.relative(Sonamu.appRootPath, destPath)),
185
+ );
186
+ }
187
+ }
188
+
189
+ async autoloadTypes() {
190
+ this.types = await loadTypes();
191
+ }
192
+
193
+ async autoloadModels() {
194
+ this.models = await loadModels();
195
+ }
196
+
197
+ async autoloadApis() {
198
+ this.apis = await loadApis();
199
+ }
200
+
201
+ /**
202
+ * 실제 싱크를 수행하는 본체입니다.
203
+ * 변경된 파일들을 타입별로 분류하고 각 타입에 맞는 액션을 실행합니다.
204
+ * @param diffFilePaths - 변경된 파일들의 절대 경로 목록
205
+ * @returns diffTypes - 변경된 파일의 타입 목록 (entity, types, model 등)
206
+ */
207
+ async doSyncActions(diffFilePaths: AbsolutePath[]): Promise<{ diffTypes: string[] }> {
208
+ const diffGroups = this.calculateDiffGroups(diffFilePaths);
209
+ const diffTypes = Object.keys(diffGroups);
210
+
211
+ // 트리거: entity, types
212
+ // 액션: 스키마 생성
213
+ if (diffTypes.includes("entity")) {
214
+ await this.handleEntityChange(diffGroups, diffTypes);
298
215
  }
299
216
 
300
217
  // 트리거: types, enums, generated 변경시
@@ -304,244 +221,192 @@ export class Syncer {
304
221
  diffTypes.includes("functions") ||
305
222
  diffTypes.includes("generated")
306
223
  ) {
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);
224
+ await this.handleTypesOrFunctionsOrGeneratedChange(diffGroups);
315
225
  }
316
226
 
317
227
  // 트리거: model
318
228
  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);
229
+ await this.handleModelOrFrameChange(diffGroups);
230
+ }
361
231
 
362
- await this.actionGenerateHttps();
232
+ // 트리거: config
233
+ if (diffTypes.includes("config")) {
234
+ await this.actionSyncConfig();
363
235
  }
364
236
 
365
237
  return {
366
238
  diffTypes,
367
- changedChecksums: currentChecksums,
368
239
  };
369
240
  }
370
241
 
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
- });
242
+ calculateDiffGroups(diffFiles: AbsolutePath[]): DiffGroups {
243
+ return group(diffFiles, (r) => {
244
+ const matched = r.match(/\.(model|types|functions|entity|generated|frame|config)\.[tj]s/);
245
+ return matched?.[1] ?? "unknown";
246
+ }) as unknown as DiffGroups;
247
+ }
394
248
 
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
- }
249
+ async handleEntityChange(diffGroups: DiffGroups, diffTypes: string[]): Promise<void> {
250
+ Naite.t("handleEntityChange", { diffGroups, diffTypes });
251
+
252
+ await EntityManager.reload();
412
253
 
413
- console.log(
414
- chalk.bold("Transpiled: ") +
415
- chalk.blue(`${jsPath.replace(Sonamu.apiRootPath, "api")}`)
416
- );
417
- return jsPath;
418
- })
254
+ // types 생성(entity 새로 추가된 경우)
255
+ // parentId가 없고, types가 없는 경우에만 생성
256
+ const entityId = EntityManager.getEntityIdFromPath(diffGroups.entity?.[0]);
257
+
258
+ if (entityId) {
259
+ const entity = EntityManager.get(entityId);
260
+ // 프로젝트에 생성되어야 하는 .ts 파일의 경로입니다.
261
+ const typeFilePath = path.join(
262
+ Sonamu.apiRootPath,
263
+ `src/application/${entity.names.fs}/${entity.names.fs}.types.ts`,
419
264
  );
420
- transpiledFilePaths.push(..._transpiledFilePaths);
265
+ if (entity.parentId === undefined && !(await exists(typeFilePath))) {
266
+ await generateTemplate("init_types", { entityId });
267
+ }
421
268
  }
422
269
 
423
- // module reload - doSyncActions 전에 캐시 삭제
424
- function clearModuleAndDependents(filePath: string) {
425
- const resolved = require.resolve(filePath);
426
- const toDelete = new Set([resolved]);
270
+ await this.actionGenerateSchemas();
427
271
 
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
- });
272
+ diffGroups.generated = unique([
273
+ ...(diffGroups.generated ?? []),
274
+ path.join(Sonamu.apiRootPath, "src/application/sonamu.generated.ts") as AbsolutePath,
275
+ ]);
276
+ diffTypes.push("generated");
277
+ }
435
278
 
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
- });
279
+ async handleTypesOrFunctionsOrGeneratedChange(diffGroups: DiffGroups): Promise<FileType[]> {
280
+ const tsPaths = unique([
281
+ ...(diffGroups.types ?? []),
282
+ ...(diffGroups.functions ?? []),
283
+ ...(diffGroups.generated ?? []),
284
+ ]);
285
+ Naite.t("handleTypesOrFunctionsOrGeneratedChange", { diffGroups });
450
286
 
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);
287
+ // console.log(
288
+ // chalk.gray(
289
+ // `[Processing] Handling types/functions/generated changes: ${tsPaths.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
290
+ // )
291
+ // );
461
292
 
462
- this.apis = [];
463
- this.types = {};
464
- this.models = {};
465
- await this.autoloadTypes();
293
+ await this.actionSyncFilesToTargets(tsPaths);
294
+
295
+ return [];
296
+ }
297
+
298
+ async handleModelOrFrameChange(diffGroups: DiffGroups): Promise<void> {
299
+ Naite.t("handleModelOrFrameChange", { diffGroups });
300
+ const mergedGroup = [...(diffGroups.model ?? []), ...(diffGroups.frame ?? [])];
301
+
302
+ // console.log(
303
+ // chalk.gray(
304
+ // `[Processing] Handling model/frame changes: ${mergedGroup.map((p) => path.relative(Sonamu.apiRootPath, p)).join(", ")}`
305
+ // )
306
+ // );
307
+
308
+ // generated_http.template.ts에서 syncer.types를 씁니다.
309
+ // service.template.ts에서 syncer.apis를 씁니다.
466
310
  await this.autoloadModels();
311
+ await this.autoloadTypes();
467
312
  await this.autoloadApis();
468
313
 
469
- this.syncUI();
314
+ const params: {
315
+ namesRecord: EntityNamesRecord;
316
+ }[] = mergedGroup.map((modelPath) => {
317
+ if (modelPath.endsWith(".model.ts")) {
318
+ const entityId = EntityManager.getEntityIdFromPath(modelPath);
319
+ assert(entityId);
320
+ return {
321
+ namesRecord: EntityManager.getNamesFromId(entityId),
322
+ };
323
+ }
324
+ if (modelPath.endsWith("frame.ts")) {
325
+ const [, frameName] = modelPath.match(/.+\/(.+)\.frame.js$/) ?? [];
326
+ assert(frameName);
327
+ return {
328
+ namesRecord: EntityManager.getNamesFromId(frameName),
329
+ };
330
+ }
331
+ throw new Error("not reachable");
332
+ });
333
+
334
+ await this.actionGenerateServices(params);
335
+ await this.actionGenerateHttps();
470
336
  }
471
337
 
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, "_"));
478
- })
338
+ // web/.sonamu.env 현재 설정값 저장
339
+ async actionSyncConfig() {
340
+ const { host, port } = Sonamu.config.server.listen ?? {};
341
+ const content = `API_HOST=${host ?? "localhost"}\nAPI_PORT=${port ?? 3000}`;
342
+
343
+ Naite.t("actionSyncConfig", { content });
344
+ await Promise.all(
345
+ Sonamu.config.sync.targets.map(async (target) => {
346
+ await writeFile(path.join(Sonamu.appRootPath, target, ".sonamu.env"), content);
347
+ }),
479
348
  );
480
349
  }
481
350
 
482
- async actionGenerateSchemas(): Promise<string[]> {
351
+ /**
352
+ * sonamu.generated.ts와 sonamu.generated.sso.ts를 생성합니다.
353
+ * @returns 생성된 파일 경로 배열.
354
+ */
355
+ async actionGenerateSchemas(): Promise<AbsolutePath[]> {
483
356
  return (
484
357
  await Promise.all([
485
- this.generateTemplate("generated_sso", {}, { overwrite: true }),
486
- this.generateTemplate("generated", {}, { overwrite: true }),
358
+ generateTemplate("generated_sso", {}, { overwrite: true }),
359
+ generateTemplate("generated", {}, { overwrite: true }),
487
360
  ])
488
361
  )
489
362
  .flat()
490
363
  .flat();
491
364
  }
492
365
 
366
+ /**
367
+ * *.service.ts를 생성합니다.
368
+ * @param paramsArray
369
+ * @returns 생성된 파일 경로 배열.
370
+ */
493
371
  async actionGenerateServices(
494
372
  paramsArray: {
495
373
  namesRecord: EntityNamesRecord;
496
- modelTsPath: string;
497
- }[]
374
+ }[],
498
375
  ): Promise<string[]> {
376
+ Naite.t("actionGenerateServices", paramsArray);
499
377
  return (
500
378
  await Promise.all(
501
379
  paramsArray.map(async (params) =>
502
- this.generateTemplate("service", params, {
380
+ generateTemplate("service", params as TemplateOptions["service"], {
503
381
  overwrite: true,
504
- })
505
- )
382
+ }),
383
+ ),
506
384
  )
507
385
  )
508
386
  .flat()
509
387
  .flat();
510
388
  }
511
389
 
512
- async actionGenerateHttps(): Promise<string[]> {
513
- const [res] = await this.generateTemplate(
390
+ /**
391
+ * sonamu.generated.http를 생성합니다.
392
+ * @returns 생성된 파일 경로.
393
+ */
394
+ async actionGenerateHttps(): Promise<AbsolutePath> {
395
+ const [res] = await generateTemplate(
514
396
  "generated_http",
515
- {},
516
- { overwrite: true }
397
+ { entityId: "dummy" },
398
+ { overwrite: true },
517
399
  );
518
400
  assert(res);
519
401
  return res;
520
402
  }
521
403
 
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[]> {
404
+ /**
405
+ * *.types.ts, *.functions.ts, *.generated.ts를 타겟 디렉토리에 복사합니다.
406
+ * @param tsPaths
407
+ * @returns 복사된 파일 경로 배열.
408
+ */
409
+ async actionSyncFilesToTargets(tsPaths: AbsolutePath[]): Promise<string[]> {
545
410
  const { targets } = Sonamu.config.sync;
546
411
  const { dir: apiDir } = Sonamu.config.api;
547
412
 
@@ -549,8 +414,7 @@ export class Syncer {
549
414
  await Promise.all(
550
415
  targets.map(async (target) =>
551
416
  Promise.all(
552
- tsPaths.map(async (src) => {
553
- const realSrc = Sonamu.apiRootPath + src;
417
+ tsPaths.map(async (realSrc) => {
554
418
  const dst = realSrc
555
419
  .replace(`/${apiDir}/`, `/${target}/`)
556
420
  .replace("/application/", "/services/");
@@ -558,686 +422,52 @@ export class Syncer {
558
422
  if (!(await exists(dir))) {
559
423
  await mkdir(dir, { recursive: true });
560
424
  }
561
- console.log(
562
- chalk.bold("Copied: ") +
563
- chalk.blue(
564
- `Copied: ${dst.replace(Sonamu.appRootPath + "/", "")}`
565
- )
566
- );
425
+ !isTest() &&
426
+ console.log(
427
+ chalk.bold("Copied: ") + chalk.blue(dst.replace(`${Sonamu.appRootPath}/`, "")),
428
+ );
567
429
  await this.copyFileWithReplaceCoreToShared(realSrc, dst);
568
430
  return dst;
569
- })
570
- )
571
- )
572
- )
573
- ).flat();
574
- }
575
-
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)
431
+ }),
845
432
  ),
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")
433
+ ),
960
434
  )
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
- }
435
+ ).flat();
1035
436
  }
1036
437
 
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();
438
+ private async copyFileWithReplaceCoreToShared(fromPath: string, toPath: string) {
439
+ if (!(await exists(fromPath))) {
440
+ return;
1080
441
  }
1081
442
 
1082
- return [resolved, ...preTemplateResolved];
1083
- }
1084
-
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
1127
- );
1128
-
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();
443
+ const oldFileContent = (await readFile(fromPath)).toString();
1199
444
 
1200
- const filteredPathAndCodes: PathAndCode[] = await (async () => {
1201
- if (generateOptions.overwrite === true) {
1202
- return pathAndCodes;
1203
- } 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
- });
1215
- }
445
+ const newFileContent = (() => {
446
+ const nfc = oldFileContent.replace(/from "sonamu"/g, `from "./sonamu.shared"`);
447
+ return nfc;
1216
448
  })();
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
- );
449
+ return writeFile(toPath, newFileContent);
1228
450
  }
1229
451
 
452
+ /**
453
+ * 주어진 엔티티와 템플릿 키에 대해, 생성된 코드가 존재하는지 확인합니다.
454
+ * @param entityId 엔티티 ID
455
+ * @param templateKey 템플릿 키
456
+ * @param enumId 열거형 ID
457
+ * @returns 생성된 코드가 존재하는지 여부
458
+ */
1230
459
  async checkExistsGenCode(
1231
460
  entityId: string,
1232
461
  templateKey: TemplateKey,
1233
- enumId?: string
462
+ enumId?: string,
1234
463
  ): Promise<{ subPath: string; fullPath: string; isExists: boolean }> {
1235
- const { target, path: genPath } = this.getTemplate(
1236
- templateKey
1237
- ).getTargetAndPath(EntityManager.getNamesFromId(entityId), enumId);
464
+ const { target, path: genPath } = TemplateManager.get(templateKey).getTargetAndPath(
465
+ EntityManager.getNamesFromId(entityId),
466
+ enumId,
467
+ );
1238
468
 
1239
- const fullPath = path.join(Sonamu.appRootPath, target, genPath);
1240
469
  const subPath = path.join(target, genPath);
470
+ const fullPath = path.join(Sonamu.appRootPath, subPath);
1241
471
  return {
1242
472
  subPath,
1243
473
  fullPath,
@@ -1245,30 +475,31 @@ export class Syncer {
1245
475
  };
1246
476
  }
1247
477
 
478
+ /**
479
+ * 주어진 엔티티와 열거형에 대해, 생성된 코드가 존재하는지 확인합니다.
480
+ * @param entityId 엔티티 ID
481
+ * @param enums 열거형 레이블
482
+ * @returns 생성된 코드가 존재하는지 여부
483
+ */
1248
484
  async checkExists(
1249
485
  entityId: string,
1250
486
  enums: {
1251
- [name: string]: z.ZodEnum<any>;
1252
- }
487
+ [name: string]: z.ZodEnum<Readonly<Record<string, string | number>>>;
488
+ },
1253
489
  ): Promise<Record<`${TemplateKey}${string}`, boolean>> {
1254
490
  const keys: TemplateKey[] = TemplateKey.options;
1255
491
  const names = EntityManager.getNamesFromId(entityId);
1256
- const enumsKeys = Object.keys(enums).filter(
1257
- (name) => name !== names.constant
1258
- );
492
+ const enumsKeys = Object.keys(enums).filter((name) => name !== names.constant);
1259
493
 
1260
494
  return await reduceAsync(
1261
495
  keys,
1262
496
  async (result, key) => {
1263
- const tpl = this.getTemplate(key);
497
+ const tpl = TemplateManager.get(key);
1264
498
  if (key.startsWith("view_enums")) {
1265
499
  await mapAsync(enumsKeys, async (componentId) => {
1266
- const { target, path: p } = tpl.getTargetAndPath(
1267
- names,
1268
- componentId
1269
- );
500
+ const { target, path: p } = tpl.getTargetAndPath(names, componentId);
1270
501
  result[`${key}__${componentId}`] = await exists(
1271
- path.join(Sonamu.appRootPath, target, p)
502
+ path.join(Sonamu.appRootPath, target, p),
1272
503
  );
1273
504
  });
1274
505
  return result;
@@ -1279,7 +510,7 @@ export class Syncer {
1279
510
  if (target.includes(":target")) {
1280
511
  await mapAsync(targets, async (t) => {
1281
512
  result[`${key}__${t}`] = await exists(
1282
- path.join(Sonamu.appRootPath, target.replace(":target", t), p)
513
+ path.join(Sonamu.appRootPath, target.replace(":target", t), p),
1283
514
  );
1284
515
  });
1285
516
  } else {
@@ -1288,354 +519,59 @@ export class Syncer {
1288
519
 
1289
520
  return result;
1290
521
  },
1291
- {} as Record<`${TemplateKey}${string}`, boolean>
1292
- );
1293
- }
1294
-
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"
522
+ {} as Record<`${TemplateKey}${string}`, boolean>,
1302
523
  );
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
524
  }
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
525
 
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
- }
526
+ syncUI() {
527
+ const uiPort = Sonamu.config.ui?.port ?? 57000;
1405
528
 
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}`);
529
+ if (!isTest()) {
530
+ fetch(`http://127.0.0.1:${uiPort}/api/reload`, {
531
+ method: "GET",
532
+ }).catch((e) => console.log(chalk.dim(`Failed to reload Sonamu UI: ${e.message}`)));
1444
533
  }
1445
534
  }
1446
535
 
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
- }
536
+ /**
537
+ * 하위호환용 프록시 메소드입니다.
538
+ */
539
+ async createEntity(form: TemplateOptions["entity"]) {
540
+ return await createEntity(form);
1502
541
  }
1503
542
 
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
- });
1565
-
1566
- return columnsNode;
543
+ /**
544
+ * 하위호환용 프록시 메소드입니다.
545
+ */
546
+ async delEntity(entityId: string): Promise<{ delPaths: string[] }> {
547
+ return await delEntity(entityId);
1567
548
  }
1568
549
 
1569
- async createEntity(
1570
- form: Omit<TemplateOptions["entity"], "title"> & { title?: string }
1571
- ) {
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
+ /**
551
+ * 하위호환용 프록시 메소드입니다.
552
+ */
553
+ async generateTemplate<T extends TemplateKey>(
554
+ key: T,
555
+ templateOptions: TemplateOptions[T],
556
+ _generateOptions?: GenerateOptions,
557
+ ): Promise<AbsolutePath[]> {
558
+ return await generateTemplate(key, templateOptions, _generateOptions);
1594
559
  }
1595
560
 
1596
- 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();
1628
-
1629
- return { delPaths };
561
+ /**
562
+ * 하위호환용 프록시 메소드입니다.
563
+ */
564
+ async renderTemplate<T extends keyof TemplateOptions>(
565
+ key: T,
566
+ templateOptions: TemplateOptions[T],
567
+ ): Promise<PathAndCode[]> {
568
+ return await renderTemplate(key, templateOptions);
1630
569
  }
1631
570
 
1632
- syncUI() {
1633
- const uiPort = Sonamu.config.ui?.port ?? 57000;
1634
-
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
- );
571
+ /**
572
+ * 하위호환용 프록시 메소드입니다.
573
+ */
574
+ async renewChecksums(): Promise<void> {
575
+ return await renewChecksums();
1640
576
  }
1641
577
  }