sonamu 0.10.11 → 0.10.13

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 (519) hide show
  1. package/dist/ai/agents/agent.d.ts +1 -1
  2. package/dist/ai/agents/agent.d.ts.map +1 -1
  3. package/dist/ai/agents/agent.js +5 -6
  4. package/dist/ai/agents/types.d.ts +1 -1
  5. package/dist/ai/agents/types.d.ts.map +1 -1
  6. package/dist/ai/providers/rtzr/api.js +18 -18
  7. package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
  8. package/dist/ai/providers/rtzr/error.js +17 -15
  9. package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
  10. package/dist/ai/providers/rtzr/model.js +3 -2
  11. package/dist/ai/providers/rtzr/options.d.ts.map +1 -1
  12. package/dist/ai/providers/rtzr/options.js +16 -16
  13. package/dist/ai/providers/rtzr/provider.d.ts.map +1 -1
  14. package/dist/ai/providers/rtzr/provider.js +9 -9
  15. package/dist/ai/providers/rtzr/utils.d.ts.map +1 -1
  16. package/dist/ai/providers/rtzr/utils.js +1 -1
  17. package/dist/api/caster.d.ts.map +1 -1
  18. package/dist/api/caster.js +17 -17
  19. package/dist/api/code-converters.d.ts +4 -2
  20. package/dist/api/code-converters.d.ts.map +1 -1
  21. package/dist/api/code-converters.js +100 -19
  22. package/dist/api/config.d.ts +16 -1
  23. package/dist/api/config.d.ts.map +1 -1
  24. package/dist/api/config.js +28 -21
  25. package/dist/api/context.d.ts +2 -2
  26. package/dist/api/context.d.ts.map +1 -1
  27. package/dist/api/context.js +1 -1
  28. package/dist/api/decorators.d.ts.map +1 -1
  29. package/dist/api/decorators.js +18 -11
  30. package/dist/api/http-validator.d.ts +36 -0
  31. package/dist/api/http-validator.d.ts.map +1 -0
  32. package/dist/api/http-validator.js +72 -0
  33. package/dist/api/index.js +6 -4
  34. package/dist/api/sonamu.d.ts +59 -20
  35. package/dist/api/sonamu.d.ts.map +1 -1
  36. package/dist/api/sonamu.js +250 -106
  37. package/dist/api/websocket-helpers.d.ts +7 -5
  38. package/dist/api/websocket-helpers.d.ts.map +1 -1
  39. package/dist/api/websocket-helpers.js +6 -6
  40. package/dist/auth/audit-log/builders.d.ts.map +1 -1
  41. package/dist/auth/audit-log/builders.js +1 -1
  42. package/dist/auth/audit-log/client-ip.d.ts +1 -1
  43. package/dist/auth/audit-log/client-ip.d.ts.map +1 -1
  44. package/dist/auth/audit-log/client-ip.js +4 -2
  45. package/dist/auth/audit-log/events.d.ts +5 -1
  46. package/dist/auth/audit-log/events.d.ts.map +1 -1
  47. package/dist/auth/audit-log/events.js +1 -1
  48. package/dist/auth/audit-log/plugin.d.ts +0 -8
  49. package/dist/auth/audit-log/plugin.d.ts.map +1 -1
  50. package/dist/auth/audit-log/plugin.js +48 -52
  51. package/dist/auth/audit-log-ingestor.d.ts.map +1 -1
  52. package/dist/auth/audit-log-ingestor.js +16 -12
  53. package/dist/auth/auth-generator.d.ts.map +1 -1
  54. package/dist/auth/auth-generator.js +1 -1
  55. package/dist/auth/better-auth-request.d.ts.map +1 -1
  56. package/dist/auth/better-auth-request.js +6 -5
  57. package/dist/auth/knex-adapter.js +1 -1
  58. package/dist/auth/plugins/entity-definitions/index.d.ts +13 -1
  59. package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
  60. package/dist/auth/plugins/entity-definitions/index.js +1 -1
  61. package/dist/auth/plugins/wrappers/admin.d.ts +1 -1
  62. package/dist/bin/build-config.d.ts +25 -0
  63. package/dist/bin/build-config.d.ts.map +1 -1
  64. package/dist/bin/build-config.js +105 -2
  65. package/dist/bin/cli.js +133 -29
  66. package/dist/bin/compiler-policy.d.ts +3 -0
  67. package/dist/bin/compiler-policy.d.ts.map +1 -0
  68. package/dist/bin/compiler-policy.js +24 -0
  69. package/dist/bin/fixture.d.ts.map +1 -1
  70. package/dist/bin/fixture.js +5 -3
  71. package/dist/bin/test-command.d.ts +22 -1
  72. package/dist/bin/test-command.d.ts.map +1 -1
  73. package/dist/bin/test-command.js +18 -10
  74. package/dist/bin/ts-loader-registration.d.ts +7 -1
  75. package/dist/bin/ts-loader-registration.d.ts.map +1 -1
  76. package/dist/bin/ts-loader-registration.js +13 -4
  77. package/dist/cache/decorator.d.ts.map +1 -1
  78. package/dist/cache/decorator.js +11 -4
  79. package/dist/compress/compress.d.ts.map +1 -1
  80. package/dist/compress/compress.js +1 -1
  81. package/dist/cone/cone-generator.d.ts +14 -1
  82. package/dist/cone/cone-generator.d.ts.map +1 -1
  83. package/dist/cone/cone-generator.js +13 -9
  84. package/dist/database/_batch_update.js +1 -1
  85. package/dist/database/base-model.d.ts +22 -10
  86. package/dist/database/base-model.d.ts.map +1 -1
  87. package/dist/database/base-model.js +26 -16
  88. package/dist/database/base-model.types.d.ts +15 -3
  89. package/dist/database/base-model.types.d.ts.map +1 -1
  90. package/dist/database/db.d.ts +12 -2
  91. package/dist/database/db.d.ts.map +1 -1
  92. package/dist/database/db.js +10 -7
  93. package/dist/database/knex.d.ts.map +1 -1
  94. package/dist/database/knex.js +17 -14
  95. package/dist/database/puri.d.ts +11 -9
  96. package/dist/database/puri.d.ts.map +1 -1
  97. package/dist/database/puri.js +93 -79
  98. package/dist/database/puri.types.d.ts +21 -21
  99. package/dist/database/puri.types.d.ts.map +1 -1
  100. package/dist/database/puri.types.js +1 -1
  101. package/dist/database/upsert-builder.d.ts +12 -10
  102. package/dist/database/upsert-builder.d.ts.map +1 -1
  103. package/dist/database/upsert-builder.js +20 -12
  104. package/dist/dict/sd.d.ts.map +1 -1
  105. package/dist/dict/sd.js +1 -1
  106. package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
  107. package/dist/dict/sonamu-dictionary.js +1 -1
  108. package/dist/dict/utils.d.ts.map +1 -1
  109. package/dist/dict/utils.js +7 -2
  110. package/dist/entity/entity-manager.d.ts +13 -4
  111. package/dist/entity/entity-manager.d.ts.map +1 -1
  112. package/dist/entity/entity-manager.js +19 -11
  113. package/dist/entity/entity-template-cone.d.ts +73 -5
  114. package/dist/entity/entity-template-cone.d.ts.map +1 -1
  115. package/dist/entity/entity-template-cone.js +4 -2
  116. package/dist/entity/entity.d.ts +6 -2
  117. package/dist/entity/entity.d.ts.map +1 -1
  118. package/dist/entity/entity.js +30 -28
  119. package/dist/env.d.ts +3 -1
  120. package/dist/env.d.ts.map +1 -1
  121. package/dist/env.js +1 -1
  122. package/dist/exceptions/error-handler.d.ts.map +1 -1
  123. package/dist/exceptions/error-handler.js +7 -2
  124. package/dist/exceptions/so-exceptions.d.ts +24 -20
  125. package/dist/exceptions/so-exceptions.d.ts.map +1 -1
  126. package/dist/exceptions/so-exceptions.js +4 -2
  127. package/dist/filter/utils.d.ts +7 -2
  128. package/dist/filter/utils.d.ts.map +1 -1
  129. package/dist/filter/utils.js +8 -6
  130. package/dist/index.d.ts +3 -0
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +13 -7
  133. package/dist/logger/configure.d.ts.map +1 -1
  134. package/dist/logger/configure.js +10 -10
  135. package/dist/migration/code-generation.d.ts.map +1 -1
  136. package/dist/migration/code-generation.js +50 -46
  137. package/dist/migration/index-where-predicate.d.ts.map +1 -1
  138. package/dist/migration/index-where-predicate.js +22 -8
  139. package/dist/migration/migration-set.d.ts.map +1 -1
  140. package/dist/migration/migration-set.js +1 -1
  141. package/dist/migration/migrator.d.ts +61 -36
  142. package/dist/migration/migrator.d.ts.map +1 -1
  143. package/dist/migration/migrator.js +276 -167
  144. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  145. package/dist/migration/postgresql-schema-reader.js +51 -36
  146. package/dist/migration/slack-confirm.d.ts.map +1 -1
  147. package/dist/migration/slack-confirm.js +1 -1
  148. package/dist/migration/types.d.ts +54 -0
  149. package/dist/migration/types.d.ts.map +1 -1
  150. package/dist/naite/naite-reporter.js +2 -2
  151. package/dist/naite/naite.d.ts +74 -15
  152. package/dist/naite/naite.d.ts.map +1 -1
  153. package/dist/naite/naite.js +5 -19
  154. package/dist/runtime-type.d.ts +6 -0
  155. package/dist/runtime-type.d.ts.map +1 -0
  156. package/dist/runtime-type.js +26 -0
  157. package/dist/ssr/hmr-opt-out.d.ts +26 -0
  158. package/dist/ssr/hmr-opt-out.d.ts.map +1 -0
  159. package/dist/ssr/hmr-opt-out.js +59 -0
  160. package/dist/ssr/renderer.d.ts.map +1 -1
  161. package/dist/ssr/renderer.js +4 -1
  162. package/dist/storage/base-file.js +14 -14
  163. package/dist/storage/buffered-file.d.ts +2 -2
  164. package/dist/storage/buffered-file.d.ts.map +1 -1
  165. package/dist/storage/buffered-file.js +12 -12
  166. package/dist/storage/s3-driver.d.ts.map +1 -1
  167. package/dist/storage/s3-driver.js +1 -1
  168. package/dist/storage/uploaded-file.d.ts +2 -2
  169. package/dist/storage/uploaded-file.d.ts.map +1 -1
  170. package/dist/storage/uploaded-file.js +8 -8
  171. package/dist/stream/sse.js +11 -11
  172. package/dist/stream/ws-core.d.ts +1 -1
  173. package/dist/stream/ws-core.d.ts.map +1 -1
  174. package/dist/stream/ws-delivery.d.ts +1 -1
  175. package/dist/stream/ws-delivery.d.ts.map +1 -1
  176. package/dist/stream/ws-delivery.js +6 -4
  177. package/dist/stream/ws-local-connection-store.d.ts.map +1 -1
  178. package/dist/stream/ws-local-connection-store.js +4 -2
  179. package/dist/stream/ws-presence-store.js +1 -1
  180. package/dist/stream/ws-registry.d.ts +4 -4
  181. package/dist/stream/ws-registry.d.ts.map +1 -1
  182. package/dist/stream/ws-registry.js +4 -2
  183. package/dist/stream/ws-telemetry.d.ts +16 -12
  184. package/dist/stream/ws-telemetry.d.ts.map +1 -1
  185. package/dist/stream/ws-telemetry.js +22 -18
  186. package/dist/stream/ws.d.ts +18 -12
  187. package/dist/stream/ws.d.ts.map +1 -1
  188. package/dist/stream/ws.js +26 -22
  189. package/dist/syncer/api-parser.js +1 -1
  190. package/dist/syncer/checksum.d.ts.map +1 -1
  191. package/dist/syncer/checksum.js +7 -8
  192. package/dist/syncer/code-generator.d.ts.map +1 -1
  193. package/dist/syncer/code-generator.js +13 -14
  194. package/dist/syncer/entity-operations.d.ts.map +1 -1
  195. package/dist/syncer/entity-operations.js +5 -6
  196. package/dist/syncer/file-copy.d.ts +5 -0
  197. package/dist/syncer/file-copy.d.ts.map +1 -0
  198. package/dist/syncer/file-copy.js +36 -0
  199. package/dist/syncer/file-patterns.d.ts +1 -0
  200. package/dist/syncer/file-patterns.d.ts.map +1 -1
  201. package/dist/syncer/file-patterns.js +5 -2
  202. package/dist/syncer/file-tracking.d.ts.map +1 -1
  203. package/dist/syncer/file-tracking.js +5 -4
  204. package/dist/syncer/filesystem-dependencies.d.ts +15 -0
  205. package/dist/syncer/filesystem-dependencies.d.ts.map +1 -0
  206. package/dist/syncer/filesystem-dependencies.js +53 -0
  207. package/dist/syncer/module-loader.d.ts +1 -1
  208. package/dist/syncer/module-loader.d.ts.map +1 -1
  209. package/dist/syncer/module-loader.js +6 -4
  210. package/dist/syncer/syncer-actions.d.ts +4 -0
  211. package/dist/syncer/syncer-actions.d.ts.map +1 -1
  212. package/dist/syncer/syncer-actions.js +34 -17
  213. package/dist/syncer/syncer.d.ts +17 -6
  214. package/dist/syncer/syncer.d.ts.map +1 -1
  215. package/dist/syncer/syncer.js +71 -29
  216. package/dist/syncer/watcher.d.ts.map +1 -1
  217. package/dist/syncer/watcher.js +3 -3
  218. package/dist/tasks/decorator.d.ts +6 -6
  219. package/dist/tasks/decorator.d.ts.map +1 -1
  220. package/dist/tasks/decorator.js +4 -4
  221. package/dist/tasks/step-wrapper.d.ts +6 -4
  222. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  223. package/dist/tasks/step-wrapper.js +5 -5
  224. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  225. package/dist/tasks/workflow-manager.js +13 -7
  226. package/dist/template/entity-converter.d.ts +1 -1
  227. package/dist/template/entity-converter.d.ts.map +1 -1
  228. package/dist/template/entity-converter.js +2 -2
  229. package/dist/template/helpers.d.ts +8 -6
  230. package/dist/template/helpers.d.ts.map +1 -1
  231. package/dist/template/helpers.js +1 -1
  232. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  233. package/dist/template/implementations/generated.template.js +4 -4
  234. package/dist/template/implementations/generated_http.template.d.ts +7 -4
  235. package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
  236. package/dist/template/implementations/generated_http.template.js +21 -21
  237. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  238. package/dist/template/implementations/generated_sso.template.js +6 -3
  239. package/dist/template/implementations/http_validators.template.d.ts +18 -0
  240. package/dist/template/implementations/http_validators.template.d.ts.map +1 -0
  241. package/dist/template/implementations/http_validators.template.js +127 -0
  242. package/dist/template/implementations/model.template.d.ts.map +1 -1
  243. package/dist/template/implementations/model.template.js +1 -1
  244. package/dist/template/implementations/queries.template.js +1 -1
  245. package/dist/template/implementations/sd.template.d.ts.map +1 -1
  246. package/dist/template/implementations/sd.template.js +79 -39
  247. package/dist/template/implementations/services.template.d.ts.map +1 -1
  248. package/dist/template/implementations/services.template.js +6 -4
  249. package/dist/template/implementations/view_form.template.d.ts +6 -1
  250. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  251. package/dist/template/implementations/view_form.template.js +23 -15
  252. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  253. package/dist/template/implementations/view_list.template.js +11 -6
  254. package/dist/template/template-manager.d.ts.map +1 -1
  255. package/dist/template/template-manager.js +9 -7
  256. package/dist/template/template-types.d.ts +12 -5
  257. package/dist/template/template-types.d.ts.map +1 -1
  258. package/dist/template/template-types.js +1 -1
  259. package/dist/template/template.d.ts +2 -2
  260. package/dist/template/template.d.ts.map +1 -1
  261. package/dist/template/template.js +6 -4
  262. package/dist/template/zod-converter.d.ts +0 -19
  263. package/dist/template/zod-converter.d.ts.map +1 -1
  264. package/dist/template/zod-converter.js +100 -84
  265. package/dist/testing/_relation-graph.d.ts.map +1 -1
  266. package/dist/testing/_relation-graph.js +2 -1
  267. package/dist/testing/bootstrap.d.ts.map +1 -1
  268. package/dist/testing/bootstrap.js +9 -6
  269. package/dist/testing/data-explorer.d.ts +32 -7
  270. package/dist/testing/data-explorer.d.ts.map +1 -1
  271. package/dist/testing/data-explorer.js +77 -31
  272. package/dist/testing/dev-test-routes.d.ts +8 -1
  273. package/dist/testing/dev-test-routes.d.ts.map +1 -1
  274. package/dist/testing/dev-test-routes.js +125 -108
  275. package/dist/testing/dev-vitest-manager.d.ts +54 -8
  276. package/dist/testing/dev-vitest-manager.d.ts.map +1 -1
  277. package/dist/testing/dev-vitest-manager.js +24 -9
  278. package/dist/testing/faker-mappings.d.ts +412 -1
  279. package/dist/testing/faker-mappings.d.ts.map +1 -1
  280. package/dist/testing/faker-mappings.js +1 -1
  281. package/dist/testing/filesystem.d.ts +2 -0
  282. package/dist/testing/filesystem.d.ts.map +1 -0
  283. package/dist/testing/filesystem.js +7 -0
  284. package/dist/testing/fixture-generator.d.ts +25 -8
  285. package/dist/testing/fixture-generator.d.ts.map +1 -1
  286. package/dist/testing/fixture-generator.js +132 -73
  287. package/dist/testing/fixture-loader.d.ts +3 -1
  288. package/dist/testing/fixture-loader.d.ts.map +1 -1
  289. package/dist/testing/fixture-loader.js +1 -20
  290. package/dist/testing/fixture-manager.d.ts +8 -7
  291. package/dist/testing/fixture-manager.d.ts.map +1 -1
  292. package/dist/testing/fixture-manager.js +91 -59
  293. package/dist/testing/global-setup.d.ts.map +1 -1
  294. package/dist/testing/global-setup.js +5 -2
  295. package/dist/testing/index.d.ts +2 -0
  296. package/dist/testing/index.d.ts.map +1 -1
  297. package/dist/testing/index.js +6 -3
  298. package/dist/types/types.d.ts +101 -99
  299. package/dist/types/types.d.ts.map +1 -1
  300. package/dist/types/types.js +234 -225
  301. package/dist/ui/ai-api.d.ts.map +1 -1
  302. package/dist/ui/ai-api.js +1 -1
  303. package/dist/ui/ai-client.d.ts.map +1 -1
  304. package/dist/ui/ai-client.js +28 -18
  305. package/dist/ui/api.d.ts.map +1 -1
  306. package/dist/ui/api.js +146 -210
  307. package/dist/ui/async-route.d.ts +3 -0
  308. package/dist/ui/async-route.d.ts.map +1 -0
  309. package/dist/ui/async-route.js +12 -0
  310. package/dist/ui/cdd-service.d.ts +9 -7
  311. package/dist/ui/cdd-service.d.ts.map +1 -1
  312. package/dist/ui/cdd-service.js +7 -5
  313. package/dist/ui/migrations-api.d.ts +3 -0
  314. package/dist/ui/migrations-api.d.ts.map +1 -0
  315. package/dist/ui/migrations-api.js +215 -0
  316. package/dist/ui-web/assets/index-D3QNH-Dv.css +1 -0
  317. package/dist/ui-web/assets/index-DV1AAq6W.js +178 -0
  318. package/dist/ui-web/index.html +2 -2
  319. package/dist/utils/formatter.d.ts.map +1 -1
  320. package/dist/utils/formatter.js +7 -5
  321. package/dist/utils/fs-utils.d.ts +3 -3
  322. package/dist/utils/fs-utils.d.ts.map +1 -1
  323. package/dist/utils/fs-utils.js +6 -6
  324. package/dist/utils/model.d.ts +3 -5
  325. package/dist/utils/model.d.ts.map +1 -1
  326. package/dist/utils/model.js +1 -1
  327. package/dist/utils/object-utils.d.ts +1 -1
  328. package/dist/utils/object-utils.d.ts.map +1 -1
  329. package/dist/utils/object-utils.js +17 -16
  330. package/dist/utils/process-utils.d.ts.map +1 -1
  331. package/dist/utils/process-utils.js +4 -2
  332. package/dist/utils/runtime-value.d.ts +13 -0
  333. package/dist/utils/runtime-value.d.ts.map +1 -0
  334. package/dist/utils/runtime-value.js +40 -0
  335. package/dist/utils/sql-parser.d.ts +8 -1
  336. package/dist/utils/sql-parser.d.ts.map +1 -1
  337. package/dist/utils/sql-parser.js +6 -3
  338. package/dist/utils/type-utils.d.ts.map +1 -1
  339. package/dist/utils/type-utils.js +10 -8
  340. package/dist/utils/utils.d.ts +2 -2
  341. package/dist/utils/utils.d.ts.map +1 -1
  342. package/dist/utils/utils.js +11 -8
  343. package/dist/utils/zod-error.d.ts.map +1 -1
  344. package/dist/utils/zod-error.js +7 -4
  345. package/dist/vector/chunking.d.ts.map +1 -1
  346. package/dist/vector/chunking.js +1 -1
  347. package/dist/vector/embedding.d.ts.map +1 -1
  348. package/dist/vector/embedding.js +1 -1
  349. package/dist/vector/types.d.ts +3 -2
  350. package/dist/vector/types.d.ts.map +1 -1
  351. package/package.json +11 -7
  352. package/src/ai/agents/agent.ts +5 -5
  353. package/src/ai/agents/types.ts +1 -1
  354. package/src/ai/providers/rtzr/__tests__/model.test.ts +2 -2
  355. package/src/ai/providers/rtzr/error.ts +5 -3
  356. package/src/ai/providers/rtzr/model.ts +3 -2
  357. package/src/ai/providers/rtzr/options.ts +10 -5
  358. package/src/ai/providers/rtzr/provider.ts +10 -10
  359. package/src/ai/providers/rtzr/utils.ts +3 -1
  360. package/src/api/__tests__/config.test.ts +11 -21
  361. package/src/api/__tests__/sonamu.http-validator.test.ts +512 -0
  362. package/src/api/__tests__/sonamu.websocket.test.ts +51 -41
  363. package/src/api/__tests__/zod-compiler-policy.test.ts +95 -0
  364. package/src/api/caster.ts +6 -8
  365. package/src/api/code-converters.ts +136 -36
  366. package/src/api/config.ts +62 -4
  367. package/src/api/context.ts +3 -3
  368. package/src/api/decorators.ts +28 -17
  369. package/src/api/http-validator.ts +149 -0
  370. package/src/api/sonamu.ts +459 -152
  371. package/src/api/websocket-helpers.ts +24 -17
  372. package/src/auth/audit-log/builders.ts +3 -2
  373. package/src/auth/audit-log/client-ip.ts +4 -2
  374. package/src/auth/audit-log/events.ts +15 -1
  375. package/src/auth/audit-log/plugin.ts +129 -200
  376. package/src/auth/audit-log-ingestor.ts +19 -15
  377. package/src/auth/auth-generator.ts +10 -5
  378. package/src/auth/better-auth-request.ts +4 -3
  379. package/src/auth/knex-adapter.ts +8 -2
  380. package/src/auth/plugins/entity-definitions/index.ts +6 -5
  381. package/src/bin/__tests__/http-validator-benchmark.test.ts +94 -0
  382. package/src/bin/__tests__/test-command.test.ts +9 -15
  383. package/src/bin/__tests__/ts-loader-register.test.ts +15 -17
  384. package/src/bin/__tests__/zod-compiler-build.test.ts +307 -0
  385. package/src/bin/build-config.ts +167 -0
  386. package/src/bin/cli.ts +155 -38
  387. package/src/bin/compiler-policy.ts +35 -0
  388. package/src/bin/fixture.ts +14 -3
  389. package/src/bin/test-command.ts +74 -29
  390. package/src/bin/ts-loader-registration.ts +22 -4
  391. package/src/cache/decorator.ts +10 -3
  392. package/src/compress/compress.ts +17 -4
  393. package/src/cone/cone-generator.ts +34 -6
  394. package/src/database/__tests__/db.test.ts +1 -0
  395. package/src/database/__tests__/puri.test.ts +16 -15
  396. package/src/database/__tests__/transaction-scope.test.ts +5 -0
  397. package/src/database/_batch_update.ts +1 -0
  398. package/src/database/base-model.ts +81 -50
  399. package/src/database/base-model.types.ts +6 -6
  400. package/src/database/db.ts +29 -19
  401. package/src/database/knex.ts +30 -17
  402. package/src/database/puri-subset.test-d.ts +17 -0
  403. package/src/database/puri.ts +181 -143
  404. package/src/database/puri.types.test-d.ts +31 -1
  405. package/src/database/puri.types.ts +23 -21
  406. package/src/database/upsert-builder.ts +75 -53
  407. package/src/dict/sd.ts +15 -5
  408. package/src/dict/sonamu-dictionary.ts +6 -6
  409. package/src/dict/utils.ts +9 -1
  410. package/src/entity/entity-manager.ts +26 -9
  411. package/src/entity/entity-template-cone.ts +5 -10
  412. package/src/entity/entity.ts +50 -34
  413. package/src/env.ts +7 -3
  414. package/src/exceptions/error-handler.ts +17 -2
  415. package/src/exceptions/so-exceptions.ts +26 -20
  416. package/src/filter/utils.ts +21 -11
  417. package/src/index.ts +3 -0
  418. package/src/logger/configure.ts +26 -18
  419. package/src/migration/__tests__/migrator.test.ts +50 -57
  420. package/src/migration/code-generation.ts +132 -104
  421. package/src/migration/index-where-predicate.ts +51 -24
  422. package/src/migration/migration-set.ts +11 -5
  423. package/src/migration/migrator.ts +392 -244
  424. package/src/migration/postgresql-schema-reader.ts +52 -47
  425. package/src/migration/slack-confirm.ts +17 -15
  426. package/src/migration/types.ts +57 -0
  427. package/src/naite/naite-reporter.ts +1 -1
  428. package/src/naite/naite.ts +97 -24
  429. package/src/runtime-type.ts +34 -0
  430. package/src/ssr/hmr-opt-out.ts +78 -0
  431. package/src/ssr/renderer.ts +6 -0
  432. package/src/storage/base-file.ts +13 -13
  433. package/src/storage/buffered-file.ts +11 -11
  434. package/src/storage/s3-driver.ts +4 -1
  435. package/src/storage/uploaded-file.ts +8 -8
  436. package/src/stream/__tests__/ws-contracts.test.ts +4 -2
  437. package/src/stream/__tests__/ws-telemetry.test.ts +1 -1
  438. package/src/stream/__tests__/ws.test.ts +30 -6
  439. package/src/stream/sse.ts +12 -11
  440. package/src/stream/ws-core.ts +1 -1
  441. package/src/stream/ws-delivery.ts +16 -7
  442. package/src/stream/ws-local-connection-store.ts +2 -3
  443. package/src/stream/ws-presence-store.ts +6 -1
  444. package/src/stream/ws-registry.ts +16 -7
  445. package/src/stream/ws-telemetry.ts +66 -43
  446. package/src/stream/ws.ts +73 -39
  447. package/src/syncer/__tests__/api-parser.websocket-type-ref.test.ts +3 -2
  448. package/src/syncer/__tests__/http-validator-registry.test.ts +137 -0
  449. package/src/syncer/api-parser.ts +67 -27
  450. package/src/syncer/checksum.ts +17 -11
  451. package/src/syncer/code-generator.ts +25 -17
  452. package/src/syncer/entity-operations.ts +3 -5
  453. package/src/syncer/file-copy.ts +46 -0
  454. package/src/syncer/file-patterns.ts +11 -8
  455. package/src/syncer/file-tracking.ts +3 -4
  456. package/src/syncer/filesystem-dependencies.ts +73 -0
  457. package/src/syncer/module-loader.ts +8 -4
  458. package/src/syncer/syncer-actions.ts +56 -18
  459. package/src/syncer/syncer.ts +116 -43
  460. package/src/syncer/watcher.ts +8 -4
  461. package/src/tasks/decorator.ts +20 -12
  462. package/src/tasks/step-wrapper.ts +19 -9
  463. package/src/tasks/workflow-manager.ts +15 -9
  464. package/src/template/__tests__/http_validators.template.test.ts +160 -0
  465. package/src/template/__tests__/services.template.mutation.test.ts +24 -23
  466. package/src/template/__tests__/services.template.websocket.test.ts +21 -25
  467. package/src/template/entity-converter.ts +5 -2
  468. package/src/template/helpers.ts +8 -9
  469. package/src/template/implementations/generated.template.ts +5 -4
  470. package/src/template/implementations/generated_http.template.ts +61 -15
  471. package/src/template/implementations/generated_sso.template.ts +15 -4
  472. package/src/template/implementations/http_validators.template.ts +175 -0
  473. package/src/template/implementations/model.template.ts +3 -1
  474. package/src/template/implementations/sd.template.ts +81 -41
  475. package/src/template/implementations/services.template.ts +2 -2
  476. package/src/template/implementations/view_form.template.ts +56 -39
  477. package/src/template/implementations/view_list.template.ts +31 -23
  478. package/src/template/template-manager.ts +15 -13
  479. package/src/template/template-types.ts +13 -5
  480. package/src/template/template.ts +4 -3
  481. package/src/template/zod-converter.ts +206 -91
  482. package/src/testing/__tests__/dev-test-routes.test.ts +85 -25
  483. package/src/testing/__tests__/dev-vitest-manager.test.ts +15 -15
  484. package/src/testing/_relation-graph.ts +4 -1
  485. package/src/testing/bootstrap.ts +12 -5
  486. package/src/testing/data-explorer.ts +102 -53
  487. package/src/testing/dev-test-routes.ts +87 -50
  488. package/src/testing/dev-vitest-manager.ts +88 -32
  489. package/src/testing/faker-mappings.ts +2 -2
  490. package/src/testing/filesystem.ts +1 -0
  491. package/src/testing/fixture-generator.ts +191 -131
  492. package/src/testing/fixture-loader.ts +5 -1
  493. package/src/testing/fixture-manager.ts +129 -90
  494. package/src/testing/global-setup.ts +7 -3
  495. package/src/testing/index.ts +5 -0
  496. package/src/types/__tests__/entity-json-schema-search-text.test.ts +9 -3
  497. package/src/types/types.ts +184 -158
  498. package/src/ui/ai-api.ts +9 -7
  499. package/src/ui/ai-client.ts +61 -56
  500. package/src/ui/api.ts +726 -770
  501. package/src/ui/async-route.ts +4 -0
  502. package/src/ui/cdd-service.ts +35 -12
  503. package/src/ui/migrations-api.ts +381 -0
  504. package/src/utils/formatter.ts +7 -3
  505. package/src/utils/fs-utils.ts +5 -5
  506. package/src/utils/model.ts +4 -5
  507. package/src/utils/object-utils.ts +32 -22
  508. package/src/utils/process-utils.ts +4 -1
  509. package/src/utils/runtime-value.ts +43 -0
  510. package/src/utils/sql-parser.ts +13 -6
  511. package/src/utils/type-utils.ts +9 -7
  512. package/src/utils/utils.ts +19 -21
  513. package/src/utils/zod-error.ts +4 -2
  514. package/src/vector/chunking.ts +6 -1
  515. package/src/vector/embedding.ts +2 -1
  516. package/src/vector/types.ts +4 -2
  517. package/tsdown.api.config.ts +5 -2
  518. package/dist/ui-web/assets/index-CJf8uJYf.js +0 -176
  519. package/dist/ui-web/assets/index-GMMIVGja.css +0 -1
