@decaf-ts/for-nest 0.11.2 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +274 -2
  2. package/dist/for-nest.cjs +1 -1
  3. package/dist/for-nest.cjs.map +1 -1
  4. package/dist/for-nest.js +1 -1
  5. package/dist/for-nest.js.map +1 -1
  6. package/lib/cjs/auth/AuthInterceptor.cjs +93 -0
  7. package/lib/cjs/auth/AuthInterceptor.cjs.map +1 -0
  8. package/lib/cjs/auth/DecafAuthHandler.cjs +37 -0
  9. package/lib/cjs/auth/DecafAuthHandler.cjs.map +1 -0
  10. package/lib/cjs/auth/DecafAuthModule.cjs +50 -0
  11. package/lib/cjs/auth/DecafAuthModule.cjs.map +1 -0
  12. package/lib/cjs/auth/constants.cjs +10 -0
  13. package/lib/cjs/auth/constants.cjs.map +1 -0
  14. package/lib/cjs/auth/decorators.cjs +28 -0
  15. package/lib/cjs/auth/decorators.cjs.map +1 -0
  16. package/lib/cjs/{ram → auth}/index.cjs +6 -2
  17. package/lib/cjs/auth/index.cjs.map +1 -0
  18. package/lib/cjs/bin/cli.cjs +1 -1
  19. package/lib/cjs/bin/cli.cjs.map +1 -1
  20. package/lib/cjs/cli-module.cjs +63 -6
  21. package/lib/cjs/cli-module.cjs.map +1 -1
  22. package/lib/cjs/constants.cjs +4 -5
  23. package/lib/cjs/constants.cjs.map +1 -1
  24. package/lib/cjs/controllers.cjs +15 -112
  25. package/lib/cjs/controllers.cjs.map +1 -1
  26. package/lib/cjs/core-module.cjs +7 -7
  27. package/lib/cjs/core-module.cjs.map +1 -1
  28. package/lib/cjs/decaf-model/DecafModelModule.cjs +27 -3
  29. package/lib/cjs/decaf-model/DecafModelModule.cjs.map +1 -1
  30. package/lib/cjs/decaf-model/FromModelController.cjs +618 -792
  31. package/lib/cjs/decaf-model/FromModelController.cjs.map +1 -1
  32. package/lib/cjs/decaf-model/decorators/ApiOperationFromModel.cjs +52 -15
  33. package/lib/cjs/decaf-model/decorators/ApiOperationFromModel.cjs.map +1 -1
  34. package/lib/cjs/decaf-model/decorators/ApiParamsFromModel.cjs +1 -1
  35. package/lib/cjs/decaf-model/decorators/ApiParamsFromModel.cjs.map +1 -1
  36. package/lib/cjs/decaf-model/decorators/DecafBody.cjs +1 -1
  37. package/lib/cjs/decaf-model/decorators/DecafBody.cjs.map +1 -1
  38. package/lib/cjs/decaf-model/decorators/DecafParams.cjs +1 -1
  39. package/lib/cjs/decaf-model/decorators/DecafParams.cjs.map +1 -1
  40. package/lib/cjs/decaf-model/decorators/controller-config.cjs +29 -0
  41. package/lib/cjs/decaf-model/decorators/controller-config.cjs.map +1 -0
  42. package/lib/cjs/decaf-model/decorators/decorators.cjs +4 -44
  43. package/lib/cjs/decaf-model/decorators/decorators.cjs.map +1 -1
  44. package/lib/cjs/decaf-model/decorators/expose.cjs +13 -0
  45. package/lib/cjs/decaf-model/decorators/expose.cjs.map +1 -0
  46. package/lib/cjs/decaf-model/decorators/index.cjs +2 -1
  47. package/lib/cjs/decaf-model/decorators/index.cjs.map +1 -1
  48. package/lib/cjs/decaf-model/decorators/types.cjs +1 -1
  49. package/lib/cjs/decaf-model/decorators/types.cjs.map +1 -1
  50. package/lib/cjs/decaf-model/decorators/utils.cjs +3 -2
  51. package/lib/cjs/decaf-model/decorators/utils.cjs.map +1 -1
  52. package/lib/cjs/decaf-model/index.cjs +27 -18
  53. package/lib/cjs/decaf-model/index.cjs.map +1 -1
  54. package/lib/cjs/decaf-model/types.cjs +1 -1
  55. package/lib/cjs/decaf-model/types.cjs.map +1 -1
  56. package/lib/cjs/decaf-model/utils.cjs +10 -4
  57. package/lib/cjs/decaf-model/utils.cjs.map +1 -1
  58. package/lib/cjs/decoration.cjs +1 -1
  59. package/lib/cjs/decoration.cjs.map +1 -1
  60. package/lib/cjs/decorators.cjs +93 -0
  61. package/lib/cjs/decorators.cjs.map +1 -0
  62. package/lib/cjs/events-module/DecafStreamModule.cjs +1 -1
  63. package/lib/cjs/events-module/DecafStreamModule.cjs.map +1 -1
  64. package/lib/cjs/events-module/EventsController.cjs +1 -1
  65. package/lib/cjs/events-module/EventsController.cjs.map +1 -1
  66. package/lib/cjs/events-module/constant.cjs +1 -1
  67. package/lib/cjs/events-module/constant.cjs.map +1 -1
  68. package/lib/cjs/events-module/index.cjs +1 -1
  69. package/lib/cjs/events-module/index.cjs.map +1 -1
  70. package/lib/cjs/events-module/utils.cjs +1 -1
  71. package/lib/cjs/events-module/utils.cjs.map +1 -1
  72. package/lib/cjs/factory/NestBootstraper.cjs +11 -4
  73. package/lib/cjs/factory/NestBootstraper.cjs.map +1 -1
  74. package/lib/cjs/factory/errors/cors.cjs +1 -1
  75. package/lib/cjs/factory/errors/cors.cjs.map +1 -1
  76. package/lib/cjs/factory/errors/index.cjs +1 -1
  77. package/lib/cjs/factory/errors/index.cjs.map +1 -1
  78. package/lib/cjs/factory/errors/throttling.cjs +1 -1
  79. package/lib/cjs/factory/errors/throttling.cjs.map +1 -1
  80. package/lib/cjs/factory/exceptions/DecafErrorFilter.cjs +105 -6
  81. package/lib/cjs/factory/exceptions/DecafErrorFilter.cjs.map +1 -1
  82. package/lib/cjs/factory/exceptions/decorators.cjs +4 -1
  83. package/lib/cjs/factory/exceptions/decorators.cjs.map +1 -1
  84. package/lib/cjs/factory/exceptions/index.cjs +1 -1
  85. package/lib/cjs/factory/exceptions/index.cjs.map +1 -1
  86. package/lib/cjs/factory/index.cjs +1 -1
  87. package/lib/cjs/factory/index.cjs.map +1 -1
  88. package/lib/cjs/factory/openapi/DtoBuilder.cjs +36 -5
  89. package/lib/cjs/factory/openapi/DtoBuilder.cjs.map +1 -1
  90. package/lib/cjs/factory/openapi/SwaggerBuilder.cjs +1 -1
  91. package/lib/cjs/factory/openapi/SwaggerBuilder.cjs.map +1 -1
  92. package/lib/cjs/factory/openapi/SwaggerCustomUI.cjs +1 -1
  93. package/lib/cjs/factory/openapi/SwaggerCustomUI.cjs.map +1 -1
  94. package/lib/cjs/factory/openapi/constants.cjs +1 -1
  95. package/lib/cjs/factory/openapi/constants.cjs.map +1 -1
  96. package/lib/cjs/factory/openapi/index.cjs +1 -1
  97. package/lib/cjs/factory/openapi/index.cjs.map +1 -1
  98. package/lib/cjs/index.cjs +6 -5
  99. package/lib/cjs/index.cjs.map +1 -1
  100. package/lib/cjs/interceptors/DecafRequestHandlerInterceptor.cjs +24 -20
  101. package/lib/cjs/interceptors/DecafRequestHandlerInterceptor.cjs.map +1 -1
  102. package/lib/cjs/interceptors/index.cjs +2 -3
  103. package/lib/cjs/interceptors/index.cjs.map +1 -1
  104. package/lib/cjs/module.cjs +4 -1
  105. package/lib/cjs/module.cjs.map +1 -1
  106. package/lib/cjs/overrides/Adapter.cjs +1 -1
  107. package/lib/cjs/overrides/Adapter.cjs.map +1 -1
  108. package/lib/cjs/overrides/ModelBuilderExtensions.cjs +1 -1
  109. package/lib/cjs/overrides/ModelBuilderExtensions.cjs.map +1 -1
  110. package/lib/cjs/overrides/constants.cjs +1 -1
  111. package/lib/cjs/overrides/constants.cjs.map +1 -1
  112. package/lib/cjs/overrides/decoration.cjs +1 -1
  113. package/lib/cjs/overrides/decoration.cjs.map +1 -1
  114. package/lib/cjs/overrides/helpers.cjs +1 -1
  115. package/lib/cjs/overrides/helpers.cjs.map +1 -1
  116. package/lib/cjs/overrides/index.cjs +2 -1
  117. package/lib/cjs/overrides/index.cjs.map +1 -1
  118. package/lib/cjs/overrides/logging.cjs +5 -0
  119. package/lib/cjs/overrides/logging.cjs.map +1 -0
  120. package/lib/cjs/overrides/overrides.cjs +1 -1
  121. package/lib/cjs/overrides/overrides.cjs.map +1 -1
  122. package/lib/cjs/request/DecafAuthHandler.cjs +4 -38
  123. package/lib/cjs/request/DecafAuthHandler.cjs.map +1 -1
  124. package/lib/cjs/request/DecafHandlerExecutor.cjs +1 -1
  125. package/lib/cjs/request/DecafHandlerExecutor.cjs.map +1 -1
  126. package/lib/cjs/request/DecafRequestContext.cjs +12 -19
  127. package/lib/cjs/request/DecafRequestContext.cjs.map +1 -1
  128. package/lib/cjs/request/DecafResponseInterceptor.cjs +5 -3
  129. package/lib/cjs/request/DecafResponseInterceptor.cjs.map +1 -1
  130. package/lib/cjs/request/index.cjs +1 -1
  131. package/lib/cjs/request/index.cjs.map +1 -1
  132. package/lib/cjs/swagger-types.cjs +1 -1
  133. package/lib/cjs/swagger-types.cjs.map +1 -1
  134. package/lib/cjs/types.cjs +1 -1
  135. package/lib/cjs/types.cjs.map +1 -1
  136. package/lib/cjs/utils.cjs +1 -1
  137. package/lib/cjs/utils.cjs.map +1 -1
  138. package/lib/cjs/webhooks/DecafWebhookModule.cjs +4 -3
  139. package/lib/cjs/webhooks/DecafWebhookModule.cjs.map +1 -1
  140. package/lib/cjs/webhooks/controllers.cjs +49 -54
  141. package/lib/cjs/webhooks/controllers.cjs.map +1 -1
  142. package/lib/cjs/webhooks/index.cjs +1 -1
  143. package/lib/cjs/webhooks/index.cjs.map +1 -1
  144. package/lib/cjs/webhooks/types.cjs +1 -1
  145. package/lib/cjs/webhooks/types.cjs.map +1 -1
  146. package/lib/esm/auth/AuthInterceptor.js +84 -0
  147. package/lib/esm/auth/AuthInterceptor.js.map +1 -0
  148. package/lib/esm/auth/DecafAuthHandler.js +31 -0
  149. package/lib/esm/auth/DecafAuthHandler.js.map +1 -0
  150. package/lib/esm/auth/DecafAuthModule.js +42 -0
  151. package/lib/esm/auth/DecafAuthModule.js.map +1 -0
  152. package/lib/esm/auth/constants.js +6 -0
  153. package/lib/esm/auth/constants.js.map +1 -0
  154. package/lib/esm/auth/decorators.js +22 -0
  155. package/lib/esm/auth/decorators.js.map +1 -0
  156. package/lib/esm/auth/index.js +6 -0
  157. package/lib/esm/auth/index.js.map +1 -0
  158. package/lib/esm/cli-module.js +62 -5
  159. package/lib/esm/cli-module.js.map +1 -1
  160. package/lib/esm/constants.js +2 -3
  161. package/lib/esm/constants.js.map +1 -1
  162. package/lib/esm/controllers.js +15 -112
  163. package/lib/esm/controllers.js.map +1 -1
  164. package/lib/esm/core-module.js +6 -6
  165. package/lib/esm/core-module.js.map +1 -1
  166. package/lib/esm/decaf-model/DecafModelModule.js +26 -2
  167. package/lib/esm/decaf-model/DecafModelModule.js.map +1 -1
  168. package/lib/esm/decaf-model/FromModelController.js +622 -796
  169. package/lib/esm/decaf-model/FromModelController.js.map +1 -1
  170. package/lib/esm/decaf-model/decorators/ApiOperationFromModel.js +52 -14
  171. package/lib/esm/decaf-model/decorators/ApiOperationFromModel.js.map +1 -1
  172. package/lib/esm/decaf-model/decorators/controller-config.js +25 -0
  173. package/lib/esm/decaf-model/decorators/controller-config.js.map +1 -0
  174. package/lib/esm/decaf-model/decorators/decorators.js +1 -42
  175. package/lib/esm/decaf-model/decorators/decorators.js.map +1 -1
  176. package/lib/esm/decaf-model/decorators/expose.js +9 -0
  177. package/lib/esm/decaf-model/decorators/expose.js.map +1 -0
  178. package/lib/esm/decaf-model/decorators/index.js +1 -0
  179. package/lib/esm/decaf-model/decorators/index.js.map +1 -1
  180. package/lib/esm/decaf-model/decorators/utils.js +2 -1
  181. package/lib/esm/decaf-model/decorators/utils.js.map +1 -1
  182. package/lib/esm/decaf-model/index.js +10 -3
  183. package/lib/esm/decaf-model/index.js.map +1 -1
  184. package/lib/esm/decaf-model/utils.js +9 -3
  185. package/lib/esm/decaf-model/utils.js.map +1 -1
  186. package/lib/esm/decorators.js +87 -0
  187. package/lib/esm/decorators.js.map +1 -0
  188. package/lib/esm/events-module/utils.js.map +1 -1
  189. package/lib/esm/factory/NestBootstraper.js +10 -3
  190. package/lib/esm/factory/NestBootstraper.js.map +1 -1
  191. package/lib/esm/factory/exceptions/DecafErrorFilter.js +105 -7
  192. package/lib/esm/factory/exceptions/DecafErrorFilter.js.map +1 -1
  193. package/lib/esm/factory/exceptions/decorators.js +3 -0
  194. package/lib/esm/factory/exceptions/decorators.js.map +1 -1
  195. package/lib/esm/factory/openapi/DtoBuilder.js +35 -4
  196. package/lib/esm/factory/openapi/DtoBuilder.js.map +1 -1
  197. package/lib/esm/index.js +5 -4
  198. package/lib/esm/index.js.map +1 -1
  199. package/lib/esm/interceptors/DecafRequestHandlerInterceptor.js +24 -20
  200. package/lib/esm/interceptors/DecafRequestHandlerInterceptor.js.map +1 -1
  201. package/lib/esm/interceptors/index.js +1 -2
  202. package/lib/esm/interceptors/index.js.map +1 -1
  203. package/lib/esm/module.js +3 -0
  204. package/lib/esm/module.js.map +1 -1
  205. package/lib/esm/overrides/index.js +1 -0
  206. package/lib/esm/overrides/index.js.map +1 -1
  207. package/lib/esm/overrides/logging.js +2 -0
  208. package/lib/esm/overrides/logging.js.map +1 -0
  209. package/lib/esm/request/DecafAuthHandler.js +1 -35
  210. package/lib/esm/request/DecafAuthHandler.js.map +1 -1
  211. package/lib/esm/request/DecafRequestContext.js +11 -18
  212. package/lib/esm/request/DecafRequestContext.js.map +1 -1
  213. package/lib/esm/request/DecafResponseInterceptor.js +4 -2
  214. package/lib/esm/request/DecafResponseInterceptor.js.map +1 -1
  215. package/lib/esm/webhooks/DecafWebhookModule.js +2 -1
  216. package/lib/esm/webhooks/DecafWebhookModule.js.map +1 -1
  217. package/lib/esm/webhooks/controllers.js +48 -53
  218. package/lib/esm/webhooks/controllers.js.map +1 -1
  219. package/lib/types/{interceptors → auth}/AuthInterceptor.d.cts +4 -1
  220. package/lib/types/{interceptors → auth}/AuthInterceptor.d.mts +4 -1
  221. package/lib/types/auth/DecafAuthHandler.d.cts +22 -0
  222. package/lib/types/auth/DecafAuthHandler.d.mts +22 -0
  223. package/lib/types/auth/DecafAuthModule.d.cts +9 -0
  224. package/lib/types/auth/DecafAuthModule.d.mts +9 -0
  225. package/lib/types/auth/constants.d.cts +5 -0
  226. package/lib/types/auth/constants.d.mts +5 -0
  227. package/lib/types/auth/decorators.d.cts +4 -0
  228. package/lib/types/auth/decorators.d.mts +4 -0
  229. package/lib/types/auth/index.d.cts +6 -0
  230. package/lib/types/auth/index.d.mts +6 -0
  231. package/lib/types/cli-module.d.cts +1 -4
  232. package/lib/types/cli-module.d.mts +1 -4
  233. package/lib/types/constants.d.cts +2 -3
  234. package/lib/types/constants.d.mts +2 -3
  235. package/lib/types/controllers.d.cts +7 -12
  236. package/lib/types/controllers.d.mts +7 -12
  237. package/lib/types/decaf-model/DecafModelModule.d.cts +1 -0
  238. package/lib/types/decaf-model/DecafModelModule.d.mts +1 -0
  239. package/lib/types/decaf-model/FromModelController.d.cts +32 -52
  240. package/lib/types/decaf-model/FromModelController.d.mts +32 -52
  241. package/lib/types/decaf-model/decorators/ApiOperationFromModel.d.cts +0 -2
  242. package/lib/types/decaf-model/decorators/ApiOperationFromModel.d.mts +0 -2
  243. package/lib/types/decaf-model/decorators/controller-config.d.cts +18 -0
  244. package/lib/types/decaf-model/decorators/controller-config.d.mts +18 -0
  245. package/lib/types/decaf-model/decorators/decorators.d.cts +1 -29
  246. package/lib/types/decaf-model/decorators/decorators.d.mts +1 -29
  247. package/lib/types/decaf-model/decorators/expose.d.cts +1 -0
  248. package/lib/types/decaf-model/decorators/expose.d.mts +1 -0
  249. package/lib/types/decaf-model/decorators/index.d.cts +1 -0
  250. package/lib/types/decaf-model/decorators/index.d.mts +1 -0
  251. package/lib/types/decaf-model/decorators/types.d.cts +8 -21
  252. package/lib/types/decaf-model/decorators/types.d.mts +8 -21
  253. package/lib/types/decaf-model/index.d.cts +11 -3
  254. package/lib/types/decaf-model/index.d.mts +11 -3
  255. package/lib/types/decaf-model/utils.d.cts +2 -1
  256. package/lib/types/decaf-model/utils.d.mts +2 -1
  257. package/lib/types/decorators.d.cts +35 -0
  258. package/lib/types/decorators.d.mts +35 -0
  259. package/lib/types/factory/NestBootstraper.d.cts +5 -0
  260. package/lib/types/factory/NestBootstraper.d.mts +5 -0
  261. package/lib/types/factory/exceptions/DecafErrorFilter.d.cts +33 -1
  262. package/lib/types/factory/exceptions/DecafErrorFilter.d.mts +33 -1
  263. package/lib/types/factory/openapi/DtoBuilder.d.cts +7 -3
  264. package/lib/types/factory/openapi/DtoBuilder.d.mts +7 -3
  265. package/lib/types/index.d.cts +5 -4
  266. package/lib/types/index.d.mts +5 -4
  267. package/lib/types/interceptors/DecafRequestHandlerInterceptor.d.cts +1 -2
  268. package/lib/types/interceptors/DecafRequestHandlerInterceptor.d.mts +1 -2
  269. package/lib/types/interceptors/index.d.cts +1 -2
  270. package/lib/types/interceptors/index.d.mts +1 -2
  271. package/lib/types/overrides/Adapter.d.cts +1 -1
  272. package/lib/types/overrides/Adapter.d.mts +1 -1
  273. package/lib/types/overrides/index.d.cts +1 -0
  274. package/lib/types/overrides/index.d.mts +1 -0
  275. package/lib/types/overrides/logging.d.cts +1 -0
  276. package/lib/types/overrides/logging.d.mts +1 -0
  277. package/lib/types/request/DecafAuthHandler.d.cts +1 -10
  278. package/lib/types/request/DecafAuthHandler.d.mts +1 -10
  279. package/lib/types/request/DecafRequestContext.d.cts +5 -5
  280. package/lib/types/request/DecafRequestContext.d.mts +5 -5
  281. package/lib/types/types.d.cts +12 -24
  282. package/lib/types/types.d.mts +12 -24
  283. package/package.json +12 -13
  284. package/lib/cjs/interceptors/AuthInterceptor.cjs +0 -52
  285. package/lib/cjs/interceptors/AuthInterceptor.cjs.map +0 -1
  286. package/lib/cjs/interceptors/context.cjs +0 -18
  287. package/lib/cjs/interceptors/context.cjs.map +0 -1
  288. package/lib/cjs/migrations/index.cjs +0 -35
  289. package/lib/cjs/migrations/index.cjs.map +0 -1
  290. package/lib/cjs/migrations/migration-module.cjs +0 -60
  291. package/lib/cjs/migrations/migration-module.cjs.map +0 -1
  292. package/lib/cjs/migrations/migration-options.cjs +0 -4
  293. package/lib/cjs/migrations/migration-options.cjs.map +0 -1
  294. package/lib/cjs/ram/RamRequestTransformer.cjs +0 -26
  295. package/lib/cjs/ram/RamRequestTransformer.cjs.map +0 -1
  296. package/lib/cjs/ram/index.cjs.map +0 -1
  297. package/lib/esm/interceptors/AuthInterceptor.js +0 -43
  298. package/lib/esm/interceptors/AuthInterceptor.js.map +0 -1
  299. package/lib/esm/interceptors/context.js +0 -12
  300. package/lib/esm/interceptors/context.js.map +0 -1
  301. package/lib/esm/migrations/index.js +0 -18
  302. package/lib/esm/migrations/index.js.map +0 -1
  303. package/lib/esm/migrations/migration-module.js +0 -52
  304. package/lib/esm/migrations/migration-module.js.map +0 -1
  305. package/lib/esm/migrations/migration-options.js +0 -7
  306. package/lib/esm/migrations/migration-options.js.map +0 -1
  307. package/lib/esm/ram/RamRequestTransformer.js +0 -21
  308. package/lib/esm/ram/RamRequestTransformer.js.map +0 -1
  309. package/lib/esm/ram/index.js +0 -2
  310. package/lib/esm/ram/index.js.map +0 -1
  311. package/lib/types/interceptors/context.d.cts +0 -6
  312. package/lib/types/interceptors/context.d.mts +0 -6
  313. package/lib/types/migrations/index.d.cts +0 -17
  314. package/lib/types/migrations/index.d.mts +0 -17
  315. package/lib/types/migrations/migration-module.d.cts +0 -15
  316. package/lib/types/migrations/migration-module.d.mts +0 -15
  317. package/lib/types/migrations/migration-options.d.cts +0 -11
  318. package/lib/types/migrations/migration-options.d.mts +0 -11
  319. package/lib/types/ram/RamRequestTransformer.d.cts +0 -5
  320. package/lib/types/ram/RamRequestTransformer.d.mts +0 -5
  321. package/lib/types/ram/index.d.cts +0 -1
  322. package/lib/types/ram/index.d.mts +0 -1