package/src/api/sonamu.ts CHANGED
@@ -4,7 +4,7 @@ import { type IncomingMessage, type Server, type ServerResponse } from "http";
4
4
  import os from "os";
5
5
  import path from "path";
6
6
 
7
- import type {} from "@fastify/websocket";
7
+ import "@fastify/websocket";
8
8
  import { dispose as logtapeDispose } from "@logtape/logtape";
9
9
  import { type Auth, type BetterAuthOptions } from "better-auth";
10
10
  import chalk from "chalk";
@@ -27,16 +27,22 @@ import { SD, setSDConfig } from "../dict/sd";
27
27
  import { type LocalizedString } from "../dict/types";
28
28
  import { getSonamuEnvironment, readAllEnvironmentSnapshots } from "../env";
29
29
  import { NotFoundException } from "../exceptions/so-exceptions";
30
+ import { runtimeTypeOf } from "../runtime-type";
30
31
  import { BufferedFile } from "../storage/buffered-file";
31
32
  import { type StorageManager } from "../storage/storage-manager";
32
33
  import { type KeyGenerator } from "../storage/types";
33
34
  import { UploadedFile } from "../storage/uploaded-file";
34
35
  import { createMockSSEFactory } from "../stream/sse";
35
- import { WebSocketRuntime, type WebSocketConnection, type WebSocketEventMap } from "../stream/ws";
36
+ import {
37
+ type AnyWebSocketConnection,
38
+ WebSocketRuntime,
39
+ type WebSocketConnection,
40
+ } from "../stream/ws";
36
41
  import {
37
42
  type TelemetryContextProvider,
38
43
  type WebSocketTelemetryConnectionContext,
39
44
  } from "../stream/ws-telemetry";
45
+ import { type LoadedModels } from "../syncer/module-loader";
40
46
  import { type Syncer } from "../syncer/syncer";
41
47
  import { type WorkflowManager } from "../tasks/workflow-manager";
42
48
  import { type DevVitestManager } from "../testing/dev-vitest-manager";
@@ -45,10 +51,30 @@ import { centerText } from "../utils/console-util";
45
51
  import { isDaemonServer } from "../utils/controller";
46
52
  import { exists, fileExists } from "../utils/fs-utils";
47
53
  import { type AbsolutePath } from "../utils/path-utils";
54
+ import {
55
+ isBigIntValue,
56
+ isFunctionValue,
57
+ isNumberValue,
58
+ isObjectValue,
59
+ isStringValue,
60
+ isSymbolValue,
61
+ } from "../utils/runtime-value";
48
62
  import { convertFastifyHeadersToStandard, merge } from "../utils/utils";
49
- import { type SonamuConfig, type SonamuServerOptions, type SonamuTaskOptions } from "./config";
63
+ import {
64
+ normalizeZodCompilerPolicy,
65
+ type SonamuConfig,
66
+ type SonamuServerOptions,
67
+ type SonamuTaskOptions,
68
+ } from "./config";
50
69
  import { type Context, type RuntimeContext, type WebSocketContext } from "./context";