@@ -7,75 +7,33 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- var __param = (this && this.__param) || function (paramIndex, decorator) {
11
- return function (target, key) { decorator(target, key, paramIndex); }
12
- };
13
- import { Controller, Param, Query, Response } from "@nestjs/common";
14
- import { ApiBadRequestResponse, ApiBody, ApiCreatedResponse, ApiExtraModels, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnprocessableEntityResponse, getSchemaPath, } from "@nestjs/swagger";
10
+ var DynamicModelController_1;
11
+ import { Controller, Param, Query, Response, SetMetadata, } from "@nestjs/common";
12
+ import { ApiBadRequestResponse, ApiBody, ApiCreatedResponse, ApiExtraModels, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnprocessableEntityResponse, getSchemaPath, } from "@nestjs/swagger";
15
13
  import { ModelService, OrderDirection, PersistenceKeys, PreparedStatementKeys, Repository, Service, } from "@decaf-ts/core";
16
14
  import { Model } from "@decaf-ts/decorator-validation";
17
15
  import { Logging, toKebabCase } from "@decaf-ts/logging";
18
- import { BulkCrudOperationKeys, DBKeys, OperationKeys, ValidationError, } from "@decaf-ts/db-decorators";
16
+ import { DBKeys, BaseError, InternalError, OperationKeys, } from "@decaf-ts/db-decorators";
19
17
  import { Metadata } from "@decaf-ts/decoration";