51
70
  import { type ExtendedApi } from "./decorators";
71
+ import {
72
+ assertHttpValidatorRegistry,
73
+ createHttpValidator,
74
+ getHttpValidatorRouteKey,
75
+ type HttpValidator,
76
+ type HttpValidatorRegistry,
77
+ } from "./http-validator";
52
78
  import { getSecrets } from "./secret";
53
79
  import { type SonamuSecrets } from "./secret";
54
80
  import {
@@ -64,22 +90,100 @@ export {
64
90
  resolveWebSocketPluginOptions,
65
91
  } from "./websocket-helpers";
66
92
 
93
+ interface UploadedRequestFiles {
94
+ bufferedFiles: BufferedFile[];
95
+ uploadedFiles: UploadedFile[];
96
+ }
97
+
98
+ type ApiArgument =
99
+ | string
100
+ | number
101
+ | boolean
102
+ | bigint
103
+ | symbol
104
+ | CallableFunction
105
+ | object
106
+ | null
107
+ | undefined;
108
+ type ApiMethodResult = ApiArgument | void;
109
+
110
+ function toApiArgument<Value>(value: Value): ApiArgument {
111
+ if (value === null) return null;
112
+ if (value === undefined) return undefined;
113
+ if (value === true) return true;
114
+ if (value === false) return false;
115
+ if (isStringValue(value)) return value;
116
+ if (isNumberValue(value)) return value;
117
+ if (isBigIntValue(value)) return value;
118
+ if (isSymbolValue(value)) return value;
119
+ if (isFunctionValue(value)) return value;
120
+ if (isObjectValue(value)) {
121
+ return value;
122
+ }
123
+ throw new Error("지원하지 않는 API 인수 타입입니다.");
124
+ }
125
+
126
+ function toApiArgumentMap<Value extends object>(value: Value): Map<string, ApiArgument> {
127
+ return new Map(Object.entries(value).map(([key, item]) => [key, toApiArgument(item)]));
128
+ }
129
+
130
+ function createUnavailableSSE<T extends ZodObject>(_events: T): never {
131
+ throw new Error(
132
+ "createSSE is not available in websocket context. Define websocketContextProvider if your context setup depends on SSE helpers.",
133
+ );
134
+ }
135
+
136
+ function printDimSummary(message: string): void {
137
+ console.log(chalk.dim(`✓ ${message}`));
138
+ }
139
+
140
+ function printGreenSummary(message: string): void {
141
+ console.log(chalk.green(`✓ ${message}`));
142
+ }
143
+
144
+ export interface SonamuTestingSnapshot {
145
+ apiRootPath: AbsolutePath | null;
146
+ config: SonamuConfig | null;
147
+ syncer: Syncer | null;
148
+ httpValidators: WeakMap<ExtendedApi, HttpValidator>;
149
+ pendingHttpValidators: WeakMap<ExtendedApi, Promise<HttpValidator>>;
150
+ aotHttpValidatorRegistry: HttpValidatorRegistry | undefined;
151
+ preparedApis: readonly ExtendedApi[];
152
+ preparedModels: LoadedModels;
153
+ websocketRuntime: WebSocketRuntime | null;
154
+ }
155
+
156
+ export interface SonamuHttpValidatorDependencies {
157
+ createValidator: typeof createHttpValidator;
158
+ assertRegistry: typeof assertHttpValidatorRegistry;
159
+ }
160
+
161
+ export const defaultSonamuHttpValidatorDependencies: SonamuHttpValidatorDependencies = {
162
+ createValidator: createHttpValidator,
163
+ assertRegistry: assertHttpValidatorRegistry,
164
+ };
165
+
67
166
  class SonamuClass {
68
167
  public isInitialized: boolean = false;
69
168
  public forTesting: boolean = false;
70
169
  public asyncLocalStorage: AsyncLocalStorage<{
71
170
  context: RuntimeContext;
72
171
  }> = new AsyncLocalStorage();
172
+ private httpValidators: WeakMap<ExtendedApi, HttpValidator> = new WeakMap();
173
+ private pendingHttpValidators: WeakMap<ExtendedApi, Promise<HttpValidator>> = new WeakMap();
174
+ private aotHttpValidatorRegistry: HttpValidatorRegistry | undefined;
175
+ private preparedApis: readonly ExtendedApi[] = [];
176
+ private preparedModels: LoadedModels = {};
73
177
 
74
178
  public getContext<T extends RuntimeContext = Context>(): T {
75
179
  const store = this.asyncLocalStorage.getStore();
76
180
  if (store?.context) {
77
- return store.context as T;
181
+ return /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ store.context as T;
78
182
  }
79
183
 
80
184
  if (process.env.NODE_ENV === "test") {
81
185
  // 테스팅 환경에서 컨텍스트가 주입되지 않은 경우 빈 컨텍스트 리턴
82
- return {
186
+ const mockContext = {
83
187
  transport: "http",
84
188
  request: null,
85
189
  reply: null,
@@ -89,122 +193,151 @@ class SonamuClass {
89
193
  user: null,
90
194
  session: null,
91
195
  naiteStore: new Map<string, any>(),
92
- } as unknown as T;
196
+ };
197
+ // SAFETY: 테스트 기본 컨텍스트는 HTTP Context이며 기본 제네릭과 일치한다.
198
+ return /* SAFETY: 테스트 기본 컨텍스트는 요청 범위가 없는 HTTP 컨텍스트 대체값입니다. */ mockContext as T &
199
+ typeof mockContext;
93
200
  } else {
94
201
  throw new Error("Sonamu cannot find context");
95
202
  }
96
203
  }
97
204
 
98
- private _apiRootPath: AbsolutePath | null = null;
205
+ private apiRootPathValue: AbsolutePath | null = null;
99
206
  set apiRootPath(apiRootPath: AbsolutePath) {
100
- this._apiRootPath = apiRootPath;
207
+ this.apiRootPathValue = apiRootPath;
101
208
  }
102
209
  get apiRootPath(): AbsolutePath {
103
- if (this._apiRootPath === null) {
210
+ if (this.apiRootPathValue === null) {
104
211
  throw new Error("Sonamu has not been initialized");
105
212
  }
106
- return this._apiRootPath;
213
+ return this.apiRootPathValue;
107
214
  }
108
215
  get appRootPath(): string {
109
216
  return this.apiRootPath.split(path.sep).slice(0, -1).join(path.sep);
110
217
  }
111
218
 
112
- private _dbConfig: SonamuDBConfig | null = null;
219
+ private dbConfigValue: SonamuDBConfig | null = null;
113
220
  set dbConfig(dbConfig: SonamuDBConfig) {
114
- this._dbConfig = dbConfig;
221
+ this.dbConfigValue = dbConfig;
115
222
  }
116
223
  get dbConfig(): SonamuDBConfig {
117
- if (this._dbConfig === null) {
224
+ if (this.dbConfigValue === null) {
118
225
  throw new Error("Sonamu has not been initialized");
119
226
  }
120
- return this._dbConfig;
227
+ return this.dbConfigValue;
121
228
  }
122
229
 
123
- private _syncer: Syncer | null = null;
230
+ private syncerValue: Syncer | null = null;
124
231
  set syncer(syncer: Syncer) {
125
- this._syncer = syncer;
232
+ this.syncerValue = syncer;
126
233
  }
127
234
  get syncer(): Syncer {
128
- if (this._syncer === null) {
235
+ if (this.syncerValue === null) {
129
236
  throw new Error("Sonamu has not been initialized");
130
237
  }
131
- return this._syncer;
238
+ return this.syncerValue;
132
239
  }
133
240
 
134
- private _config: SonamuConfig | null = null;
241
+ private configValue: SonamuConfig | null = null;
135
242
  set config(config: SonamuConfig) {
136
- this._config = config;
243
+ this.configValue = config;
137
244
  }
138
245
  get config(): SonamuConfig {
139
- if (this._config === null) {
246
+ if (this.configValue === null) {
140
247
  throw new Error("Sonamu has not been initialized");
141
248
  }
142
- return this._config;
249
+ return this.configValue;
143
250
  }
144
251
 
145
252
  public readonly secrets: SonamuSecrets = getSecrets();
146
253
 
147
- private _storage: StorageManager | null = null;
254
+ private storageValue: StorageManager | null = null;
148
255
  /**
149
256
  * StorageManager 인스턴스
150
257
  */
151
258
  get storage(): StorageManager {
152
- if (!this._storage) {
259
+ if (!this.storageValue) {
153
260
  throw new Error("Storage has not been initialized. Check storage config.");
154
261
  }
155
- return this._storage;
262
+ return this.storageValue;
156
263
  }
157
264
 
158
- private _cache: CacheManager | null = null;
265
+ private cacheValue: CacheManager | null = null;
159
266
  /**
160
267
  * CacheManager 인스턴스 (BentoCache)
161
268
  */
162
269
  get cache(): CacheManager {
163
- if (!this._cache) {
270
+ if (!this.cacheValue) {
164
271
  throw new Error("Cache has not been initialized. Check cache config in sonamu.config.ts.");
165
272
  }
166
- return this._cache;
273
+ return this.cacheValue;
167
274
  }
168
275
 
169
- private _workflows: WorkflowManager | null = null;
276
+ private workflowsValue: WorkflowManager | null = null;
170
277
  get workflows(): WorkflowManager {
171
- if (this._workflows === null) {
278
+ if (this.workflowsValue === null) {
172
279
  throw new Error("Sonamu has not been initialized");
173
280
  }
174
281
 
175
- return this._workflows;
282
+ return this.workflowsValue;
176
283
  }
177
284
 
178
- private _auth: Auth<BetterAuthOptions> | null = null;
285
+ private authValue: Auth<BetterAuthOptions> | null = null;
179
286
  get auth(): Auth<BetterAuthOptions> {
180
- if (!this._auth) {
287
+ if (!this.authValue) {
181
288
  throw new Error("Auth has not been initialized. Check auth config in sonamu.config.ts.");
182
289
  }
183
- return this._auth;
290
+ return this.authValue;
184
291
  }
185
292
 
186
- private _devVitestManager: DevVitestManager | null = null;
293
+ private devVitestManagerValue: DevVitestManager | null = null;
187
294
  get devVitestManager(): DevVitestManager | null {
188
- return this._devVitestManager;
295
+ return this.devVitestManagerValue;
189
296
  }
190
297
  set devVitestManager(manager: DevVitestManager | null) {
191
- this._devVitestManager = manager;
298
+ this.devVitestManagerValue = manager;
192
299
  }
193
300
 
194
301
  // Sonamu가 runtime을 직접 소유해 registry/connection lifecycle을 애플리케이션 수명주기와 동기화함
195
- private _websocketRuntime: WebSocketRuntime | null = null;
302
+ private websocketRuntimeValue: WebSocketRuntime | null = null;
196
303
  // 같은 Fastify 인스턴스에 @fastify/websocket을 중복 등록하는 것을 WeakSet으로 차단함
197
304
  private readonly websocketPluginServers = new WeakSet<
198
305
  FastifyInstance<Server, IncomingMessage, ServerResponse>
199
306
  >();
200
307
  get websocketRuntime(): WebSocketRuntime {
201
- if (!this._websocketRuntime) {
308
+ if (!this.websocketRuntimeValue) {
202
309
  throw new Error("WebSocket runtime has not been initialized.");
203
310
  }
204
- return this._websocketRuntime;
311
+ return this.websocketRuntimeValue;
205
312
  }
206
313
  set websocketRuntime(runtime: WebSocketRuntime | null) {
207
- this._websocketRuntime = runtime;
314
+ this.websocketRuntimeValue = runtime;
315
+ }
316
+
317
+ captureTestingSnapshot(): SonamuTestingSnapshot {
318
+ return {
319
+ apiRootPath: this.apiRootPathValue,
320
+ config: this.configValue,
321
+ syncer: this.syncerValue,
322
+ httpValidators: this.httpValidators,
323
+ pendingHttpValidators: this.pendingHttpValidators,
324
+ aotHttpValidatorRegistry: this.aotHttpValidatorRegistry,
325
+ preparedApis: this.preparedApis,
326
+ preparedModels: this.preparedModels,
327
+ websocketRuntime: this.websocketRuntimeValue,
328
+ };
329
+ }
330
+
331
+ restoreTestingSnapshot(snapshot: SonamuTestingSnapshot): void {
332
+ this.apiRootPathValue = snapshot.apiRootPath;
333
+ this.configValue = snapshot.config;
334
+ this.syncerValue = snapshot.syncer;
335
+ this.httpValidators = snapshot.httpValidators;
336
+ this.pendingHttpValidators = snapshot.pendingHttpValidators;
337
+ this.aotHttpValidatorRegistry = snapshot.aotHttpValidatorRegistry;
338
+ this.preparedApis = snapshot.preparedApis;
339
+ this.preparedModels = snapshot.preparedModels;
340
+ this.websocketRuntimeValue = snapshot.websocketRuntime;
208
341
  }
209
342
 
210
343
  // HMR 처리: 파일 시스템 감시 + HMR/sync 사이클 실행은 watcher 모듈로 위임합니다.
@@ -255,17 +388,17 @@ class SonamuClass {
255
388
  }
256
389
 
257
390
  // DB 로드
258
- const { DB } = await import("../database/db");
391
+ const { DB: RuntimeDB } = await import("../database/db");
259
392
  const { isLocal: isLocalEnvironment } = await import("../utils/controller");
260
393
  const environmentSnapshots = isLocalEnvironment()
261
394
  ? readAllEnvironmentSnapshots(this.apiRootPath, baseEnvBeforeConfigLoad)
262
395
  : undefined;
263
- this.dbConfig = DB.generateDBConfig(
396
+ this.dbConfig = RuntimeDB.generateDBConfig(
264
397
  this.config.database,
265
398
  this.config.projectName,
266
399
  environmentSnapshots,
267
400
  );
268
- DB.setConfig(this.dbConfig);
401
+ RuntimeDB.setConfig(this.dbConfig);
269
402
 
270
403
  // Entity 로드
271
404
  // 테스트에서도 Entity 정보는 필요합니다.
@@ -290,7 +423,7 @@ class SonamuClass {
290
423
  database: sonamuKnexAdapter(),
291
424
  ...mergedFieldMappings,
292
425
  };
293
- this._auth = betterAuth(authOptions);
426
+ this.authValue = betterAuth(authOptions);
294
427
  }
295
428
 
296
429
  // 테스팅인 경우 싱크 없이 중단
@@ -322,12 +455,12 @@ class SonamuClass {
322
455
  }
323
456
 
324
457
  this.isInitialized = true;
325
- this._initElapsed = performance.now() - initStart;
326
- this._configElapsed = configTime;
458
+ this.initElapsed = performance.now() - initStart;
459
+ this.configElapsed = configTime;
327
460
  }
328
461
 
329
- private _initElapsed = 0;
330
- private _configElapsed = 0;
462
+ private initElapsed = 0;
463
+ private configElapsed = 0;
331
464
 
332
465
  async createServer(initOptions?: { enableSync?: boolean; doSilent?: boolean }) {
333
466
  if (!this.isInitialized) {
@@ -352,7 +485,7 @@ class SonamuClass {
352
485
  // Storage 설정 → StorageManager 생성
353
486
  if (options.storage) {
354
487
  const { StorageManager } = await import("../storage/storage-manager");
355
- this._storage = new StorageManager(options.storage);
488
+ this.storageValue = new StorageManager(options.storage);
356
489
  }
357
490
 
358
491
  // 플러그인 등록
@@ -394,6 +527,7 @@ class SonamuClass {
394
527
 
395
528
  this.server = server;
396
529
  this.websocketRuntime ??= new WebSocketRuntime(this.config.server.websocket);
530
+ await this.refreshHttpValidators();
397
531
 
398
532
  // timezone 설정
399
533
  const timezone = this.config.api.timezone;
@@ -413,10 +547,10 @@ class SonamuClass {
413
547
 
414
548
  server.setReplySerializer((payload) => {
415
549
  return JSON.stringify(payload, (_key, value) => {
416
- if (typeof value === "string" && ISO_DATE_REGEX.test(value)) {
550
+ if (isStringValue(value) && ISO_DATE_REGEX.test(value)) {
417
551
  return formatInTimeZone(
418
552
  new Date(value),
419
- timezone as `${string}/${string}`,
553
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ timezone as `${string}/${string}`,
420
554
  DATE_FORMAT,
421
555
  );
422
556
  }
@@ -500,7 +634,7 @@ class SonamuClass {
500
634
  server.route({
501
635
  method: api.options.httpMethod ?? "GET",
502
636
  url: this.config.api.route.prefix + api.path,
503
- handler: this.createApiHandler(api, config),
637
+ handler: await this.createApiHandler(api, config),
504
638
  compress: toFastifyCompressOption(api.options.compress, globalCompressOptions),
505
639
  });
506
640
  }
@@ -512,17 +646,17 @@ class SonamuClass {
512
646
  }
513
647
 
514
648
  /**
515
- * dev 모드 공통: catch-all에서 syncer.apis를 동적으로 탐색하여 API 요청을 처리합니다.
649
+ * dev 모드 공통: catch-all에서 마지막 준비 완료 API snapshot을 탐색하여 요청을 처리합니다.
516
650
  * server.route()로 개별 등록하면 handler가 고정되어 HMR이 동작하지 않으므로,
517
- * 매 요청마다 syncer.apis를 조회하는 이 방식을 사용합니다.
651
+ * refresh 성공 시 교체되는 snapshot을 매 요청 조회합니다.
518
652
  *
519
653
  * 요청이 /api(정확히는 this.config.api.route.prefix)로 시작하지 않는 경우라면 null을 반환하며 끝냅니다.
520
654
  */
521
- private handleDevApiRequest(
655
+ private async handleDevApiRequest(
522
656
  request: FastifyRequest,
523
657
  config: SonamuFastifyConfig,
524
- ): ((request: FastifyRequest, reply: FastifyReply) => Promise<unknown>) | null {
525
- const matchedApi = this.findMatchedApi(request);
658
+ ): Promise<((request: FastifyRequest, reply: FastifyReply) => Promise<ApiMethodResult>) | null> {
659
+ const matchedApi = this.findMatchedApi(request, this.preparedApis, this.preparedModels);
526
660
 
527
661
  if (!matchedApi) {
528
662
  throw new NotFoundException(SD("error.api.notFound"));
@@ -533,10 +667,14 @@ class SonamuClass {
533
667
  return this.createWebSocketUpgradeRequiredHandler();
534
668
  }
535
669
 
536
- return this.createApiHandler(matchedApi, config);
670
+ return await this.createApiHandler(matchedApi, config, this.preparedModels);
537
671
  }
538
672
 
539
- private findMatchedApi(request: FastifyRequest): ExtendedApi | undefined {
673
+ private findMatchedApi(
674
+ request: FastifyRequest,
675
+ apis: readonly ExtendedApi[],
676
+ models: LoadedModels,
677
+ ): ExtendedApi | undefined {
540
678
  const url = this.getPathnameFromUrl(request.url);
541
679
  const method = request.method;
542
680
 
@@ -544,8 +682,8 @@ class SonamuClass {
544
682
  return undefined;
545
683
  }
546
684
 
547
- return this.syncer.apis.find((api) => {
548
- if (this.syncer.models[api.modelName] === undefined) {
685
+ return apis.find((api) => {
686
+ if (models[api.modelName] === undefined) {
549
687
  return false;
550
688
  }
551
689
  const apiMethod = api.options.httpMethod ?? "GET";
@@ -558,7 +696,7 @@ class SonamuClass {
558
696
 
559
697
  /**
560
698
  * dev api 모드: Vite 없이 API 동적 라우팅만 제공합니다.
561
- * HMR을 위해 catch-all에서 매 요청마다 syncer.apis를 조회합니다.
699
+ * HMR을 위해 catch-all에서 매 요청마다 준비 완료 API snapshot을 조회합니다.
562
700
  */
563
701
  private setupDevServer(
564
702
  server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
@@ -569,7 +707,7 @@ class SonamuClass {
569
707
  method: "GET",
570
708
  url: `${this.config.api.route.prefix}/*`,
571
709
  handler: async (request, reply) => {
572
- const handler = this.handleDevApiRequest(request, config);
710
+ const handler = await this.handleDevApiRequest(request, config);
573
711
  if (handler) {
574
712
  return handler(request, reply);
575
713
  }
@@ -585,7 +723,7 @@ class SonamuClass {
585
723
  method: ["HEAD", "POST", "PUT", "DELETE", "PATCH"],
586
724
  url: `${this.config.api.route.prefix}/*`,
587
725
  handler: async (request, reply) => {
588
- const handler = this.handleDevApiRequest(request, config);
726
+ const handler = await this.handleDevApiRequest(request, config);
589
727
  if (handler) {
590
728
  return handler(request, reply);
591
729
  }
@@ -640,7 +778,7 @@ class SonamuClass {
640
778
  method: "GET",
641
779
  url: `${this.config.api.route.prefix}/*`,
642
780
  handler: async (request, reply) => {
643
- const result = this.handleDevApiRequest(request, config);
781
+ const result = await this.handleDevApiRequest(request, config);
644
782
  if (result) {
645
783
  return result(request, reply);
646
784
  }
@@ -655,7 +793,7 @@ class SonamuClass {
655
793
  method: ["HEAD", "POST", "PUT", "DELETE", "PATCH"],
656
794
  url: `${this.config.api.route.prefix}/*`,
657
795
  handler: async (request, reply) => {
658
- const result = this.handleDevApiRequest(request, config);
796
+ const result = await this.handleDevApiRequest(request, config);
659
797
  if (result) {
660
798
  return result(request, reply);
661
799
  }
@@ -691,20 +829,29 @@ class SonamuClass {
691
829
 
692
830
  // 2. CSR fallback
693
831
  try {
694
- const fs = await import("node:fs/promises");
695
832
  let template = await fs.readFile(
696
833
  path.join(this.viteServer.config.root, "index.html"),
697
834
  "utf-8",
698
835
  );
699
836
  template = await this.viteServer.transformIndexHtml(url, template);
700
837
 
838
+ // ?hmr=false로 연 페이지는 HMR 웹소켓만 끊어 새로고침되지 않게 한다.
839
+ const { HMR_OPT_OUT_SCRIPT, isHmrDisabledByQuery } = await import("../ssr/hmr-opt-out");
840
+ if (isHmrDisabledByQuery(url)) {
841
+ template = template.replace("<head>", `<head>\n${HMR_OPT_OUT_SCRIPT}`);
842
+ }
843
+
701
844
  reply.type("text/html");
702
845
  return template;
703
846
  } catch (e) {
704
- this.viteServer.ssrFixStacktrace(e as Error);
847
+ this.viteServer.ssrFixStacktrace(
848
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ e as Error,
849
+ );
705
850
  console.error(e);
706
851
  reply.status(500);
707
- return (e as Error).message;
852
+ return /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ (
853
+ e as Error
854
+ ).message;
708
855
  }
709
856
  },
710
857
  });
@@ -714,7 +861,6 @@ class SonamuClass {
714
861
  await this.viteServer.close();
715
862
  });
716
863
 
717
- const chalk = (await import("chalk")).default;
718
864
  if ("port" in hmr) {
719
865
  console.log(
720
866
  chalk.dim(
@@ -764,7 +910,10 @@ class SonamuClass {
764
910
 
765
911
  // 롤링 업데이트 대응: asset hash 불일치 시 현재 버전 직접 서빙
766
912
  server.get("/assets/:filename", async (request, reply) => {
767
- const requestedFile = (request.params as { filename: string }).filename;
913
+ const requestedFile =
914
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ (
915
+ request.params as { filename: string }
916
+ ).filename;
768
917
  const assetsDir = path.join(webDistPath, "assets");
769
918
  const safeFilePath = this.resolvePathWithinBaseDir(assetsDir, requestedFile);
770
919
  if (safeFilePath === null) {
@@ -899,8 +1048,14 @@ class SonamuClass {
899
1048
  createApiHandler(
900
1049
  api: ExtendedApi,
901
1050
  config: SonamuFastifyConfig,
902
- ): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown> {
903
- return async (request: FastifyRequest, reply: FastifyReply): Promise<unknown> => {
1051
+ models: LoadedModels = this.syncer.models,
1052
+ dependencies: SonamuHttpValidatorDependencies = defaultSonamuHttpValidatorDependencies,
1053
+ ): (request: FastifyRequest, reply: FastifyReply) => Promise<ApiMethodResult> {
1054
+ const preparedValidator = this.getOrCreateHttpValidator(api, dependencies);
1055
+
1056
+ return async (request: FastifyRequest, reply: FastifyReply): Promise<ApiMethodResult> => {
1057
+ const validator =
1058
+ preparedValidator instanceof Promise ? await preparedValidator : preparedValidator;
904
1059
  // Context 생성
905
1060
  const context: Context = await this.createContext(config, request, reply);
906
1061
 
@@ -913,26 +1068,17 @@ class SonamuClass {
913
1068
  api,
914
1069
  });
915
1070
 
916
- // 파라미터 정보로 zod 스키마 빌드
917
- const { getZodObjectFromApi } = await import("./code-converters");
918
- const ReqType = getZodObjectFromApi(api, this.syncer.types);
919
-
920
1071
  // request 파싱
921
1072
  const which = api.options.httpMethod === "GET" ? "query" : "body";
922
- let reqBody: {
923
- [key: string]: unknown;
924
- };
1073
+ let reqBody = new Map<string, ApiArgument>();
925
1074
  // 파일 업로드 있는 경우 임시 데이터
926
- const files: {
927
- bufferedFiles: BufferedFile[];
928
- uploadedFiles: UploadedFile[];
929
- } = {
1075
+ const files: UploadedRequestFiles = {
930
1076
  bufferedFiles: [],
931
1077
  uploadedFiles: [],
932
1078
  };
933
1079
 
934
1080
  try {
935
- const body = (request[which] ?? {}) as Record<string, unknown>;
1081
+ const body = request[which] ?? {};
936
1082
  if (api.uploadOptions) {
937
1083
  const parts = request.parts({
938
1084
  limits: api.uploadOptions.limits,
@@ -1001,8 +1147,7 @@ class SonamuClass {
1001
1147
  Object.assign(body, parsed);
1002
1148
  }
1003
1149
 
1004
- const { fastifyCaster } = await import("./caster");
1005
- reqBody = fastifyCaster(ReqType).parse(body);
1150
+ reqBody = toApiArgumentMap(validator.parse(body));
1006
1151
  } catch (e) {
1007
1152
  const { ZodError } = await import("zod");
1008
1153
  if (e instanceof ZodError) {
@@ -1011,9 +1156,12 @@ class SonamuClass {
1011
1156
  .map((issue) => issue.message)
1012
1157
  .join(" ");
1013
1158
  const { BadRequestException } = await import("../exceptions/so-exceptions");
1014
- throw new BadRequestException(messages as LocalizedString, {
1015
- zodError: e,
1016
- });
1159
+ throw new BadRequestException(
1160
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ messages as LocalizedString,
1161
+ {
1162
+ zodError: e,
1163
+ },
1164
+ );
1017
1165
  } else {
1018
1166
  throw e;
1019
1167
  }
@@ -1045,15 +1193,146 @@ class SonamuClass {
1045
1193
  if (ApiParamType.isContext(param.type)) {
1046
1194
  return context;
1047
1195
  } else {
1048
- return reqBody[param.name];
1196
+ return reqBody.get(param.name);
1049
1197
  }
1050
1198
  });
1051
1199
 
1052
- return this.invokeModelMethod(api, args, reply);
1200
+ return this.invokeModelMethod(api, args, reply, models);
1053
1201
  });
1054
1202
  };
1055
1203
  }
1056
1204
 
1205
+ private getOrCreateHttpValidator(
1206
+ api: ExtendedApi,
1207
+ dependencies: SonamuHttpValidatorDependencies,
1208
+ ): HttpValidator | Promise<HttpValidator> {
1209
+ const cached = this.httpValidators.get(api);
1210
+ if (cached !== undefined) {
1211
+ return cached;
1212
+ }
1213
+ const existing = this.pendingHttpValidators.get(api);
1214
+ if (existing !== undefined) {
1215
+ return existing;
1216
+ }
1217
+ const pending = this.buildHttpValidator(api, dependencies);
1218
+ this.pendingHttpValidators.set(api, pending);
1219
+ void pending.then(
1220
+ (validator) => {
1221
+ this.httpValidators.set(api, validator);
1222
+ this.pendingHttpValidators.delete(api);
1223
+ },
1224
+ () => {
1225
+ this.pendingHttpValidators.delete(api);
1226
+ },
1227
+ );
1228
+ return pending;
1229
+ }
1230
+
1231
+ private async buildHttpValidator(
1232
+ api: ExtendedApi,
1233
+ dependencies: SonamuHttpValidatorDependencies,
1234
+ ): Promise<HttpValidator> {
1235
+ const policy = normalizeZodCompilerPolicy(this.config.validation?.zodCompiler);
1236
+ const sourceAot = process.env.HOT === "yes" || process.env.VITEST === "true";
1237
+ if (policy.api === "aot" && !sourceAot) {
1238
+ const registry = this.aotHttpValidatorRegistry ?? (await this.loadAotHttpValidatorRegistry());
1239
+ return this.getAotHttpValidator(api, registry);
1240
+ }
1241
+
1242
+ return await dependencies.createValidator({
1243
+ api,
1244
+ policy,
1245
+ types: this.syncer.types,
1246
+ allowAotSourceFallback: sourceAot,
1247
+ });
1248
+ }
1249
+
1250
+ private getAotHttpValidator(api: ExtendedApi, registry: HttpValidatorRegistry): HttpValidator {
1251
+ const routeKey = getHttpValidatorRouteKey(api);
1252
+ const validator = registry.validators.get(routeKey);
1253
+ if (validator === undefined) {
1254
+ throw new Error(`HTTP validator registry is missing ${routeKey}`);
1255
+ }
1256
+ return validator;
1257
+ }
1258
+
1259
+ private async loadAotHttpValidatorRegistry(
1260
+ apis: readonly ExtendedApi[] = this.syncer.apis,
1261
+ dependencies: SonamuHttpValidatorDependencies = defaultSonamuHttpValidatorDependencies,
1262
+ ): Promise<HttpValidatorRegistry> {
1263
+ const { pathToFileURL } = await import("node:url");
1264
+ const registryPath = path.join(
1265
+ this.apiRootPath,
1266
+ "dist/application/sonamu.validators.generated.js",
1267
+ );
1268
+ if (!(await exists(registryPath))) {
1269
+ throw new Error(`Generated HTTP validator registry not found: ${registryPath}`);
1270
+ }
1271
+
1272
+ const imported =
1273
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ (await import(
1274
+ pathToFileURL(registryPath).href
1275
+ )) as {
1276
+ fingerprint?: unknown;
1277
+ validators?: unknown;
1278
+ };
1279
+ if (!isStringValue(imported.fingerprint) || !(imported.validators instanceof Map)) {
1280
+ throw new Error(`Invalid generated HTTP validator registry: ${registryPath}`);
1281
+ }
1282
+ for (const [key, validator] of imported.validators) {
1283
+ if (
1284
+ !isStringValue(key) ||
1285
+ !isObjectValue(validator) ||
1286
+ validator === null ||
1287
+ !("parse" in validator) ||
1288
+ !isFunctionValue(validator.parse)
1289
+ ) {
1290
+ throw new Error(`Invalid HTTP validator registry entry: ${String(key)}`);
1291
+ }
1292
+ }
1293
+
1294
+ const registry: HttpValidatorRegistry = {
1295
+ fingerprint: imported.fingerprint,
1296
+ validators: imported.validators,
1297
+ };
1298
+ dependencies.assertRegistry(apis, registry);
1299
+ return registry;
1300
+ }
1301
+
1302
+ async refreshHttpValidators(
1303
+ dependencies: SonamuHttpValidatorDependencies = defaultSonamuHttpValidatorDependencies,
1304
+ ): Promise<void> {
1305
+ const policy = normalizeZodCompilerPolicy(this.config.validation?.zodCompiler);
1306
+ const sourceAot = process.env.HOT === "yes" || process.env.VITEST === "true";
1307
+ const nextApis = [...this.syncer.apis];
1308
+ const nextModels = { ...this.syncer.models };
1309
+ const restApis = nextApis.filter((api) => api.websocketOptions === undefined);
1310
+ const next = new WeakMap<ExtendedApi, HttpValidator>();
1311
+ let nextAotRegistry: HttpValidatorRegistry | undefined;
1312
+
1313
+ if (policy.api === "aot" && !sourceAot) {
1314
+ const registry = await this.loadAotHttpValidatorRegistry(nextApis, dependencies);
1315
+ for (const api of restApis) {
1316
+ next.set(api, this.getAotHttpValidator(api, registry));
1317
+ }
1318
+ nextAotRegistry = registry;
1319
+ } else {
1320
+ await Promise.all(
1321
+ restApis.map(async (api) => {
1322
+ const validator = await this.buildHttpValidator(api, dependencies);
1323
+ next.set(api, validator);
1324
+ }),
1325
+ );
1326
+ }
1327
+
1328
+ // validator와 같은 revision의 API/model snapshot을 함께 공개해 HMR 중간 상태를 숨깁니다.
1329
+ this.httpValidators = next;
1330
+ this.pendingHttpValidators = new WeakMap();
1331
+ this.aotHttpValidatorRegistry = nextAotRegistry;
1332
+ this.preparedApis = nextApis;
1333
+ this.preparedModels = nextModels;
1334
+ }
1335
+
1057
1336
  // WS path를 일반 HTTP GET으로 호출한 경우 426 + Upgrade 헤더로 명시적으로 websocket 접속을 유도함
1058
1337
  private createWebSocketUpgradeRequiredHandler() {
1059
1338
  return async (_request: FastifyRequest, reply: FastifyReply): Promise<void> => {
@@ -1069,7 +1348,7 @@ class SonamuClass {
1069
1348
  request: FastifyRequest,
1070
1349
  config: SonamuFastifyConfig,
1071
1350
  ): Promise<void> {
1072
- const matchedApi = this.findMatchedApi(request);
1351
+ const matchedApi = this.findMatchedApi(request, this.syncer.apis, this.syncer.models);
1073
1352
  if (!matchedApi?.websocketOptions) {
1074
1353
  const traceContext = this.websocketRuntime.telemetryController.createConnectionContext({
1075
1354
  headers: request.headers,
@@ -1099,6 +1378,10 @@ class SonamuClass {
1099
1378
  // 2) `active: false`로 먼저 등록한 뒤 context 안에서 guard를 실행해 인증 정보를 참조할 수 있게 함
1100
1379
  // 3) context/guard 준비가 끝난 뒤 `activate()`해 브로드캐스트가 초기화 중간 상태를 보지 못하게 함
1101
1380
  // 에러 발생 시에는 resolveWebSocketCloseDescriptor 정책에 따라 close code를 매핑함
1381
+ createWebSocketHandlerForTesting(api: ExtendedApi, config: SonamuFastifyConfig) {
1382
+ return this.createWebSocketHandler(api, config);
1383
+ }
1384
+
1102
1385
  private createWebSocketHandler(api: ExtendedApi, config: SonamuFastifyConfig) {
1103
1386
  return async (
1104
1387
  connection: {
@@ -1107,7 +1390,7 @@ class SonamuClass {
1107
1390
  request: FastifyRequest,
1108
1391
  ): Promise<void> => {
1109
1392
  const socket = connection.socket;
1110
- let wsContext: WebSocketContext | null = null;
1393
+ let wsContext: WebSocketContext<object, object> | null = null;
1111
1394
  let rawWs: ReturnType<WebSocketRuntime["registerConnection"]> | null = null;
1112
1395
  let preRegisterPhase: "guard" | "query" | "register" | "handler" = "guard";
1113
1396
  let traceContext: WebSocketTelemetryConnectionContext = {};
@@ -1153,7 +1436,7 @@ class SonamuClass {
1153
1436
  return wsContext;
1154
1437
  }
1155
1438
 
1156
- return reqBody[param.name];
1439
+ return reqBody.get(param.name);
1157
1440
  });
1158
1441
 
1159
1442
  await this.asyncLocalStorage.run({ context: wsContext }, async () => {
@@ -1208,14 +1491,11 @@ class SonamuClass {
1208
1491
 
1209
1492
  // onMessage/onClose처럼 다른 tick에서 실행되는 callback은 ALS context가 끊기므로 wrapper에서 `asyncLocalStorage.run`으로 다시 감싸 복원함
1210
1493
  // publish/join/leave/setUserId 같은 즉시 실행 API는 단순 위임만 하고, deferred callback에만 context 복원을 적용함
1211
- private createScopedWebSocketConnection<
1212
- TOut extends WebSocketEventMap,
1213
- TIn extends WebSocketEventMap,
1214
- >(
1494
+ private createScopedWebSocketConnection<TOut extends object, TIn extends object>(
1215
1495
  ws: WebSocketConnection<TOut, TIn>,
1216
1496
  getContext: () => WebSocketContext | null,
1217
1497
  ): WebSocketConnection<TOut, TIn> {
1218
- const telemetryController = this._websocketRuntime?.telemetryController;
1498
+ const telemetryController = this.websocketRuntimeValue?.telemetryController;
1219
1499
  const runInContext = <T>(callback: () => T): T => {
1220
1500
  const context = getContext();
1221
1501
  if (!context) {
@@ -1309,7 +1589,7 @@ class SonamuClass {
1309
1589
  namespace: ws.namespace,
1310
1590
  userId: ws.userId,
1311
1591
  attributes: { event: eventName },
1312
- errorType: error instanceof Error ? error.name : typeof error,
1592
+ errorType: error instanceof Error ? error.name : runtimeTypeOf(error),
1313
1593
  ...traceContext,
1314
1594
  });
1315
1595
  throw error;
@@ -1338,16 +1618,23 @@ class SonamuClass {
1338
1618
  };
1339
1619
  }
1340
1620
 
1621
+ createScopedWebSocketConnectionForTesting<TOut extends object, TIn extends object>(
1622
+ ws: WebSocketConnection<TOut, TIn>,
1623
+ getContext: () => WebSocketContext | null,
1624
+ ): WebSocketConnection<TOut, TIn> {
1625
+ return this.createScopedWebSocketConnection(ws, getContext);
1626
+ }
1627
+
1341
1628
  private async parseWebSocketRequestParams(
1342
1629
  api: ExtendedApi,
1343
1630
  request: FastifyRequest,
1344
- ): Promise<Record<string, unknown>> {
1631
+ ): Promise<Map<string, ApiArgument>> {
1345
1632
  const { getZodObjectFromApi } = await import("./code-converters");
1346
1633
  const ReqType = getZodObjectFromApi(api, this.syncer.types);
1347
1634
 
1348
1635
  try {
1349
1636
  const { fastifyCaster } = await import("./caster");
1350
- return fastifyCaster(ReqType).parse((request.query ?? {}) as Record<string, unknown>);
1637
+ return toApiArgumentMap(fastifyCaster(ReqType).parse(request.query ?? {}));
1351
1638
  } catch (e) {
1352
1639
  const { ZodError } = await import("zod");
1353
1640
  if (e instanceof ZodError) {
@@ -1356,9 +1643,12 @@ class SonamuClass {
1356
1643
  .map((issue) => issue.message)
1357
1644
  .join(" ");
1358
1645
  const { BadRequestException } = await import("../exceptions/so-exceptions");
1359
- throw new BadRequestException(messages as LocalizedString, {
1360
- zodError: e,
1361
- });
1646
+ throw new BadRequestException(
1647
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ messages as LocalizedString,
1648
+ {
1649
+ zodError: e,
1650
+ },
1651
+ );
1362
1652
  }
1363
1653
 
1364
1654
  throw e;
@@ -1369,7 +1659,7 @@ class SonamuClass {
1369
1659
  * URL에서 path params를 추출합니다.
1370
1660
  * 예: pattern="/admin/companies/:companyId", url="/admin/companies/123" → { companyId: "123" }
1371
1661
  */
1372
- private extractPathParams(pattern: string, url: string): Record<string, string> {
1662
+ private extractPathParams(pattern: string, url: string) {
1373
1663
  const patternParts = pattern.split("/").filter(Boolean);
1374
1664
  const urlParts = this.getPathnameFromUrl(url).split("/").filter(Boolean);
1375
1665
  const params: Record<string, string> = {};
@@ -1466,7 +1756,7 @@ class SonamuClass {
1466
1756
  config: SonamuFastifyConfig,
1467
1757
  request: FastifyRequest,
1468
1758
  reply: FastifyReply,
1469
- ): Promise<unknown> {
1759
+ ): Promise<ApiMethodResult> {
1470
1760
  // Context 생성 (기존 메소드 재사용)
1471
1761
  const context = await this.createContext(config, request, reply);
1472
1762
 
@@ -1486,14 +1776,29 @@ class SonamuClass {
1486
1776
  });
1487
1777
  }
1488
1778
 
1779
+ async handleDevApiRequestForTesting(request: FastifyRequest, config: SonamuFastifyConfig) {
1780
+ return await this.handleDevApiRequest(request, config);
1781
+ }
1782
+
1489
1783
  // WS 경로에서는 HTTP reply가 없으므로 reply를 optional로 받아 공통 호출 경로를 유지함
1490
1784
  async invokeModelMethod(
1491
1785
  api: ExtendedApi,
1492
1786
  args: unknown[],
1493
1787
  reply?: FastifyReply,
1494
- ): Promise<unknown> {
1495
- const model = this.syncer.models[api.modelName];
1496
- const result = await (model as any)[api.methodName].apply(model, args);
1788
+ models: LoadedModels = this.syncer.models,
1789
+ ): Promise<ApiMethodResult> {
1790
+ const model = models[api.modelName];
1791
+ if (model === undefined) {
1792
+ throw new Error(`Model not found: ${api.modelName}`);
1793
+ }
1794
+ const method =
1795
+ model[
1796
+ /* SAFETY: 동기화기가 모델 클래스에서 수집한 API 메서드 이름만 전달한다. */ api.methodName as keyof typeof model
1797
+ ];
1798
+ if (!isFunctionValue(method)) {
1799
+ throw new Error(`Model method not found: ${api.modelName}.${api.methodName}`);
1800
+ }
1801
+ const result = await Function.prototype.apply.call(method, model, args);
1497
1802
  reply?.type(api.options.contentType ?? "application/json");
1498
1803
 
1499
1804
  return result;
@@ -1519,7 +1824,7 @@ class SonamuClass {
1519
1824
 
1520
1825
  // auth context 추가
1521
1826
  const headers = convertFastifyHeadersToStandard(request.headers);
1522
- const session = (await this._auth?.api.getSession({ headers })) ?? null;
1827
+ const session = (await this.authValue?.api.getSession({ headers })) ?? null;
1523
1828
 
1524
1829
  const context: Context = await Promise.resolve(
1525
1830
  config.contextProvider(
@@ -1547,14 +1852,14 @@ class SonamuClass {
1547
1852
  async createWebSocketContext(
1548
1853
  config: SonamuFastifyConfig,
1549
1854
  request: FastifyRequest,
1550
- ws: WebSocketContext["ws"],
1855
+ ws: AnyWebSocketConnection,
1551
1856
  ): Promise<WebSocketContext> {
1552
1857
  const locale =
1553
1858
  this.detectLocale(request.headers["accept-language"], this.config.i18n.supportedLocales) ??
1554
1859
  this.config.i18n.defaultLocale;
1555
1860
 
1556
1861
  const headers = convertFastifyHeadersToStandard(request.headers);
1557
- const session = (await this._auth?.api.getSession({ headers })) ?? null;
1862
+ const session = (await this.authValue?.api.getSession({ headers })) ?? null;
1558
1863
 
1559
1864
  const defaultContext = {
1560
1865
  transport: "ws" as const,
@@ -1575,11 +1880,6 @@ class SonamuClass {
1575
1880
 
1576
1881
  // reply/createSSE에 의존하는 contextProvider가 있으면 즉시 에러를 던져 transport misuse를 빨리 드러냄
1577
1882
  const replyStub = createWebSocketReplyStub();
1578
- const createSSE = <T extends ZodObject>(_events: T) => {
1579
- throw new Error(
1580
- "createSSE is not available in websocket context. Define websocketContextProvider if your context setup depends on SSE helpers.",
1581
- );
1582
- };
1583
1883
  const httpLikeContext = await Promise.resolve(
1584
1884
  config.contextProvider(
1585
1885
  {
@@ -1587,7 +1887,7 @@ class SonamuClass {
1587
1887
  request,
1588
1888
  reply: replyStub,
1589
1889
  headers: request.headers,
1590
- createSSE,
1890
+ createSSE: createUnavailableSSE,
1591
1891
  naiteStore: defaultContext.naiteStore,
1592
1892
  locale,
1593
1893
  user: defaultContext.user,
@@ -1684,7 +1984,11 @@ class SonamuClass {
1684
1984
  const compressPlugin = (await import("@fastify/compress")).default;
1685
1985
  const defaultOptions = {
1686
1986
  threshold: 1024,
1687
- encodings: ["br", "gzip", "deflate"] as ("br" | "gzip" | "deflate")[],
1987
+ encodings: /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ [
1988
+ "br",
1989
+ "gzip",
1990
+ "deflate",
1991
+ ] as ("br" | "gzip" | "deflate")[],
1688
1992
  };
1689
1993
 
1690
1994
  if (plugins.compress === true) {
@@ -1721,7 +2025,10 @@ class SonamuClass {
1721
2025
  };
1722
2026
 
1723
2027
  for (const [key, pluginName] of Object.entries(pluginsModules)) {
1724
- await registerPlugin(key as keyof typeof plugins, pluginName);
2028
+ await registerPlugin(
2029
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ key as keyof typeof plugins,
2030
+ pluginName,
2031
+ );
1725
2032
  }
1726
2033
 
1727
2034
  if (plugins.ws) {
@@ -1820,23 +2127,24 @@ class SonamuClass {
1820
2127
  }
1821
2128
 
1822
2129
  private async printStartupSummary() {
1823
- const chalk = (await import("chalk")).default;
1824
2130
  const activePreset = getSonamuEnvironment();
1825
2131
 
1826
- const dim = (msg: string) => console.log(chalk.dim(`✓ ${msg}`));
1827
- const green = (msg: string) => console.log(chalk.green(`✓ ${msg}`));
1828
-
1829
- dim(`Config loaded${formatTime(this._configElapsed)}`);
2132
+ printDimSummary(`Config loaded${formatTime(this.configElapsed)}`);
1830
2133
 
1831
2134
  // DB preset 목록
1832
- green("DB");
2135
+ printGreenSummary("DB");
1833
2136
  const { isLocal } = await import("../utils/controller");
1834
- const presetNames = Object.keys(this.dbConfig) as (keyof SonamuDBConfig)[];
2137
+ const presetNames =
2138
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ Object.keys(
2139
+ this.dbConfig,
2140
+ ) as (keyof SonamuDBConfig)[];
1835
2141
  const maxLen = Math.max(...presetNames.map((n) => n.length));
1836
2142
  for (const name of presetNames) {
1837
- const conn = this.dbConfig[name].connection as
1838
- | { host?: string; port?: number; database?: string }
1839
- | undefined;
2143
+ const conn =
2144
+ /* SAFETY: API 데코레이터와 Zod 검증기 등록 계약이 이 값의 타입을 보장한다. */ this
2145
+ .dbConfig[name].connection as
2146
+ | { host?: string; port?: number; database?: string }
2147
+ | undefined;
1840
2148
  const host = conn?.host ?? "localhost";
1841
2149
  const addr = `@ ${host}:${conn?.port ?? 5432}/${conn?.database ?? "(unknown)"}`;
1842
2150
  const padded = name.padEnd(maxLen);
@@ -1851,12 +2159,12 @@ class SonamuClass {
1851
2159
 
1852
2160
  if (this.config.server.auth) {
1853
2161
  const basePath = this.config.server.auth.basePath ?? "/api/auth";
1854
- dim(`Auth: better-auth at ${basePath}/*`);
2162
+ printDimSummary(`Auth: better-auth at ${basePath}/*`);
1855
2163
  }
1856
2164
  if (this.config.api.timezone) {
1857
- dim(`Timezone: ${this.config.api.timezone}`);
2165
+ printDimSummary(`Timezone: ${this.config.api.timezone}`);
1858
2166
  }
1859
- green(`Sonamu ready${formatTime(this._initElapsed)}`);
2167
+ printGreenSummary(`Sonamu ready${formatTime(this.initElapsed)}`);
1860
2168
  }
1861
2169
 
1862
2170
  private async initializeCache(config: CacheConfig | undefined, forTesting: boolean) {
@@ -1865,8 +2173,8 @@ class SonamuClass {
1865
2173
  // 테스트 환경에서 메모리 드라이버 자동 사용
1866
2174
  if (forTesting) {
1867
2175
  const { createTestCacheManager } = await import("../cache/cache-manager");
1868
- this._cache = createTestCacheManager();
1869
- setCacheManagerRef(this._cache);
2176
+ this.cacheValue = createTestCacheManager();
2177
+ setCacheManagerRef(this.cacheValue);
1870
2178
  return;
1871
2179
  }
1872
2180
 
@@ -1878,14 +2186,14 @@ class SonamuClass {
1878
2186
 
1879
2187
  // 설정에 따라 CacheManager 생성
1880
2188
  const { createCacheManager } = await import("../cache/cache-manager");
1881
- this._cache = createCacheManager(config);
1882
- setCacheManagerRef(this._cache);
2189
+ this.cacheValue = createCacheManager(config);
2190
+ setCacheManagerRef(this.cacheValue);
1883
2191
  }
1884
2192
 
1885
2193
  private async initializeWorkflows(options: SonamuTaskOptions | undefined) {
1886
2194
  const { WorkflowManager } = await import("../tasks/workflow-manager");
1887
2195
  // NOTE: @sonamu-kit/tasks 안에선 knex config를 수정하기 때문에 connection이 아닌 config 째로 보냅니다.
1888
- this._workflows = new WorkflowManager(DB.getDBConfig("w"));
2196
+ this.workflowsValue = new WorkflowManager(DB.getDBConfig("w"));
1889
2197
  if (!options) {
1890
2198
  return;
1891
2199
  }
@@ -1939,7 +2247,6 @@ class SonamuClass {
1939
2247
  await options.lifecycle?.onStart?.(server);
1940
2248
  })
1941
2249
  .catch(async (err) => {
1942
- const chalk = (await import("chalk")).default;
1943
2250
  console.error(chalk.red("Failed to start server:", err));
1944
2251
  await shutdown();
1945
2252
  });
@@ -1950,15 +2257,15 @@ class SonamuClass {
1950
2257
  // 먼저 처리해야함.
1951
2258
  await BaseModel.destroy();
1952
2259
  // WebSocket shutdown first to flush telemetry
1953
- if (this._websocketRuntime) {
1954
- await this._websocketRuntime.shutdown();
2260
+ if (this.websocketRuntimeValue) {
2261
+ await this.websocketRuntimeValue.shutdown();
1955
2262
  }
1956
- this._websocketRuntime = null;
2263
+ this.websocketRuntimeValue = null;
1957
2264
  // Then shut down remaining resources in parallel
1958
2265
  await Promise.allSettled([
1959
- this._workflows?.destroy() ?? Promise.resolve(),
1960
- this._cache?.disconnect() ?? Promise.resolve(),
1961
- this._devVitestManager?.shutdown() ?? Promise.resolve(),
2266
+ this.workflowsValue?.destroy() ?? Promise.resolve(),
2267
+ this.cacheValue?.disconnect() ?? Promise.resolve(),
2268
+ this.devVitestManagerValue?.shutdown() ?? Promise.resolve(),
1962
2269
  this.watcher?.close() ?? Promise.resolve(),
1963
2270
  ]);
1964
2271
  // LogTape dispose after WS shutdown so telemetry records are flushed first
@@ -1967,7 +2274,7 @@ class SonamuClass {
1967
2274
  }
1968
2275
 
1969
2276
  function getWebSocketTelemetryContext(
1970
- ws: WebSocketConnection<WebSocketEventMap, WebSocketEventMap>,
2277
+ ws: AnyWebSocketConnection,
1971
2278
  ): WebSocketTelemetryConnectionContext {
1972
2279
  if (!isTelemetryContextProvider(ws)) {
1973
2280
  return {};
@@ -1977,9 +2284,9 @@ function getWebSocketTelemetryContext(
1977
2284
  }
1978
2285
 
1979
2286
  function isTelemetryContextProvider(
1980
- ws: WebSocketConnection<WebSocketEventMap, WebSocketEventMap>,
1981
- ): ws is WebSocketConnection<WebSocketEventMap, WebSocketEventMap> & TelemetryContextProvider {
1982
- return "getTelemetryContext" in ws && typeof ws.getTelemetryContext === "function";
2287
+ ws: AnyWebSocketConnection,
2288
+ ): ws is AnyWebSocketConnection & TelemetryContextProvider {
2289
+ return "getTelemetryContext" in ws && isFunctionValue(ws.getTelemetryContext);
1983
2290
  }
1984
2291
 
1985
2292
  export const Sonamu = new SonamuClass();