20
- import { ApiOperationFromModel, ApiParamsFromModel, DecafBody, DecafParams, DecafQuery, } from "./decorators/index.js";
18
+ import { ApiOperationFromModel, ApiParamsFromModel, DecafBody, DecafQuery, } from "./decorators/index.js";
19
+ import { HttpVerbToDecorator } from "./decorators/utils.js";
21
20
  import { DecafRequestContext } from "./../request/index.js";
22
- import { DECAF_ROUTE } from "./../constants.js";
23
- import { applyApiDecorators, createRouteHandler, defineRouteMethod, getApiDecorators, resolvePersistenceMethod, } from "./utils.js";
24
- import { Auth } from "./decorators/decorators.js";
21
+ import { DECAF_CONTROLLER_CONFIG, DECAF_ROUTE } from "./../constants.js";
22
+ import { SKIP_MODEL_ROLES_KEY } from "./../auth/constants.js";
23
+ import { Auth, Public, RequireRoles } from "./../auth/decorators.js";
25
24
  import { DecafModelController } from "./../controllers.js";
26
25
  import { DtoFor } from "./../factory/openapi/DtoBuilder.js";
27
26
  import "./../overrides/index.js";
28
- /**
29
- * @description
30
- * Factory and utilities for generating dynamic NestJS controllers from Decaf {@link Model} definitions.
31
- *
32
- * @summary
33
- * The `FromModelController` class provides the infrastructure necessary to automatically generate
34
- * strongly-typed CRUD controllers based on a given {@link ModelConstructor}. It inspects metadata from
35
- * the model, derives route paths, parameters, and generates a dynamic controller class at runtime with
36
- * full support for querying, creation, update, and deletion of model entities through a {@link Repo}.
37
- *
38
- * @template T The {@link Model} type associated with the generated controller.
39
- *
40
- * @param ModelClazz The model class to generate the controller from.
41
- *
42
- * @class FromModelController
43
- *
44
- * @example
45
- * ```ts
46
- * // Given a Decaf Model:
47
- * class User extends Model<User> {
48
- * id!: string;
49
- * name!: string;
50
- * }
51
- *
52
- * // Register controller:
53
- * const UserController = FromModelController.create(User);
54
- *
55
- * // NestJS will expose:
56
- * // POST /user
57
- * // GET /user/:id
58
- * // GET /user/query/:method
59
- * // PUT /user/:id
60
- * // DELETE /user/:id
61
- * ```
62
- *
63
- * @mermaid
64
- * sequenceDiagram
65
- * participant Client
66
- * participant Controller
67
- * participant Repo
68
- * participant DB
69
- *
70
- * Client->>Controller: HTTP Request
71
- * Controller->>Repo: Resolve repository for Model
72
- * Repo->>DB: Execute DB operation
73
- * DB-->>Repo: DB Result
74
- * Repo-->>Controller: Model Instance(s)
75
- * Controller-->>Client: JSON Response
76
- */
27
+ import { ModelControllerFactory, } from "@decaf-ts/for-http/server";
77
28
  export class FromModelController {
78
29
  static { this.log = Logging.for(FromModelController.name); }
30
+ static toDecafError(error, fallbackMessage) {
31
+ if (error instanceof BaseError)
32
+ return error;
33
+ return new InternalError(error instanceof Error
34
+ ? `${fallbackMessage}: ${error.message}`
35
+ : fallbackMessage);
36
+ }
79
37
  static getPersistence(ModelClazz) {
80
38
  try {
81
39
  return Service.get(ModelClazz);
@@ -83,756 +41,537 @@ export class FromModelController {
83
41
  catch (e) {
84
42
  try {
85
43
  return ModelService.getService(ModelClazz);
86
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
87
44
  }
88
- catch (e) {
45
+ catch (e2) {
89
46
  return Repository.forModel(ModelClazz);
90
47
  }
91
48
  }
92
49
  }
93
50
  static createQueryRoutesFromRepository(persistence, prefix = PersistenceKeys.QUERY) {
94
- const log = FromModelController.log.for(FromModelController.createQueryRoutesFromRepository);
95
51
  const repo = persistence instanceof ModelService ? persistence.repo : persistence;
96
- const ModelConstr = repo.class;
97
52
  const queryMethods = Metadata.get(repo.constructor, Metadata.key(PersistenceKeys.QUERY)) ?? {};
98
53
  const routeMethods = Metadata.get(persistence.constructor, Metadata.key(DECAF_ROUTE)) ?? {};
99
- // create base class
100
54
  class QueryController extends DecafModelController {
101
55
  get class() {
102
- throw new Error("Method not implemented.");
56
+ throw new InternalError("Method not implemented.");
103
57
  }
104
58
  constructor(clientContext, name) {
105
59
  super(clientContext, name);
106
60
  }
107
61
  }
108
62
  for (const [methodName, params] of Object.entries(routeMethods)) {
109
- // regex to trim slashes from start and end
110
63
  const routePath = [params.path.replace(/^\/+|\/+$/g, "")]
111
64
  .filter((segment) => segment && segment.trim())
112
65
  .join("/");
113
- // const handler = params.handler.value;
114
- const handler = createRouteHandler(methodName);
115
- if (!handler) {
116
- const message = `Invalid or missing handler for model ${ModelConstr.name} on decorated method ${methodName}`;
117
- log.error(message);
118
- throw new Error(message);
119
- }
120
- const descriptor = defineRouteMethod(QueryController, methodName, handler);
121
- if (descriptor) {
122
- const decorators = getApiDecorators(methodName, routePath, params.httpMethod);
123
- applyApiDecorators(QueryController, methodName, descriptor, decorators);
124
- }
66
+ const handler = FromModelController.createComplexQueryHandler(methodName);
67
+ FromModelController.defineMethod(QueryController, methodName, handler);
68
+ const httpDecorator = HttpVerbToDecorator(params.httpMethod)(routePath || undefined);
69
+ const decorators = FromModelController.getQueryDecorators(methodName, routePath, params.httpMethod);
70
+ FromModelController.applyDecorators(QueryController, methodName, [
71
+ httpDecorator,
72
+ ...decorators,
73
+ ]);
125
74
  }
126
75
  for (const [methodName, objValues] of Object.entries(queryMethods)) {
127
76
  const fields = objValues.fields ?? [];
128
77
  const routePath = [prefix, methodName, ...fields.map((f) => `:${f}`)]
129
78
  .filter((segment) => segment && segment.trim())
130
79
  .join("/");
131
- const handler = createRouteHandler(methodName);
132
- const descriptor = defineRouteMethod(QueryController, methodName, handler);
133
- if (descriptor) {
134
- const decorators = getApiDecorators(methodName, routePath, "GET", true);
135
- applyApiDecorators(QueryController, methodName, descriptor, decorators);
136
- }
80
+ const handler = FromModelController.createComplexQueryHandler(methodName);
81
+ FromModelController.defineMethod(QueryController, methodName, handler);
82
+ const httpDecorator = HttpVerbToDecorator("GET")(routePath || undefined);
83
+ const decorators = FromModelController.getQueryDecorators(methodName, routePath, "GET", true);
84
+ FromModelController.applyDecorators(QueryController, methodName, [
85
+ httpDecorator,
86
+ ...decorators,
87
+ ]);
137
88
  }
138
89
  return QueryController;
139
90
  }
140
- static create(ModelConstr) {
91
+ static create(ModelConstr, moduleConfigOverrides, globalDefaults) {
141
92
  const log = FromModelController.log.for(FromModelController.create);
142
93
  const tableName = Model.tableName(ModelConstr);
143
94
  const routePath = toKebabCase(tableName);
144
95
  const modelClazzName = ModelConstr.name;
145
96
  const persistence = FromModelController.getPersistence(ModelConstr);
146
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
147
- const { description, getPK, apiProperties, path } = FromModelController.getRouteParametersFromModel(ModelConstr);
148
- log.debug(`Creating controller for model: ${modelClazzName}`);
149
- const BaseController = FromModelController.createQueryRoutesFromRepository(persistence // instanceof ModelService ? persistence.repo : persistence
150
- );
151
- let DynamicModelController = class DynamicModelController extends BaseController {
97
+ // When persistence is a ModelService, the @query/@route metadata lives on
98
+ // the underlying repository class (custom repo), not on ModelService itself.
99
+ // Pass the repo to the factory so addComplexQueries() can discover them.
100
+ const factoryPersistence = persistence instanceof ModelService ? persistence.repo : persistence;
101
+ const decoratorConfig = Metadata.get(ModelConstr, Metadata.key(DECAF_CONTROLLER_CONFIG));
102
+ const moduleOverride = moduleConfigOverrides?.[ModelConstr.name];
103
+ const mergedConfig = {
104
+ ...(globalDefaults || {}),
105
+ ...(decoratorConfig || {}),
106
+ ...(moduleOverride || {}),
107
+ };
108
+ const FactoryController = ModelControllerFactory.create(ModelConstr, factoryPersistence, mergedConfig);
109
+ const factoryRoutes = FactoryController.__routes__;
110
+ const { getPK, apiProperties, path: pkPath, } = FromModelController.getRouteParametersFromModel(ModelConstr);
111
+ log.debug(`Creating controller for model: ${modelClazzName} with ${factoryRoutes?.length ?? 0} factory routes`);
112
+ const authConfig = mergedConfig.auth;
113
+ function applyClassAuth(target) {
114
+ if (authConfig?.public) {
115
+ Public()(target);
116
+ }
117
+ else if (authConfig?.roles?.length) {
118
+ RequireRoles(...authConfig.roles)(target);
119
+ }
120
+ else {
121
+ Auth(ModelConstr)(target);
122
+ }
123
+ if (authConfig?.skipModelRoles) {
124
+ SetMetadata(SKIP_MODEL_ROLES_KEY, true)(target);
125
+ }
126
+ }
127
+ let DynamicModelController = DynamicModelController_1 = class DynamicModelController extends DecafModelController {
152
128
  static get class() {
153
129
  return ModelConstr;
154
130
  }
155
131
  get class() {
156
132
  return ModelConstr;
157
- // return DynamicModelController.class;
158
133
  }
159
134
  constructor(clientContext) {
160
- super(clientContext);
135
+ super(clientContext, DynamicModelController_1.name);
161
136
  this.pk = Model.pk(ModelConstr);
162
137
  log.info(`Registering dynamic controller for model: ${this.class.name} route: /${routePath}`);
163
138
  }
164
- //
165
- // @ApiOperationFromModel(ModelClazz, "GET", "query/:condition/:orderBy")
166
- // @ApiOperation({ summary: `Retrieve ${modelClazzName} records by query.` })
167
- // @ApiParam({ name: "method", description: "Query method to be called" })
168
- // @ApiOkResponse({
169
- // description: `${modelClazzName} retrieved successfully.`,
170
- // })
171
- // @ApiNotFoundResponse({
172
- // description: `No ${modelClazzName} records matches the query.`,
173
- // })
174
- // async query(
175
- // @Param("condition") condition: Condition<any>,
176
- // @Param("orderBy") orderBy: string,
177
- // @QueryDetails() details: DirectionLimitOffset,
178
- // ) {
179
- // const {direction, limit, offset} = details;
180
- // return this.persistence.query(condition, orderBy as keyof Model, direction, limit, offset);
181
- // }
182
- async listBy(key, details) {
183
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.LIST_BY, true)).for(this.listBy);
184
- return this.persistence(ctx).listBy(key, details.direction, ctx);
185
- }
186
- async paginateBy(key, details) {
187
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.PAGE_BY, true)).for(this.paginateBy);
188
- return this.persistence(ctx).paginateBy(key, details.direction, details, ctx);
189
- }
190
- async find(value, details) {
191
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.FIND, true)).for(this.find);
192
- const direction = (details.direction ??
193
- OrderDirection.ASC);
194
- return resolvePersistenceMethod(this.persistence(ctx), this.find.name, value, direction, ctx);
195
- }
196
- async page(value, details) {
197
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.PAGE, true)).for(this.page);
198
- const { direction = OrderDirection.ASC, limit, offset, bookmark, } = details;
199
- const ref = {
200
- offset: offset ?? 1,
201
- limit: limit ?? 10,
202
- bookmark,
203
- };
204
- return resolvePersistenceMethod(this.persistence(ctx), this.page.name, value, direction, ref, ctx);
205
- }
206
- async findOneBy(key, value) {
207
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.FIND_ONE_BY, true)).for(this.findOneBy);
208
- return this.persistence(ctx).findOneBy(key, value, ctx);
209
- }
210
- async findBy(key, value, details) {
211
- const { ctx } = (await this.logCtx([], PreparedStatementKeys.FIND_BY, true)).for(this.findBy);
212
- return this.persistence(ctx)
213
- .for(ctx.toOverrides())
214
- .findBy(key, value, ctx);
215
- }
216
- async statement(name, args, details) {
217
- const { ctx } = (await this.logCtx([], PersistenceKeys.STATEMENT, true)).for(this.statement);
218
- const { direction, offset, limit, bookmark } = details;
219
- args = args.map((a) => (typeof a === "string" ? parseInt(a) : a) || a);
220
- const pathDirection = args.length > 1 ? args[1] : undefined;
221
- const resolvedDirection = (direction ?? pathDirection);
222
- if (resolvedDirection && args.length > 1)
223
- args[1] = resolvedDirection;
224
- switch (name) {
225
- case PreparedStatementKeys.FIND:
226
- case PreparedStatementKeys.FIND_BY:
227
- break;
228
- case PreparedStatementKeys.LIST_BY:
229
- args.push(direction);
230
- break;
231
- case PreparedStatementKeys.PAGE:
232
- case PreparedStatementKeys.PAGE_BY:
233
- args = [
234
- args[0],
235
- resolvedDirection,
236
- {
237
- limit: limit,
238
- offset: offset,
239
- bookmark: bookmark,
240
- },
241
- ];
242
- break;
243
- case PreparedStatementKeys.FIND_ONE_BY:
244
- break;
245
- case PreparedStatementKeys.COUNT_OF:
246
- case PreparedStatementKeys.MAX_OF:
247
- case PreparedStatementKeys.MIN_OF:
248
- case PreparedStatementKeys.AVG_OF:
249
- case PreparedStatementKeys.SUM_OF:
250
- case PreparedStatementKeys.DISTINCT_OF:
251
- case PreparedStatementKeys.GROUP_OF:
252
- // Aggregation methods - args[0] is the field name (if provided)
253
- break;
139
+ };
140
+ DynamicModelController = DynamicModelController_1 = __decorate([
141
+ Controller(routePath),
142
+ ApiTags(modelClazzName),
143
+ ApiExtraModels(ModelConstr),
144
+ __metadata("design:paramtypes", [DecafRequestContext])
145
+ ], DynamicModelController);
146
+ applyClassAuth(DynamicModelController);
147
+ if (factoryRoutes) {
148
+ const sortedRoutes = [...factoryRoutes].sort((a, b) => {
149
+ const aSegments = a.path.split("/").filter(Boolean);
150
+ const bSegments = b.path.split("/").filter(Boolean);
151
+ const aParamCount = aSegments.filter((s) => s.startsWith(":")).length;
152
+ const bParamCount = bSegments.filter((s) => s.startsWith(":")).length;
153
+ const aLiteralCount = aSegments.length - aParamCount;
154
+ const bLiteralCount = bSegments.length - bParamCount;
155
+ if (aLiteralCount !== bLiteralCount)
156
+ return bLiteralCount - aLiteralCount;
157
+ if (aParamCount !== bParamCount)
158
+ return aParamCount - bParamCount;
159
+ return 0;
160
+ });
161
+ for (const route of sortedRoutes) {
162
+ const methodName = route.implementation?.name || route.method.toLowerCase();
163
+ const registration = FromModelController.matchRoute(methodName, route, pkPath, apiProperties, getPK, ModelConstr, modelClazzName);
164
+ if (!registration)
165
+ continue;
166
+ const { methodName: registeredMethodName, handler, decorators, paramDecorators, } = registration;
167
+ const descriptor = FromModelController.defineMethod(DynamicModelController, registeredMethodName, handler);
168
+ if (descriptor) {
169
+ const httpDecorator = HttpVerbToDecorator(route.method)(route.path.replace(/^\/+|\/+$/g, "") || undefined);
170
+ FromModelController.applyDecorators(DynamicModelController, registeredMethodName, [httpDecorator, ...decorators], paramDecorators);
254
171
  }
255
- return this.persistence(ctx).statement(name, ...args, ctx);
256
172
  }
257
- //
258
- // @ApiOperationFromModel(ModelConstr, "GET", "countOf")
259
- // @ApiOperation({ summary: `Count all ${modelClazzName} records.` })
260
- // @ApiOkResponse({
261
- // description: `Count of ${modelClazzName} records.`,
262
- // type: Number,
263
- // })
264
- // async countOf() {
265
- // const { ctx } = (
266
- // await this.logCtx([], PreparedStatementKeys.COUNT_OF, true)
267
- // ).for(this.countOf);
268
- // return this.persistence(ctx).statement(
269
- // PreparedStatementKeys.COUNT_OF,
270
- // ctx
271
- // );
272
- // }
273
- //
274
- // @ApiOperationFromModel(ModelConstr, "GET", "countOf/:field")
275
- // @ApiOperation({ summary: `Count ${modelClazzName} records by field.` })
276
- // @ApiParam({ name: "field", description: "The field to count" })
277
- // @ApiOkResponse({
278
- // description: `Count of ${modelClazzName} records.`,
279
- // type: Number,
280
- // })
281
- // async countOfField(@Param("field") field: string) {
282
- // const { ctx } = (
283
- // await this.logCtx([], PreparedStatementKeys.COUNT_OF, true)
284
- // ).for(this.countOfField);
285
- // return this.persistence(ctx).statement(
286
- // PreparedStatementKeys.COUNT_OF,
287
- // field,
288
- // ctx
289
- // );
290
- // }
291
- //
292
- // @ApiOperationFromModel(ModelConstr, "GET", "maxOf/:field")
293
- // @ApiOperation({
294
- // summary: `Find maximum value of a field in ${modelClazzName}.`,
295
- // })
296
- // @ApiParam({ name: "field", description: "The field to find max of" })
297
- // @ApiOkResponse({
298
- // description: `Maximum value of the field in ${modelClazzName}.`,
299
- // })
300
- // async maxOf(@Param("field") field: string) {
301
- // const { ctx } = (
302
- // await this.logCtx([], PreparedStatementKeys.MAX_OF, true)
303
- // ).for(this.maxOf);
304
- // return this.persistence(ctx).statement(
305
- // PreparedStatementKeys.MAX_OF,
306
- // field,
307
- // ctx
308
- // );
309
- // }
310
- //
311
- // @ApiOperationFromModel(ModelConstr, "GET", "minOf/:field")
312
- // @ApiOperation({
313
- // summary: `Find minimum value of a field in ${modelClazzName}.`,
314
- // })
315
- // @ApiParam({ name: "field", description: "The field to find min of" })
316
- // @ApiOkResponse({
317
- // description: `Minimum value of the field in ${modelClazzName}.`,
318
- // })
319
- // async minOf(@Param("field") field: string) {
320
- // const { ctx } = (
321
- // await this.logCtx([], PreparedStatementKeys.MIN_OF, true)
322
- // ).for(this.minOf);
323
- // return this.persistence(ctx).statement(
324
- // PreparedStatementKeys.MIN_OF,
325
- // field,
326
- // ctx
327
- // );
328
- // }
329
- //
330
- // @ApiOperationFromModel(ModelConstr, "GET", "avgOf/:field")
331
- // @ApiOperation({
332
- // summary: `Calculate average of a field in ${modelClazzName}.`,
333
- // })
334
- // @ApiParam({
335
- // name: "field",
336
- // description: "The field to calculate average of",
337
- // })
338
- // @ApiOkResponse({
339
- // description: `Average value of the field in ${modelClazzName}.`,
340
- // type: Number,
341
- // })
342
- // async avgOf(@Param("field") field: string) {
343
- // const { ctx } = (
344
- // await this.logCtx([], PreparedStatementKeys.AVG_OF, true)
345
- // ).for(this.avgOf);
346
- // return this.persistence(ctx).statement(
347
- // PreparedStatementKeys.AVG_OF,
348
- // field,
349
- // ctx
350
- // );
351
- // }
352
- //
353
- // @ApiOperationFromModel(ModelConstr, "GET", "sumOf/:field")
354
- // @ApiOperation({
355
- // summary: `Calculate sum of a field in ${modelClazzName}.`,
356
- // })
357
- // @ApiParam({ name: "field", description: "The field to calculate sum of" })
358
- // @ApiOkResponse({
359
- // description: `Sum of the field in ${modelClazzName}.`,
360
- // type: Number,
361
- // })
362
- // async sumOf(@Param("field") field: string) {
363
- // const { ctx } = (
364
- // await this.logCtx([], PreparedStatementKeys.SUM_OF, true)
365
- // ).for(this.sumOf);
366
- // return this.persistence(ctx).statement(
367
- // PreparedStatementKeys.SUM_OF,
368
- // field,
369
- // ctx
370
- // );
371
- // }
372
- //
373
- // @ApiOperationFromModel(ModelConstr, "GET", "distinctOf/:field")
374
- // @ApiOperation({
375
- // summary: `Find distinct values of a field in ${modelClazzName}.`,
376
- // })
377
- // @ApiParam({
378
- // name: "field",
379
- // description: "The field to find distinct values of",
380
- // })
381
- // @ApiOkResponse({
382
- // description: `Distinct values of the field in ${modelClazzName}.`,
383
- // type: [String],
384
- // })
385
- // async distinctOf(@Param("field") field: string) {
386
- // const { ctx } = (
387
- // await this.logCtx([], PreparedStatementKeys.DISTINCT_OF, true)
388
- // ).for(this.distinctOf);
389
- // return this.persistence(ctx).statement(
390
- // PreparedStatementKeys.DISTINCT_OF,
391
- // field,
392
- // ctx
393
- // );
394
- // }
395
- //
396
- // @ApiOperationFromModel(ModelConstr, "GET", "groupOf/:field")
397
- // @ApiOperation({
398
- // summary: `Group ${modelClazzName} records by a field.`,
399
- // })
400
- // @ApiParam({ name: "field", description: "The field to group by" })
401
- // @ApiOkResponse({
402
- // description: `${modelClazzName} records grouped by the field.`,
403
- // })
404
- // async groupOf(@Param("field") field: string) {
405
- // const { ctx } = (
406
- // await this.logCtx([], PreparedStatementKeys.GROUP_OF, true)
407
- // ).for(this.groupOf);
408
- // return this.persistence(ctx).statement(
409
- // PreparedStatementKeys.GROUP_OF,
410
- // field,
411
- // ctx
412
- // );
413
- // }
414
- async createAll(data, resp) {
415
- const { ctx, log } = (await this.logCtx([], BulkCrudOperationKeys.CREATE_ALL, true)).for(this.createAll);
416
- log.verbose(`creating new ${modelClazzName}`);
417
- let created;
418
- try {
419
- created = await this.persistence(ctx).createAll(data.map((d) => new ModelConstr(d)), ctx);
420
- }
421
- catch (e) {
422
- log.error(`Failed to create new ${modelClazzName}`, e);
423
- throw e;
424
- }
425
- log.info(`created new ${modelClazzName} with id ${created[this.pk]}`);
426
- ctx.toResponse(resp);
427
- return created;
173
+ }
174
+ return DynamicModelController;
175
+ }
176
+ static getRouteParametersFromModel(ModelClazz) {
177
+ const pk = Model.pk(ModelClazz);
178
+ const composed = Metadata.get(ModelClazz, Metadata.key(DBKeys.COMPOSED, pk));
179
+ const composedKeys = composed?.args ?? [];
180
+ const uniqueKeys = Array.isArray(composedKeys) && composedKeys.length > 0
181
+ ? Array.from(new Set([...composedKeys]))
182
+ : Array.from(new Set([pk]));
183
+ const description = Metadata.description(ModelClazz) ?? "";
184
+ const path = `:${uniqueKeys.join("/:")}`;
185
+ const apiProperties = uniqueKeys.map((key) => {
186
+ return {
187
+ name: key,
188
+ description: Metadata.description(ModelClazz, key),
189
+ required: true,
190
+ type: String,
191
+ };
192
+ });
193
+ return {
194
+ path,
195
+ description,
196
+ apiProperties,
197
+ getPK: (...params) => composed?.separator ? params.join(composed.separator) : params.join(""),
198
+ };
199
+ }
200
+ static routeParamDecorators(path) {
201
+ let paramIndex = 0;
202
+ return path
203
+ .split("/")
204
+ .filter(Boolean)
205
+ .flatMap((segment) => {
206
+ if (segment.startsWith(":")) {
207
+ return [
208
+ { decorator: Param(segment.slice(1)), index: paramIndex++ },
209
+ ];
428
210
  }
429
- async create(data, resp) {
430
- const { ctx, log } = (await this.logCtx([], OperationKeys.CREATE, true)).for(this.create);
431
- log.verbose(`creating new ${modelClazzName}`);
432
- let created;
433
- try {
434
- const persistence = this.persistence(ctx);
435
- created = await persistence.create(data, ctx);
436
- }
437
- catch (e) {
438
- log.error(`Failed to create new ${modelClazzName}`, e);
439
- throw e;
440
- }
441
- log.info(`created new ${modelClazzName} with id ${created[this.pk]}`);
442
- ctx.toResponse(resp);
443
- return created;
211
+ if (segment.startsWith("*")) {
212
+ return [
213
+ { decorator: Param(segment.slice(1)), index: paramIndex++ },
214
+ ];
444
215
  }
445
- async readAll(ids) {
446
- const { ctx, log } = (await this.logCtx([], BulkCrudOperationKeys.READ_ALL, true)).for(this.readAll);
447
- const normalizedIds = Array.isArray(ids) ? ids : [ids];
448
- let read;
449
- try {
450
- log.debug(`reading ${normalizedIds} ${modelClazzName}: ${normalizedIds}`);
451
- const persistence = this.persistence(ctx);
452
- read = await persistence.readAll(normalizedIds, ctx);
453
- }
454
- catch (e) {
455
- log.error(`Failed to ${modelClazzName} with id ${normalizedIds}`, e);
456
- throw e;
457
- }
458
- log.info(`read ${read.length} ${modelClazzName}`);
459
- return read;
216
+ return [];
217
+ });
218
+ }
219
+ static defineMethod(target, methodName, handler) {
220
+ Object.defineProperty(target.prototype || target, methodName, {
221
+ value: handler,
222
+ writable: false,
223
+ configurable: true,
224
+ enumerable: false,
225
+ });
226
+ return Object.getOwnPropertyDescriptor(target.prototype || target, methodName);
227
+ }
228
+ static applyDecorators(target, methodName, methodDecorators, paramDecorators = []) {
229
+ const proto = target?.prototype ?? target;
230
+ const descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
231
+ methodDecorators.forEach((d) => d(proto, methodName, descriptor));
232
+ paramDecorators.forEach(({ decorator, index }) => decorator(proto, methodName, index));
233
+ }
234
+ static matchRoute(methodName, route, pkPath, apiProperties, getPK, ModelConstr, modelClazzName) {
235
+ const { method, path } = route;
236
+ const normalizedPath = path.replace(/^\/+|\/+$/g, "");
237
+ const handler = route.implementation;
238
+ if (method === "POST" && normalizedPath === "") {
239
+ return FromModelController.createRegistration("create", handler, FromModelController.createCreateDecorators(ModelConstr, modelClazzName), [
240
+ { decorator: DecafBody(), index: 0 },
241
+ { decorator: Response({ passthrough: true }), index: 1 },
242
+ ]);
243
+ }
244
+ if (method === "POST" && normalizedPath === "bulk") {
245
+ return FromModelController.createRegistration("createAll", handler, FromModelController.bulkCreateDecorators(ModelConstr, modelClazzName), [
246
+ { decorator: DecafBody(), index: 0 },
247
+ { decorator: Response({ passthrough: true }), index: 1 },
248
+ ]);
249
+ }
250
+ if (method === "GET" && normalizedPath === "bulk") {
251
+ return FromModelController.createRegistration("readAll", handler, FromModelController.bulkReadDecorators(ModelConstr, modelClazzName), [{ decorator: Query("ids"), index: 0 }]);
252
+ }
253
+ if (method === "PUT" && normalizedPath === "bulk") {
254
+ return FromModelController.createRegistration("updateAll", handler, FromModelController.bulkUpdateDecorators(ModelConstr, modelClazzName, apiProperties), [
255
+ { decorator: DecafBody(), index: 0 },
256
+ { decorator: Response({ passthrough: true }), index: 1 },
257
+ ]);
258
+ }
259
+ if (method === "DELETE" && normalizedPath === "bulk") {
260
+ return FromModelController.createRegistration("deleteAll", handler, FromModelController.bulkDeleteDecorators(ModelConstr, modelClazzName, apiProperties), [
261
+ { decorator: Query("ids"), index: 0 },
262
+ { decorator: Response({ passthrough: true }), index: 1 },
263
+ ]);
264
+ }
265
+ if (method === "GET" && normalizedPath === pkPath) {
266
+ return FromModelController.createRegistration("read", handler, FromModelController.readDecorators(ModelConstr, modelClazzName, apiProperties, pkPath), FromModelController.routeParamDecorators(normalizedPath));
267
+ }
268
+ if (method === "PUT" && normalizedPath === pkPath) {
269
+ const routeParams = FromModelController.routeParamDecorators(normalizedPath);
270
+ return FromModelController.createRegistration("update", handler, FromModelController.updateDecorators(ModelConstr, modelClazzName, apiProperties, pkPath), [
271
+ { decorator: DecafBody(), index: 0 },
272
+ ...routeParams.map(({ decorator, index }) => ({
273
+ decorator,
274
+ index: index + 1,
275
+ })),
276
+ ]);
277
+ }
278
+ if (method === "DELETE" && normalizedPath === pkPath) {
279
+ const routeParams = FromModelController.routeParamDecorators(normalizedPath);
280
+ return FromModelController.createRegistration("delete", handler, FromModelController.deleteDecorators(ModelConstr, modelClazzName, apiProperties, pkPath), routeParams);
281
+ }
282
+ // Composed PK fallback routes (filterEmpty) — path differs from pkPath
283
+ const fallbackSegments = normalizedPath.split("/").filter(Boolean);
284
+ const isAllParams = fallbackSegments.length > 0 &&
285
+ fallbackSegments.every((s) => s.startsWith(":"));
286
+ if (isAllParams && normalizedPath !== pkPath) {
287
+ const fallbackApiProps = fallbackSegments
288
+ .map((s) => s.slice(1))
289
+ .map((name) => ({
290
+ name,
291
+ description: `${name} parameter`,
292
+ required: true,
293
+ type: String,
294
+ }));
295
+ const suffix = fallbackSegments.map((s) => s.slice(1)).join("And");
296
+ const routeParams = FromModelController.routeParamDecorators(normalizedPath);
297
+ if (method === "GET") {
298
+ return FromModelController.createRegistration(`readBy${suffix}`, handler, FromModelController.readDecorators(ModelConstr, modelClazzName, fallbackApiProps, normalizedPath), routeParams);
460
299
  }
461
- async read(routeParams) {
462
- const { ctx, log } = (await this.logCtx([], OperationKeys.READ, true)).for(this.read);
463
- const id = getPK(...routeParams.valuesInOrder);
464
- if (typeof id === "undefined")
465
- throw new ValidationError(`No ${this.pk} provided`);
466
- let read;
467
- try {
468
- log.debug(`reading ${modelClazzName} with ${this.pk} ${id}`);
469
- const persistence = this.persistence(ctx);
470
- read = await persistence.read(id, ctx);
471
- }
472
- catch (e) {
473
- log.error(`Failed to read ${modelClazzName} with id ${id}`, e);
474
- throw e;
475
- }
476
- log.info(`read ${modelClazzName} with id ${read[this.pk]}`);
477
- return read;
300
+ if (method === "PUT") {
301
+ return FromModelController.createRegistration(`updateBy${suffix}`, handler, FromModelController.updateDecorators(ModelConstr, modelClazzName, fallbackApiProps, normalizedPath), [
302
+ { decorator: DecafBody(), index: 0 },
303
+ ...routeParams.map(({ decorator, index }) => ({
304
+ decorator,
305
+ index: index + 1,
306
+ })),
307
+ ]);
478
308
  }
479
- async updateAll(body, resp) {
480
- const { ctx, log } = (await this.logCtx([], BulkCrudOperationKeys.UPDATE_ALL, true)).for(this.updateAll);
481
- let updated;
482
- try {
483
- log.info(`updating ${body.length} ${modelClazzName}`);
484
- updated = await this.persistence(ctx).updateAll(body, ctx);
485
- }
486
- catch (e) {
487
- log.error(e);
488
- throw e;
489
- }
490
- ctx.toResponse(resp);
491
- return updated;
309
+ if (method === "DELETE") {
310
+ return FromModelController.createRegistration(`deleteBy${suffix}`, handler, FromModelController.deleteDecorators(ModelConstr, modelClazzName, fallbackApiProps, normalizedPath), routeParams);
492
311
  }
493
- async update(routeParams, body, resp) {
494
- const { ctx, log } = (await this.logCtx([], OperationKeys.UPDATE, true)).for(this.update);
495
- const id = getPK(...routeParams.valuesInOrder);
496
- if (typeof id === "undefined")
497
- throw new ValidationError(`No ${this.pk} provided`);
498
- let updated;
499
- try {
500
- log.info(`updating ${modelClazzName} with ${this.pk} ${id}`);
501
- const payload = JSON.parse(JSON.stringify(body));
502
- const persistence = this.persistence(ctx);
503
- updated = await persistence.update(new ModelConstr({
504
- ...payload,
505
- [this.pk]: id,
506
- }), ctx);
312
+ }
313
+ if (method === "GET" && normalizedPath === "statement/:method/*args") {
314
+ return FromModelController.createRegistration("statement", handler, FromModelController.statementDecorators(ModelConstr, modelClazzName), [
315
+ { decorator: Param("method"), index: 0 },
316
+ { decorator: Param("args"), index: 1 },
317
+ { decorator: DecafQuery(), index: 2 },
318
+ ]);
319
+ }
320
+ if (method === "GET" && normalizedPath === "findOneBy/:key/:value") {
321
+ return FromModelController.createRegistration("findOneBy", handler, FromModelController.statementShortcutDecorators(ModelConstr, modelClazzName, normalizedPath, PreparedStatementKeys.FIND_ONE_BY), FromModelController.statementShortcutParams(normalizedPath));
322
+ }
323
+ const statementRoutes = {
324
+ "listBy/:key": PreparedStatementKeys.LIST_BY,
325
+ "paginateBy/:key/:page": PreparedStatementKeys.PAGE_BY,
326
+ "find/:value": PreparedStatementKeys.FIND,
327
+ "page/:value": PreparedStatementKeys.PAGE,
328
+ "findOneBy/:key/:value": PreparedStatementKeys.FIND_ONE_BY,
329
+ "findBy/:key/:value": PreparedStatementKeys.FIND_BY,
330
+ "countOf/:field": PreparedStatementKeys.COUNT_OF,
331
+ "maxOf/:field": PreparedStatementKeys.MAX_OF,
332
+ "minOf/:field": PreparedStatementKeys.MIN_OF,
333
+ "avgOf/:field": PreparedStatementKeys.AVG_OF,
334
+ "sumOf/:field": PreparedStatementKeys.SUM_OF,
335
+ "distinctOf/:field": PreparedStatementKeys.DISTINCT_OF,
336
+ "groupOf/:field": PreparedStatementKeys.GROUP_OF,
337
+ };
338
+ const statementKey = statementRoutes[normalizedPath];
339
+ if (statementKey && method === "GET") {
340
+ return FromModelController.createRegistration(FromModelController.statementMethodName(normalizedPath), handler, FromModelController.statementShortcutDecorators(ModelConstr, modelClazzName, normalizedPath, statementKey), FromModelController.statementShortcutParams(normalizedPath));
341
+ }
342
+ if (method === "GET" && normalizedPath.startsWith("query/")) {
343
+ const queryMethod = normalizedPath.replace(/^query\//, "").split("/")[0];
344
+ return FromModelController.createRegistration(queryMethod, handler, FromModelController.getQueryDecorators(queryMethod, normalizedPath, "GET", true), FromModelController.complexQueryParams(normalizedPath));
345
+ }
346
+ const pathSegments = normalizedPath.split("/").filter(Boolean);
347
+ const knownPrefixes = new Set([
348
+ "listBy",
349
+ "findBy",
350
+ "findByPaginate",
351
+ "findOneBy",
352
+ "paginateBy",
353
+ "find",
354
+ "page",
355
+ "countOf",
356
+ "maxOf",
357
+ "minOf",
358
+ "avgOf",
359
+ "sumOf",
360
+ "distinctOf",
361
+ "groupOf",
362
+ "statement",
363
+ "bulk",
364
+ "query",
365
+ ]);
366
+ if (pathSegments.length > 0 &&
367
+ !normalizedPath.startsWith("query/") &&
368
+ !normalizedPath.startsWith("statement/") &&
369
+ !knownPrefixes.has(pathSegments[0])) {
370
+ const apiPathParams = pathSegments
371
+ .filter((s) => s.startsWith(":"))
372
+ .map((s) => s.slice(1))
373
+ .map((name) => ({
374
+ name,
375
+ description: `${name} parameter for the query`,
376
+ required: true,
377
+ type: String,
378
+ }));
379
+ return FromModelController.createRegistration(methodName, handler, [
380
+ ...apiPathParams.map((p) => ApiParam(p)),
381
+ ApiOperation({ summary: `Retrieve records using "${methodName}".` }),
382
+ ApiOkResponse({ description: "Result successfully retrieved." }),
383
+ ApiNoContentResponse({
384
+ description: "No content returned by the method.",
385
+ }),
386
+ ], FromModelController.routeParamDecorators(normalizedPath));
387
+ }
388
+ return undefined;
389
+ }
390
+ static createRegistration(methodName, handler, decorators, paramDecorators) {
391
+ return { methodName, handler, decorators, paramDecorators };
392
+ }
393
+ static statementMethodName(path) {
394
+ const firstSegment = path.split("/")[0];
395
+ return firstSegment;
396
+ }
397
+ static statementShortcutParams(path) {
398
+ const segments = path.split("/").filter((s) => s.startsWith(":"));
399
+ const params = [];
400
+ segments.forEach((seg, i) => {
401
+ const name = seg.replace(":", "");
402
+ params.push({ decorator: Param(name), index: i });
403
+ });
404
+ if (path.startsWith("listBy/") ||
405
+ path.startsWith("paginateBy/") ||
406
+ path.startsWith("find/") ||
407
+ path.startsWith("page/")) {
408
+ params.push({ decorator: DecafQuery(), index: segments.length });
409
+ }
410
+ return params;
411
+ }
412
+ static complexQueryParams(path) {
413
+ const segments = path.split("/").filter((s) => s.startsWith(":"));
414
+ const params = [];
415
+ segments.forEach((seg, i) => {
416
+ const name = seg.replace(":", "");
417
+ params.push({ decorator: Param(name), index: i });
418
+ });
419
+ if (path.startsWith("query/")) {
420
+ params.push({ decorator: DecafQuery(), index: segments.length });
421
+ }
422
+ return params;
423
+ }
424
+ static createComplexQueryHandler(methodName) {
425
+ return async function complexQuery(...args) {
426
+ const log = this.log?.for?.(complexQuery);
427
+ try {
428
+ if (log)
429
+ log.debug(`Invoking custom query "${methodName}"`);
430
+ const { ctx } = (await this.logCtx([], methodName, true)).for(complexQuery);
431
+ const persistence = this.persistence(ctx);
432
+ const spreadArgs = FromModelController.normalizeQueryArgs(args);
433
+ if (persistence?.repo &&
434
+ typeof persistence.repo[methodName] === "function") {
435
+ return persistence.repo[methodName](...spreadArgs);
507
436
  }
508
- catch (e) {
509
- log.error(e);
510
- throw e;
437
+ if (typeof persistence[methodName] === "function") {
438
+ return persistence[methodName](...spreadArgs);
511
439
  }
512
- ctx.toResponse(resp);
513
- return updated;
514
- }
515
- async deleteAll(ids, resp) {
516
- const { ctx, log } = (await this.logCtx([], BulkCrudOperationKeys.DELETE_ALL, true)).for(this.deleteAll);
517
- const normalizedIds = Array.isArray(ids) ? ids : [ids];
518
- let read;
519
- try {
520
- log.debug(`deleting ${normalizedIds.length} ${modelClazzName}: ${normalizedIds}`);
521
- read = await this.persistence(ctx).deleteAll(normalizedIds, ctx);
440
+ if (persistence?.query && typeof persistence.query === "function") {
441
+ return persistence.query(methodName, ...spreadArgs);
522
442
  }
523
- catch (e) {
524
- log.error(`Failed to delete ${modelClazzName} with id ${normalizedIds}`, e);
525
- throw e;
443
+ if (typeof persistence.statement === "function") {
444
+ return persistence.statement(methodName, ...spreadArgs, ctx);
526
445
  }
527
- log.info(`deleted ${read.length} ${modelClazzName}`);
528
- ctx.toResponse(resp);
529
- return read;
446
+ throw new InternalError(`Persistence method "${methodName}" not found on ${persistence?.constructor?.name}`);
530
447
  }
531
- async delete(routeParams, resp) {
532
- const { ctx, log } = (await this.logCtx([], OperationKeys.DELETE, true)).for(this.delete);
533
- const id = getPK(...routeParams.valuesInOrder);
534
- if (typeof id === "undefined")
535
- throw new ValidationError(`No ${this.pk} provided`);
536
- let del;
537
- try {
538
- log.debug(`deleting ${modelClazzName} with ${this.pk} ${id}`);
539
- del = await this.persistence(ctx).delete(id, ctx);
540
- }
541
- catch (e) {
542
- log.error(`Failed to delete ${modelClazzName} with id ${id}`, e);
543
- throw e;
544
- }
545
- log.info(`deleted ${modelClazzName} with id ${id}`);
546
- ctx.toResponse(resp);
547
- return del;
448
+ catch (e) {
449
+ if (log)
450
+ log.error(`Custom query "${methodName}" failed`, e);
451
+ throw FromModelController.toDecafError(e, `Custom query "${methodName}" failed`);
548
452
  }
549
453
  };
550
- __decorate([
551
- ApiOperationFromModel(ModelConstr, "GET", "listBy/:key"),
552
- ApiOperation({ summary: `Retrieve ${modelClazzName} records by query.` }),
553
- ApiParam({ name: "key", description: "the model key to sort by" }),
554
- ApiQuery({ name: "direction", required: true, enum: OrderDirection }),
555
- ApiOkResponse({
556
- description: `${modelClazzName} listed successfully.`,
557
- }),
558
- __param(0, Param("key")),
559
- __param(1, DecafQuery()),
560
- __metadata("design:type", Function),
561
- __metadata("design:paramtypes", [String, Object]),
562
- __metadata("design:returntype", Promise)
563
- ], DynamicModelController.prototype, "listBy", null);
564
- __decorate([
565
- ApiOperationFromModel(ModelConstr, "GET", "paginateBy/:key/:page"),
566
- ApiOperation({ summary: `Retrieve ${modelClazzName} records by query.` }),
567
- ApiParam({ name: "key", description: "the model key to sort by" }),
568
- ApiParam({
569
- name: "page",
570
- description: "the page to retrieve or the bookmark",
571
- }),
572
- ApiQuery({
573
- name: "direction",
574
- required: true,
575
- enum: OrderDirection,
576
- description: "the sort order",
577
- }),
578
- ApiQuery({ name: "limit", required: true, description: "the page size" }),
579
- ApiQuery({ name: "offset", description: "the bookmark when necessary" }),
580
- ApiOkResponse({
581
- description: `${modelClazzName} listed paginated.`,
582
- }),
583
- __param(0, Param("key")),
584
- __param(1, DecafQuery()),
585
- __metadata("design:type", Function),
586
- __metadata("design:paramtypes", [String, Object]),
587
- __metadata("design:returntype", Promise)
588
- ], DynamicModelController.prototype, "paginateBy", null);
589
- __decorate([
590
- ApiOperationFromModel(ModelConstr, "GET", "find/:value"),
591
- ApiOperation({
592
- summary: `Find ${modelClazzName} records using the default query attributes.`,
593
- }),
594
- ApiParam({
595
- name: "value",
596
- description: "The string to match against the default query attributes",
597
- }),
598
- ApiQuery({
599
- name: "direction",
600
- required: false,
601
- enum: OrderDirection,
602
- description: "the sort order for the matching results",
603
- }),
604
- ApiOkResponse({
605
- description: `${modelClazzName} records matching the provided prefix.`,
606
- }),
607
- __param(0, Param("value")),
608
- __param(1, DecafQuery()),
609
- __metadata("design:type", Function),
610
- __metadata("design:paramtypes", [String, Object]),
611
- __metadata("design:returntype", Promise)
612
- ], DynamicModelController.prototype, "find", null);
613
- __decorate([
614
- ApiOperationFromModel(ModelConstr, "GET", "page/:value"),
615
- ApiOperation({
616
- summary: `Page ${modelClazzName} records using the default query attributes.`,
617
- }),
618
- ApiParam({
619
- name: "value",
620
- description: "The string to match against the default query attributes",
621
- }),
622
- ApiQuery({
623
- name: "direction",
624
- required: false,
625
- enum: OrderDirection,
626
- description: "the sort order for the paged results",
627
- }),
628
- ApiQuery({
629
- name: "limit",
630
- required: false,
631
- description: "page size",
632
- }),
633
- ApiQuery({
634
- name: "offset",
635
- required: false,
636
- description: "page number",
637
- }),
638
- ApiQuery({
639
- name: "bookmark",
640
- required: false,
641
- description: "bookmark for cursor pagination",
642
- }),
643
- ApiOkResponse({
644
- description: `${modelClazzName} records paged by the provided prefix.`,
645
- }),
646
- __param(0, Param("value")),
647
- __param(1, DecafQuery()),
648
- __metadata("design:type", Function),
649
- __metadata("design:paramtypes", [String, Object]),
650
- __metadata("design:returntype", Promise)
651
- ], DynamicModelController.prototype, "page", null);
652
- __decorate([
653
- ApiOperationFromModel(ModelConstr, "GET", "findOneBy/:key"),
654
- ApiOperation({ summary: `Retrieve ${modelClazzName} records by query.` }),
655
- ApiParam({ name: "key", description: "the model key to sort by" }),
656
- ApiOkResponse({
657
- description: `${modelClazzName} listed found.`,
658
- }),
659
- ApiNotFoundResponse({
660
- description: `No ${modelClazzName} record matches the provided identifier.`,
661
- }),
662
- __param(0, Param("key")),
663
- __param(1, Param("value")),
664
- __metadata("design:type", Function),
665
- __metadata("design:paramtypes", [String, Object]),
666
- __metadata("design:returntype", Promise)
667
- ], DynamicModelController.prototype, "findOneBy", null);
668
- __decorate([
669
- ApiOperationFromModel(ModelConstr, "GET", "findBy/:key/:value"),
670
- ApiOperation({ summary: `Retrieve ${modelClazzName} records by query.` }),
671
- ApiParam({ name: "key", description: "the model key to compare" }),
672
- ApiParam({ name: "value", description: "the value to match" }),
673
- ApiQuery({
674
- name: "direction",
675
- required: true,
676
- enum: OrderDirection,
677
- description: "the sort order when applicable",
678
- }),
679
- ApiOkResponse({
680
- description: `${modelClazzName} listed found.`,
681
- }),
682
- ApiNotFoundResponse({
683
- description: `No ${modelClazzName} record matches the provided identifier.`,
684
- }),
685
- __param(0, Param("key")),
686
- __param(1, Param("value")),
687
- __param(2, DecafQuery()),
688
- __metadata("design:type", Function),
689
- __metadata("design:paramtypes", [String, Object, Object]),
690
- __metadata("design:returntype", Promise)
691
- ], DynamicModelController.prototype, "findBy", null);
692
- __decorate([
693
- ApiOperationFromModel(ModelConstr, "GET", "statement/:method/*args"),
694
- ApiOperation({
695
- summary: `Executes a prepared statement on ${modelClazzName}.`,
696
- }),
697
- ApiParam({
698
- name: "method",
699
- description: "the prepared statement to execute",
700
- }),
701
- ApiParam({
702
- name: "args",
703
- description: "concatenated list of arguments the prepared statement can accept",
704
- }),
705
- ApiQuery({
706
- name: "direction",
707
- required: true,
708
- enum: OrderDirection,
709
- description: "the sort order when applicable",
710
- }),
711
- ApiQuery({
712
- name: "limit",
713
- required: true,
714
- description: "limit or page size when applicable",
715
- }),
716
- ApiQuery({
717
- name: "offset",
718
- required: true,
719
- description: "offset or bookmark when applicable",
720
- }),
721
- ApiOkResponse({
722
- description: `${modelClazzName} listed found.`,
723
- }),
724
- ApiNotFoundResponse({
725
- description: `No ${modelClazzName} record matches the provided identifier.`,
726
- }),
727
- __param(0, Param("method")),
728
- __param(1, Param("args")),
729
- __param(2, DecafQuery()),
730
- __metadata("design:type", Function),
731
- __metadata("design:paramtypes", [String, Array, Object]),
732
- __metadata("design:returntype", Promise)
733
- ], DynamicModelController.prototype, "statement", null);
734
- __decorate([
735
- ApiOperationFromModel(ModelConstr, "POST", "bulk"),
454
+ }
455
+ static extractQueryArgs(args) {
456
+ if (args.length === 0)
457
+ return args;
458
+ const last = args[args.length - 1];
459
+ if (last && typeof last === "object" && !Array.isArray(last)) {
460
+ const queryObj = args.pop();
461
+ const hasDirection = queryObj.direction !== undefined;
462
+ const hasLimit = queryObj.limit !== undefined;
463
+ const hasOffset = queryObj.offset !== undefined;
464
+ if (!hasDirection && !hasLimit && !hasOffset)
465
+ return args;
466
+ const extras = [];
467
+ if (hasDirection)
468
+ extras.push(queryObj.direction);
469
+ else if (hasLimit || hasOffset)
470
+ extras.push(undefined);
471
+ if (hasLimit)
472
+ extras.push(queryObj.limit);
473
+ if (hasOffset)
474
+ extras.push(queryObj.offset);
475
+ return [...args, ...extras];
476
+ }
477
+ return args;
478
+ }
479
+ static normalizeQueryArgs(args) {
480
+ const normalized = FromModelController.extractQueryArgs(args);
481
+ if (normalized.length === 0)
482
+ return normalized;
483
+ const last = normalized[normalized.length - 1];
484
+ if (!last || typeof last !== "object" || Array.isArray(last)) {
485
+ return normalized;
486
+ }
487
+ const queryObj = last;
488
+ const hasQueryFields = queryObj.direction !== undefined ||
489
+ queryObj.limit !== undefined ||
490
+ queryObj.offset !== undefined ||
491
+ queryObj.bookmark !== undefined;
492
+ if (!hasQueryFields) {
493
+ normalized.pop();
494
+ return normalized;
495
+ }
496
+ normalized.pop();
497
+ if (queryObj.direction !== undefined)
498
+ normalized.push(queryObj.direction);
499
+ if (queryObj.limit !== undefined)
500
+ normalized.push(queryObj.limit);
501
+ if (queryObj.offset !== undefined)
502
+ normalized.push(queryObj.offset);
503
+ if (queryObj.bookmark !== undefined)
504
+ normalized.push(queryObj.bookmark);
505
+ return normalized;
506
+ }
507
+ static createCreateDecorators(ModelConstr, modelClazzName) {
508
+ return [
509
+ ApiOperationFromModel(ModelConstr, "POST"),
736
510
  ApiOperation({ summary: `Create a new ${modelClazzName}.` }),
737
511
  ApiBody({
738
512
  description: `Payload for ${modelClazzName}`,
739
- schema: {
740
- type: "array",
741
- items: {
742
- $ref: getSchemaPath(ModelConstr),
743
- // $ref: getSchemaPath(DtoFor(OperationKeys.CREATE, ModelConstr)),
744
- },
745
- },
513
+ type: DtoFor(OperationKeys.CREATE, ModelConstr),
746
514
  }),
747
515
  ApiCreatedResponse({
748
516
  description: `${modelClazzName} created successfully.`,
749
- schema: {
750
- type: "array",
751
- items: {
752
- $ref: getSchemaPath(ModelConstr),
753
- },
754
- },
517
+ schema: { $ref: getSchemaPath(ModelConstr) },
755
518
  }),
756
519
  ApiBadRequestResponse({ description: "Payload validation failed." }),
757
520
  ApiUnprocessableEntityResponse({
758
521
  description: "Repository rejected the provided payload.",
759
522
  }),
760
- __param(0, DecafBody()),
761
- __param(1, Response({ passthrough: true })),
762
- __metadata("design:type", Function),
763
- __metadata("design:paramtypes", [Array, Object]),
764
- __metadata("design:returntype", Promise)
765
- ], DynamicModelController.prototype, "createAll", null);
766
- __decorate([
767
- ApiOperationFromModel(ModelConstr, "POST"),
523
+ ];
524
+ }
525
+ static bulkCreateDecorators(ModelConstr, modelClazzName) {
526
+ return [
527
+ ApiOperationFromModel(ModelConstr, "POST", "bulk"),
768
528
  ApiOperation({ summary: `Create a new ${modelClazzName}.` }),
769
529
  ApiBody({
770
530
  description: `Payload for ${modelClazzName}`,
771
- type: DtoFor(OperationKeys.CREATE, ModelConstr),
531
+ schema: {
532
+ type: "array",
533
+ items: { $ref: getSchemaPath(ModelConstr) },
534
+ },
772
535
  }),
773
536
  ApiCreatedResponse({
774
537
  description: `${modelClazzName} created successfully.`,
775
538
  schema: {
776
- $ref: getSchemaPath(ModelConstr),
539
+ type: "array",
540
+ items: { $ref: getSchemaPath(ModelConstr) },
777
541
  },
778
542
  }),
779
543
  ApiBadRequestResponse({ description: "Payload validation failed." }),
780
544
  ApiUnprocessableEntityResponse({
781
545
  description: "Repository rejected the provided payload.",
782
546
  }),
783
- __param(0, DecafBody()),
784
- __param(1, Response({ passthrough: true })),
785
- __metadata("design:type", Function),
786
- __metadata("design:paramtypes", [Object, Object]),
787
- __metadata("design:returntype", Promise)
788
- ], DynamicModelController.prototype, "create", null);
789
- __decorate([
547
+ ];
548
+ }
549
+ static bulkReadDecorators(ModelConstr, modelClazzName) {
550
+ return [
790
551
  ApiOperationFromModel(ModelConstr, "GET", "bulk"),
791
- ApiOperation({ summary: `Retrieve a ${modelClazzName} record by id.` }),
552
+ ApiOperation({ summary: `Retrieve ${modelClazzName} records by ids.` }),
792
553
  ApiQuery({ name: "ids", required: true, type: "array" }),
793
554
  ApiOkResponse({
794
555
  description: `${modelClazzName} retrieved successfully.`,
795
556
  schema: {
796
557
  type: "array",
797
- items: {
798
- $ref: getSchemaPath(ModelConstr),
799
- },
800
- },
801
- }),
802
- ApiNotFoundResponse({
803
- description: `No ${modelClazzName} record matches the provided identifier.`,
804
- }),
805
- __param(0, Query("ids")),
806
- __metadata("design:type", Function),
807
- __metadata("design:paramtypes", [Array]),
808
- __metadata("design:returntype", Promise)
809
- ], DynamicModelController.prototype, "readAll", null);
810
- __decorate([
811
- ApiOperationFromModel(ModelConstr, "GET", path),
812
- ApiParamsFromModel(apiProperties),
813
- ApiOperation({ summary: `Retrieve a ${modelClazzName} record by id.` }),
814
- ApiOkResponse({
815
- description: `${modelClazzName} retrieved successfully.`,
816
- schema: {
817
- $ref: getSchemaPath(ModelConstr),
558
+ items: { $ref: getSchemaPath(ModelConstr) },
818
559
  },
819
560
  }),
820
561
  ApiNotFoundResponse({
821
562
  description: `No ${modelClazzName} record matches the provided identifier.`,
822
563
  }),
823
- __param(0, DecafParams(apiProperties)),
824
- __metadata("design:type", Function),
825
- __metadata("design:paramtypes", [Object]),
826
- __metadata("design:returntype", Promise)
827
- ], DynamicModelController.prototype, "read", null);
828
- __decorate([
829
- ApiOperationFromModel(ModelConstr, "PUT", `bulk`),
564
+ ];
565
+ }
566
+ static bulkUpdateDecorators(ModelConstr, modelClazzName, apiProperties) {
567
+ return [
568
+ ApiOperationFromModel(ModelConstr, "PUT", "bulk"),
830
569
  ApiParamsFromModel(apiProperties),
831
570
  ApiOperation({
832
- summary: `Replace an existing ${modelClazzName} record with a new payload.`,
571
+ summary: `Replace existing ${modelClazzName} records with new payloads.`,
833
572
  }),
834
573
  ApiBody({
835
- description: `Payload for replace a existing record of ${modelClazzName}`,
574
+ description: `Payload for replacing existing records of ${modelClazzName}`,
836
575
  schema: {
837
576
  type: "array",
838
577
  $ref: getSchemaPath(DtoFor(OperationKeys.UPDATE, ModelConstr)),
@@ -842,123 +581,210 @@ export class FromModelController {
842
581
  description: `${modelClazzName} updated successfully.`,
843
582
  schema: {
844
583
  type: "array",
845
- items: {
846
- $ref: getSchemaPath(ModelConstr),
847
- },
584
+ items: { $ref: getSchemaPath(ModelConstr) },
848
585
  },
849
586
  }),
850
587
  ApiNotFoundResponse({
851
588
  description: `No ${modelClazzName} record matches the provided identifier.`,
852
589
  }),
853
590
  ApiBadRequestResponse({ description: "Payload validation failed." }),
854
- __param(0, DecafBody()),
855
- __param(1, Response({ passthrough: true })),
856
- __metadata("design:type", Function),
857
- __metadata("design:paramtypes", [Array, Object]),
858
- __metadata("design:returntype", Promise)
859
- ], DynamicModelController.prototype, "updateAll", null);
860
- __decorate([
861
- ApiOperationFromModel(ModelConstr, "PUT", path),
591
+ ];
592
+ }
593
+ static bulkDeleteDecorators(ModelConstr, modelClazzName, apiProperties) {
594
+ return [
595
+ ApiOperationFromModel(ModelConstr, "DELETE", "bulk"),
596
+ ApiParamsFromModel(apiProperties),
597
+ ApiOperation({ summary: `Delete ${modelClazzName} records by ids.` }),
598
+ ApiQuery({ name: "ids", required: true, type: "array" }),
599
+ ApiOkResponse({
600
+ description: `${modelClazzName} deleted successfully.`,
601
+ schema: {
602
+ type: "array",
603
+ items: { $ref: getSchemaPath(ModelConstr) },
604
+ },
605
+ }),
606
+ ApiNotFoundResponse({
607
+ description: `No ${modelClazzName} record matches the provided identifier.`,
608
+ }),
609
+ ];
610
+ }
611
+ static readDecorators(ModelConstr, modelClazzName, apiProperties, pkPath) {
612
+ return [
613
+ ApiOperationFromModel(ModelConstr, "GET", pkPath),
614
+ ApiParamsFromModel(apiProperties),
615
+ ApiOperation({ summary: `Retrieve a ${modelClazzName} record by id.` }),
616
+ ApiOkResponse({
617
+ description: `${modelClazzName} retrieved successfully.`,
618
+ schema: { $ref: getSchemaPath(ModelConstr) },
619
+ }),
620
+ ApiNotFoundResponse({
621
+ description: `No ${modelClazzName} record matches the provided identifier.`,
622
+ }),
623
+ ];
624
+ }
625
+ static updateDecorators(ModelConstr, modelClazzName, apiProperties, pkPath) {
626
+ return [
627
+ ApiOperationFromModel(ModelConstr, "PUT", pkPath),
862
628
  ApiParamsFromModel(apiProperties),
863
629
  ApiOperation({
864
630
  summary: `Replace an existing ${modelClazzName} record with a new payload.`,
865
631
  }),
866
632
  ApiBody({
867
- description: `Payload for replace a existing record of ${modelClazzName}`,
633
+ description: `Payload for replacing an existing record of ${modelClazzName}`,
868
634
  type: DtoFor(OperationKeys.UPDATE, ModelConstr),
869
635
  }),
870
636
  ApiOkResponse({
871
637
  description: `${modelClazzName} updated successfully.`,
872
- schema: {
873
- $ref: getSchemaPath(ModelConstr),
874
- },
638
+ schema: { $ref: getSchemaPath(ModelConstr) },
875
639
  }),
876
640
  ApiNotFoundResponse({
877
641
  description: `No ${modelClazzName} record matches the provided identifier.`,
878
642
  }),
879
643
  ApiBadRequestResponse({ description: "Payload validation failed." }),
880
- __param(0, DecafParams(apiProperties)),
881
- __param(1, DecafBody()),
882
- __param(2, Response({ passthrough: true })),
883
- __metadata("design:type", Function),
884
- __metadata("design:paramtypes", [Object, Object, Object]),
885
- __metadata("design:returntype", Promise)
886
- ], DynamicModelController.prototype, "update", null);
887
- __decorate([
888
- ApiOperationFromModel(ModelConstr, "DELETE", "bulk"),
644
+ ];
645
+ }
646
+ static deleteDecorators(ModelConstr, modelClazzName, apiProperties, pkPath) {
647
+ return [
648
+ ApiOperationFromModel(ModelConstr, "DELETE", pkPath),
889
649
  ApiParamsFromModel(apiProperties),
890
- ApiOperation({ summary: `Retrieve a ${modelClazzName} record by id.` }),
891
- ApiQuery({ name: "ids", required: true, type: "array" }),
650
+ ApiOperation({ summary: `Delete a ${modelClazzName} record by id.` }),
892
651
  ApiOkResponse({
893
652
  description: `${modelClazzName} deleted successfully.`,
894
- schema: {
895
- type: "array",
896
- items: {
897
- $ref: getSchemaPath(ModelConstr),
898
- },
899
- },
653
+ schema: { $ref: getSchemaPath(ModelConstr) },
900
654
  }),
901
655
  ApiNotFoundResponse({
902
656
  description: `No ${modelClazzName} record matches the provided identifier.`,
903
657
  }),
904
- __param(0, Query("ids")),
905
- __param(1, Response({ passthrough: true })),
906
- __metadata("design:type", Function),
907
- __metadata("design:paramtypes", [Array, Object]),
908
- __metadata("design:returntype", Promise)
909
- ], DynamicModelController.prototype, "deleteAll", null);
910
- __decorate([
911
- ApiOperationFromModel(ModelConstr, "DELETE", path),
912
- ApiParamsFromModel(apiProperties),
913
- ApiOperation({ summary: `Delete a ${modelClazzName} record by id.` }),
914
- ApiOkResponse({
915
- description: `${modelClazzName} deleted successfully.`,
916
- schema: {
917
- $ref: getSchemaPath(ModelConstr),
918
- },
658
+ ];
659
+ }
660
+ static statementDecorators(ModelConstr, modelClazzName) {
661
+ return [
662
+ ApiOperationFromModel(ModelConstr, "GET", "statement/:method/*args"),
663
+ ApiOperation({
664
+ summary: `Executes a prepared statement on ${modelClazzName}.`,
665
+ }),
666
+ ApiParam({
667
+ name: "method",
668
+ description: "the prepared statement to execute",
669
+ }),
670
+ ApiParam({
671
+ name: "args",
672
+ description: "concatenated list of arguments the prepared statement can accept",
673
+ }),
674
+ ApiQuery({
675
+ name: "direction",
676
+ required: true,
677
+ enum: OrderDirection,
678
+ description: "the sort order when applicable",
919
679
  }),
680
+ ApiQuery({
681
+ name: "limit",
682
+ required: true,
683
+ description: "limit or page size when applicable",
684
+ }),
685
+ ApiQuery({
686
+ name: "offset",
687
+ required: true,
688
+ description: "offset or bookmark when applicable",
689
+ }),
690
+ ApiOkResponse({ description: `${modelClazzName} listed found.` }),
920
691
  ApiNotFoundResponse({
921
692
  description: `No ${modelClazzName} record matches the provided identifier.`,
922
693
  }),
923
- __param(0, DecafParams(apiProperties)),
924
- __param(1, Response({ passthrough: true })),
925
- __metadata("design:type", Function),
926
- __metadata("design:paramtypes", [Object, Object]),
927
- __metadata("design:returntype", Promise)
928
- ], DynamicModelController.prototype, "delete", null);
929
- DynamicModelController = __decorate([
930
- Controller(routePath),
931
- ApiTags(modelClazzName),
932
- ApiExtraModels(ModelConstr),
933
- Auth(ModelConstr),
934
- __metadata("design:paramtypes", [DecafRequestContext])
935
- ], DynamicModelController);
936
- return DynamicModelController;
694
+ ];
937
695
  }
938
- static getRouteParametersFromModel(ModelClazz) {
939
- const pk = Model.pk(ModelClazz);
940
- const composed = Metadata.get(ModelClazz, Metadata.key(DBKeys.COMPOSED, pk));
941
- const composedKeys = composed?.args ?? [];
942
- // remove duplicates while preserving order
943
- const uniqueKeys = Array.isArray(composedKeys) && composedKeys.length > 0
944
- ? Array.from(new Set([...composedKeys]))
945
- : Array.from(new Set([pk]));
946
- const description = Metadata.description(ModelClazz);
947
- const path = `:${uniqueKeys.join("/:")}`;
948
- const apiProperties = uniqueKeys.map((key) => {
949
- return {
950
- name: key,
951
- description: Metadata.description(ModelClazz, key),
952
- required: true,
953
- type: String,
954
- };
696
+ static statementShortcutDecorators(ModelConstr, modelClazzName, path, statementKey) {
697
+ const base = [
698
+ ApiOperationFromModel(ModelConstr, "GET", path),
699
+ ApiOperation({ summary: `Retrieve ${modelClazzName} records.` }),
700
+ ApiOkResponse({
701
+ description: `${modelClazzName} retrieved successfully.`,
702
+ }),
703
+ ];
704
+ const segments = path.split("/").filter((s) => s.startsWith(":"));
705
+ segments.forEach((seg) => {
706
+ const name = seg.replace(":", "");
707
+ base.push(ApiParam({ name, description: `The ${name} parameter` }));
955
708
  });
956
- return {
957
- path,
958
- description,
959
- apiProperties,
960
- getPK: (...params) => composed?.separator ? params.join(composed.separator) : params.join(""),
961
- };
709
+ if (path.startsWith("listBy/") ||
710
+ path.startsWith("paginateBy/") ||
711
+ path.startsWith("find/") ||
712
+ path.startsWith("page/")) {
713
+ base.push(ApiQuery({
714
+ name: "direction",
715
+ required: true,
716
+ enum: OrderDirection,
717
+ description: "the sort order",
718
+ }));
719
+ }
720
+ if (path.startsWith("paginateBy/") || path.startsWith("page/")) {
721
+ base.push(ApiQuery({ name: "limit", required: false, description: "page size" }), ApiQuery({
722
+ name: "offset",
723
+ required: false,
724
+ description: "page number",
725
+ }), ApiQuery({
726
+ name: "bookmark",
727
+ required: false,
728
+ description: "bookmark for cursor pagination",
729
+ }));
730
+ }
731
+ if (path.startsWith("findOneBy/") || path.startsWith("findBy/")) {
732
+ base.push(ApiNotFoundResponse({
733
+ description: `No ${modelClazzName} record matches the provided identifier.`,
734
+ }));
735
+ }
736
+ if (statementKey === PreparedStatementKeys.COUNT_OF ||
737
+ statementKey === PreparedStatementKeys.AVG_OF ||
738
+ statementKey === PreparedStatementKeys.SUM_OF) {
739
+ base.push(ApiOkResponse({
740
+ description: `Result for ${modelClazzName}.`,
741
+ type: Number,
742
+ }));
743
+ }
744
+ if (statementKey === PreparedStatementKeys.DISTINCT_OF) {
745
+ base.push(ApiOkResponse({
746
+ description: `Distinct values for ${modelClazzName}.`,
747
+ type: [String],
748
+ }));
749
+ }
750
+ return base;
751
+ }
752
+ static getQueryDecorators(methodName, routePath, httpVerb, includeQueryParams = false) {
753
+ const extractPathParams = (p) => p
754
+ .split("/")
755
+ .filter((s) => s.startsWith(":"))
756
+ .map((s) => s.slice(1));
757
+ const apiPathParams = extractPathParams(routePath).map((name) => ({
758
+ name,
759
+ description: `${name} parameter for the query`,
760
+ required: true,
761
+ type: String,
762
+ }));
763
+ const decorators = [
764
+ ...apiPathParams.map((p) => ApiParam(p)),
765
+ ApiOperation({ summary: `Retrieve records using "${methodName}".` }),
766
+ ApiOkResponse({ description: "Result successfully retrieved." }),
767
+ ApiNoContentResponse({
768
+ description: "No content returned by the method.",
769
+ }),
770
+ ];
771
+ if (httpVerb === "GET" && includeQueryParams) {
772
+ decorators.push(ApiQuery({
773
+ name: "direction",
774
+ required: false,
775
+ enum: OrderDirection,
776
+ description: "the sort order when applicable",
777
+ }), ApiQuery({
778
+ name: "limit",
779
+ required: false,
780
+ description: "limit or page size",
781
+ }), ApiQuery({
782
+ name: "offset",
783
+ required: false,
784
+ description: "offset or bookmark",
785
+ }));
786
+ }
787
+ return decorators;
962
788
  }
963
789
  }
964
790
  //# sourceMappingURL=FromModelController.js.map