@venizia/ignis 0.0.7-8 → 0.0.7

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 (509) hide show
  1. package/README.md +2757 -28
  2. package/dist/base/applications/abstract.d.ts.map +1 -1
  3. package/dist/base/applications/abstract.js +0 -8
  4. package/dist/base/applications/abstract.js.map +1 -1
  5. package/dist/base/applications/base.d.ts.map +1 -1
  6. package/dist/base/applications/base.js +0 -17
  7. package/dist/base/applications/base.js.map +1 -1
  8. package/dist/base/applications/types.d.ts.map +1 -1
  9. package/dist/base/applications/types.js.map +1 -1
  10. package/dist/base/components/base.d.ts.map +1 -1
  11. package/dist/base/components/base.js +0 -2
  12. package/dist/base/components/base.js.map +1 -1
  13. package/dist/base/controllers/abstract.d.ts +10 -124
  14. package/dist/base/controllers/abstract.d.ts.map +1 -1
  15. package/dist/base/controllers/abstract.js +6 -82
  16. package/dist/base/controllers/abstract.js.map +1 -1
  17. package/dist/base/controllers/base.d.ts +5 -113
  18. package/dist/base/controllers/base.d.ts.map +1 -1
  19. package/dist/base/controllers/base.js +5 -113
  20. package/dist/base/controllers/base.js.map +1 -1
  21. package/dist/base/controllers/common/constants.d.ts +1 -16
  22. package/dist/base/controllers/common/constants.d.ts.map +1 -1
  23. package/dist/base/controllers/common/constants.js +1 -20
  24. package/dist/base/controllers/common/constants.js.map +1 -1
  25. package/dist/base/controllers/common/types.d.ts +16 -167
  26. package/dist/base/controllers/common/types.d.ts.map +1 -1
  27. package/dist/base/controllers/common/types.js +1 -4
  28. package/dist/base/controllers/common/types.js.map +1 -1
  29. package/dist/base/controllers/factory/controller.d.ts +26 -207
  30. package/dist/base/controllers/factory/controller.d.ts.map +1 -1
  31. package/dist/base/controllers/factory/controller.js +13 -140
  32. package/dist/base/controllers/factory/controller.js.map +1 -1
  33. package/dist/base/controllers/factory/definition.d.ts +11 -16
  34. package/dist/base/controllers/factory/definition.d.ts.map +1 -1
  35. package/dist/base/controllers/factory/definition.js +4 -30
  36. package/dist/base/controllers/factory/definition.js.map +1 -1
  37. package/dist/base/datasources/base.d.ts +2 -31
  38. package/dist/base/datasources/base.d.ts.map +1 -1
  39. package/dist/base/datasources/base.js +2 -39
  40. package/dist/base/datasources/base.js.map +1 -1
  41. package/dist/base/datasources/common/types.d.ts +2 -4
  42. package/dist/base/datasources/common/types.d.ts.map +1 -1
  43. package/dist/base/datasources/common/types.js +1 -8
  44. package/dist/base/datasources/common/types.js.map +1 -1
  45. package/dist/base/metadata/injectors.d.ts +1 -30
  46. package/dist/base/metadata/injectors.d.ts.map +1 -1
  47. package/dist/base/metadata/injectors.js +1 -30
  48. package/dist/base/metadata/injectors.js.map +1 -1
  49. package/dist/base/metadata/persistents.d.ts +3 -63
  50. package/dist/base/metadata/persistents.d.ts.map +1 -1
  51. package/dist/base/metadata/persistents.js +11 -88
  52. package/dist/base/metadata/persistents.js.map +1 -1
  53. package/dist/base/metadata/routes.d.ts +1 -24
  54. package/dist/base/metadata/routes.d.ts.map +1 -1
  55. package/dist/base/metadata/routes.js +1 -27
  56. package/dist/base/metadata/routes.js.map +1 -1
  57. package/dist/base/middlewares/app-error.middleware.d.ts +1 -10
  58. package/dist/base/middlewares/app-error.middleware.d.ts.map +1 -1
  59. package/dist/base/middlewares/app-error.middleware.js +2 -14
  60. package/dist/base/middlewares/app-error.middleware.js.map +1 -1
  61. package/dist/base/middlewares/emoji-favicon.middleware.d.ts +1 -7
  62. package/dist/base/middlewares/emoji-favicon.middleware.d.ts.map +1 -1
  63. package/dist/base/middlewares/emoji-favicon.middleware.js +1 -7
  64. package/dist/base/middlewares/emoji-favicon.middleware.js.map +1 -1
  65. package/dist/base/middlewares/not-found.middleware.d.ts +1 -8
  66. package/dist/base/middlewares/not-found.middleware.d.ts.map +1 -1
  67. package/dist/base/middlewares/not-found.middleware.js +1 -8
  68. package/dist/base/middlewares/not-found.middleware.js.map +1 -1
  69. package/dist/base/middlewares/request-spy.middleware.d.ts +3 -19
  70. package/dist/base/middlewares/request-spy.middleware.d.ts.map +1 -1
  71. package/dist/base/middlewares/request-spy.middleware.js +3 -23
  72. package/dist/base/middlewares/request-spy.middleware.js.map +1 -1
  73. package/dist/base/mixins/component.mixin.d.ts.map +1 -1
  74. package/dist/base/mixins/controller.mixin.d.ts.map +1 -1
  75. package/dist/base/mixins/repository.mixin.d.ts.map +1 -1
  76. package/dist/base/mixins/service.mixin.d.ts.map +1 -1
  77. package/dist/base/models/base.d.ts +4 -21
  78. package/dist/base/models/base.d.ts.map +1 -1
  79. package/dist/base/models/base.js +1 -11
  80. package/dist/base/models/base.js.map +1 -1
  81. package/dist/base/models/common/types.d.ts.map +1 -1
  82. package/dist/base/models/common/types.js +0 -2
  83. package/dist/base/models/common/types.js.map +1 -1
  84. package/dist/base/models/enrichers/id.enricher.d.ts.map +1 -1
  85. package/dist/base/models/enrichers/id.enricher.js.map +1 -1
  86. package/dist/base/models/enrichers/user-audit.enricher.d.ts.map +1 -1
  87. package/dist/base/models/enrichers/user-audit.enricher.js +1 -6
  88. package/dist/base/models/enrichers/user-audit.enricher.js.map +1 -1
  89. package/dist/base/repositories/common/constants.d.ts +5 -59
  90. package/dist/base/repositories/common/constants.d.ts.map +1 -1
  91. package/dist/base/repositories/common/constants.js +3 -51
  92. package/dist/base/repositories/common/constants.js.map +1 -1
  93. package/dist/base/repositories/common/shared.d.ts +1 -13
  94. package/dist/base/repositories/common/shared.d.ts.map +1 -1
  95. package/dist/base/repositories/common/shared.js +2 -19
  96. package/dist/base/repositories/common/shared.js.map +1 -1
  97. package/dist/base/repositories/common/types.d.ts +32 -461
  98. package/dist/base/repositories/common/types.d.ts.map +1 -1
  99. package/dist/base/repositories/common/types.js +13 -159
  100. package/dist/base/repositories/common/types.js.map +1 -1
  101. package/dist/base/repositories/core/abstract.d.ts +10 -204
  102. package/dist/base/repositories/core/abstract.d.ts.map +1 -1
  103. package/dist/base/repositories/core/abstract.js +6 -126
  104. package/dist/base/repositories/core/abstract.js.map +1 -1
  105. package/dist/base/repositories/core/default-crud.d.ts +1 -36
  106. package/dist/base/repositories/core/default-crud.d.ts.map +1 -1
  107. package/dist/base/repositories/core/default-crud.js +1 -36
  108. package/dist/base/repositories/core/default-crud.js.map +1 -1
  109. package/dist/base/repositories/core/index.d.ts +1 -0
  110. package/dist/base/repositories/core/index.d.ts.map +1 -1
  111. package/dist/base/repositories/core/index.js +1 -0
  112. package/dist/base/repositories/core/index.js.map +1 -1
  113. package/dist/base/repositories/core/persistable.d.ts +2 -71
  114. package/dist/base/repositories/core/persistable.d.ts.map +1 -1
  115. package/dist/base/repositories/core/persistable.js +2 -100
  116. package/dist/base/repositories/core/persistable.js.map +1 -1
  117. package/dist/base/repositories/core/readable.d.ts +12 -127
  118. package/dist/base/repositories/core/readable.d.ts.map +1 -1
  119. package/dist/base/repositories/core/readable.js +8 -124
  120. package/dist/base/repositories/core/readable.js.map +1 -1
  121. package/dist/base/repositories/core/soft-deletable.d.ts +140 -0
  122. package/dist/base/repositories/core/soft-deletable.d.ts.map +1 -0
  123. package/dist/base/repositories/core/soft-deletable.js +99 -0
  124. package/dist/base/repositories/core/soft-deletable.js.map +1 -0
  125. package/dist/base/repositories/mixins/default-filter.d.ts +3 -49
  126. package/dist/base/repositories/mixins/default-filter.d.ts.map +1 -1
  127. package/dist/base/repositories/mixins/default-filter.js +3 -57
  128. package/dist/base/repositories/mixins/default-filter.js.map +1 -1
  129. package/dist/base/repositories/mixins/fields-visibility.d.ts +3 -59
  130. package/dist/base/repositories/mixins/fields-visibility.d.ts.map +1 -1
  131. package/dist/base/repositories/mixins/fields-visibility.js +3 -67
  132. package/dist/base/repositories/mixins/fields-visibility.js.map +1 -1
  133. package/dist/base/repositories/operators/filter.d.ts +10 -115
  134. package/dist/base/repositories/operators/filter.d.ts.map +1 -1
  135. package/dist/base/repositories/operators/filter.js +13 -154
  136. package/dist/base/repositories/operators/filter.js.map +1 -1
  137. package/dist/base/repositories/operators/json-utils.d.ts +5 -38
  138. package/dist/base/repositories/operators/json-utils.d.ts.map +1 -1
  139. package/dist/base/repositories/operators/json-utils.js +5 -47
  140. package/dist/base/repositories/operators/json-utils.js.map +1 -1
  141. package/dist/base/repositories/operators/query.d.ts +3 -56
  142. package/dist/base/repositories/operators/query.d.ts.map +1 -1
  143. package/dist/base/repositories/operators/query.js +11 -106
  144. package/dist/base/repositories/operators/query.js.map +1 -1
  145. package/dist/base/repositories/operators/relation.d.ts +1 -33
  146. package/dist/base/repositories/operators/relation.d.ts.map +1 -1
  147. package/dist/base/repositories/operators/relation.js +1 -36
  148. package/dist/base/repositories/operators/relation.js.map +1 -1
  149. package/dist/base/repositories/operators/update.d.ts +7 -72
  150. package/dist/base/repositories/operators/update.d.ts.map +1 -1
  151. package/dist/base/repositories/operators/update.js +6 -91
  152. package/dist/base/repositories/operators/update.js.map +1 -1
  153. package/dist/base/services/types.d.ts.map +1 -1
  154. package/dist/common/bindings.d.ts +2 -17
  155. package/dist/common/bindings.d.ts.map +1 -1
  156. package/dist/common/bindings.js +2 -14
  157. package/dist/common/bindings.js.map +1 -1
  158. package/dist/common/constants.d.ts +1 -3
  159. package/dist/common/constants.d.ts.map +1 -1
  160. package/dist/common/constants.js +1 -4
  161. package/dist/common/constants.js.map +1 -1
  162. package/dist/common/environments.d.ts +1 -4
  163. package/dist/common/environments.d.ts.map +1 -1
  164. package/dist/common/environments.js +1 -4
  165. package/dist/common/environments.js.map +1 -1
  166. package/dist/common/statuses.d.ts +6 -19
  167. package/dist/common/statuses.d.ts.map +1 -1
  168. package/dist/common/statuses.js +6 -27
  169. package/dist/common/statuses.js.map +1 -1
  170. package/dist/components/auth/authenticate/common/codecs.d.ts +11 -0
  171. package/dist/components/auth/authenticate/common/codecs.d.ts.map +1 -0
  172. package/dist/components/auth/authenticate/common/codecs.js +28 -0
  173. package/dist/components/auth/authenticate/common/codecs.js.map +1 -0
  174. package/dist/components/auth/authenticate/common/constants.d.ts +28 -4
  175. package/dist/components/auth/authenticate/common/constants.d.ts.map +1 -1
  176. package/dist/components/auth/authenticate/common/constants.js +37 -9
  177. package/dist/components/auth/authenticate/common/constants.js.map +1 -1
  178. package/dist/components/auth/authenticate/common/index.d.ts +1 -0
  179. package/dist/components/auth/authenticate/common/index.d.ts.map +1 -1
  180. package/dist/components/auth/authenticate/common/index.js +1 -0
  181. package/dist/components/auth/authenticate/common/index.js.map +1 -1
  182. package/dist/components/auth/authenticate/common/keys.d.ts +1 -0
  183. package/dist/components/auth/authenticate/common/keys.d.ts.map +1 -1
  184. package/dist/components/auth/authenticate/common/keys.js +1 -0
  185. package/dist/components/auth/authenticate/common/keys.js.map +1 -1
  186. package/dist/components/auth/authenticate/common/types.d.ts +56 -34
  187. package/dist/components/auth/authenticate/common/types.d.ts.map +1 -1
  188. package/dist/components/auth/authenticate/component.d.ts +3 -16
  189. package/dist/components/auth/authenticate/component.d.ts.map +1 -1
  190. package/dist/components/auth/authenticate/component.js +105 -73
  191. package/dist/components/auth/authenticate/component.js.map +1 -1
  192. package/dist/components/auth/authenticate/controllers/factory.d.ts.map +1 -1
  193. package/dist/components/auth/authenticate/controllers/factory.js +2 -2
  194. package/dist/components/auth/authenticate/controllers/factory.js.map +1 -1
  195. package/dist/components/auth/authenticate/controllers/index.d.ts +1 -0
  196. package/dist/components/auth/authenticate/controllers/index.d.ts.map +1 -1
  197. package/dist/components/auth/authenticate/controllers/index.js +1 -0
  198. package/dist/components/auth/authenticate/controllers/index.js.map +1 -1
  199. package/dist/components/auth/authenticate/controllers/jwks/controller.d.ts +9 -0
  200. package/dist/components/auth/authenticate/controllers/jwks/controller.d.ts.map +1 -0
  201. package/dist/components/auth/authenticate/controllers/jwks/controller.js +54 -0
  202. package/dist/components/auth/authenticate/controllers/jwks/controller.js.map +1 -0
  203. package/dist/components/auth/authenticate/controllers/jwks/definitions.d.ts +66 -0
  204. package/dist/components/auth/authenticate/controllers/jwks/definitions.d.ts.map +1 -0
  205. package/dist/components/auth/authenticate/controllers/jwks/definitions.js +32 -0
  206. package/dist/components/auth/authenticate/controllers/jwks/definitions.js.map +1 -0
  207. package/dist/components/auth/authenticate/controllers/jwks/index.d.ts +2 -0
  208. package/dist/components/auth/authenticate/controllers/jwks/index.d.ts.map +1 -0
  209. package/dist/components/auth/authenticate/controllers/jwks/index.js +18 -0
  210. package/dist/components/auth/authenticate/controllers/jwks/index.js.map +1 -0
  211. package/dist/components/auth/authenticate/middlewares/authenticate.middleware.d.ts.map +1 -1
  212. package/dist/components/auth/authenticate/middlewares/authenticate.middleware.js +0 -2
  213. package/dist/components/auth/authenticate/middlewares/authenticate.middleware.js.map +1 -1
  214. package/dist/components/auth/authenticate/providers/authentication.provider.d.ts.map +1 -1
  215. package/dist/components/auth/authenticate/providers/authentication.provider.js +0 -7
  216. package/dist/components/auth/authenticate/providers/authentication.provider.js.map +1 -1
  217. package/dist/components/auth/authenticate/services/basic/index.d.ts +2 -0
  218. package/dist/components/auth/authenticate/services/basic/index.d.ts.map +1 -0
  219. package/dist/components/auth/authenticate/services/basic/index.js +18 -0
  220. package/dist/components/auth/authenticate/services/basic/index.js.map +1 -0
  221. package/dist/components/auth/authenticate/services/basic/service.d.ts +24 -0
  222. package/dist/components/auth/authenticate/services/basic/service.d.ts.map +1 -0
  223. package/dist/components/auth/authenticate/services/{basic-token.service.js → basic/service.js} +7 -45
  224. package/dist/components/auth/authenticate/services/basic/service.js.map +1 -0
  225. package/dist/components/auth/authenticate/services/bearer/abstract.service.d.ts +52 -0
  226. package/dist/components/auth/authenticate/services/bearer/abstract.service.d.ts.map +1 -0
  227. package/dist/components/auth/authenticate/services/bearer/abstract.service.js +177 -0
  228. package/dist/components/auth/authenticate/services/bearer/abstract.service.js.map +1 -0
  229. package/dist/components/auth/authenticate/services/bearer/index.d.ts +4 -0
  230. package/dist/components/auth/authenticate/services/bearer/index.d.ts.map +1 -0
  231. package/dist/components/auth/authenticate/services/bearer/index.js +20 -0
  232. package/dist/components/auth/authenticate/services/bearer/index.js.map +1 -0
  233. package/dist/components/auth/authenticate/services/bearer/jwks/abstract.service.d.ts +16 -0
  234. package/dist/components/auth/authenticate/services/bearer/jwks/abstract.service.d.ts.map +1 -0
  235. package/dist/components/auth/authenticate/services/bearer/jwks/abstract.service.js +32 -0
  236. package/dist/components/auth/authenticate/services/bearer/jwks/abstract.service.js.map +1 -0
  237. package/dist/components/auth/authenticate/services/bearer/jwks/index.d.ts +4 -0
  238. package/dist/components/auth/authenticate/services/bearer/jwks/index.d.ts.map +1 -0
  239. package/dist/components/auth/authenticate/services/bearer/jwks/index.js +20 -0
  240. package/dist/components/auth/authenticate/services/bearer/jwks/index.js.map +1 -0
  241. package/dist/components/auth/authenticate/services/bearer/jwks/issuer.service.d.ts +46 -0
  242. package/dist/components/auth/authenticate/services/bearer/jwks/issuer.service.d.ts.map +1 -0
  243. package/dist/components/auth/authenticate/services/bearer/jwks/issuer.service.js +168 -0
  244. package/dist/components/auth/authenticate/services/bearer/jwks/issuer.service.js.map +1 -0
  245. package/dist/components/auth/authenticate/services/bearer/jwks/verifier.service.d.ts +18 -0
  246. package/dist/components/auth/authenticate/services/bearer/jwks/verifier.service.d.ts.map +1 -0
  247. package/dist/components/auth/authenticate/services/bearer/jwks/verifier.service.js +73 -0
  248. package/dist/components/auth/authenticate/services/bearer/jwks/verifier.service.js.map +1 -0
  249. package/dist/components/auth/authenticate/services/bearer/jws.service.d.ts +19 -0
  250. package/dist/components/auth/authenticate/services/bearer/jws.service.d.ts.map +1 -0
  251. package/dist/components/auth/authenticate/services/bearer/jws.service.js +76 -0
  252. package/dist/components/auth/authenticate/services/bearer/jws.service.js.map +1 -0
  253. package/dist/components/auth/authenticate/services/index.d.ts +2 -2
  254. package/dist/components/auth/authenticate/services/index.d.ts.map +1 -1
  255. package/dist/components/auth/authenticate/services/index.js +2 -2
  256. package/dist/components/auth/authenticate/services/index.js.map +1 -1
  257. package/dist/components/auth/authenticate/strategies/basic.strategy.d.ts +1 -22
  258. package/dist/components/auth/authenticate/strategies/basic.strategy.d.ts.map +1 -1
  259. package/dist/components/auth/authenticate/strategies/basic.strategy.js +9 -23
  260. package/dist/components/auth/authenticate/strategies/basic.strategy.js.map +1 -1
  261. package/dist/components/auth/authenticate/strategies/index.d.ts +2 -1
  262. package/dist/components/auth/authenticate/strategies/index.d.ts.map +1 -1
  263. package/dist/components/auth/authenticate/strategies/index.js +2 -1
  264. package/dist/components/auth/authenticate/strategies/index.js.map +1 -1
  265. package/dist/components/auth/authenticate/strategies/jwks.strategy.d.ts +21 -0
  266. package/dist/components/auth/authenticate/strategies/jwks.strategy.d.ts.map +1 -0
  267. package/dist/components/auth/authenticate/strategies/jwks.strategy.js +68 -0
  268. package/dist/components/auth/authenticate/strategies/jwks.strategy.js.map +1 -0
  269. package/dist/components/auth/authenticate/strategies/{jwt.strategy.d.ts → jws.strategy.d.ts} +5 -4
  270. package/dist/components/auth/authenticate/strategies/jws.strategy.d.ts.map +1 -0
  271. package/dist/components/auth/authenticate/strategies/{jwt.strategy.js → jws.strategy.js} +18 -10
  272. package/dist/components/auth/authenticate/strategies/jws.strategy.js.map +1 -0
  273. package/dist/components/auth/authenticate/strategies/strategy-registry.d.ts.map +1 -1
  274. package/dist/components/auth/authenticate/strategies/strategy-registry.js +0 -5
  275. package/dist/components/auth/authenticate/strategies/strategy-registry.js.map +1 -1
  276. package/dist/components/auth/authorize/adapters/base-filtered.d.ts +73 -0
  277. package/dist/components/auth/authorize/adapters/base-filtered.d.ts.map +1 -0
  278. package/dist/components/auth/authorize/adapters/base-filtered.js +90 -0
  279. package/dist/components/auth/authorize/adapters/base-filtered.js.map +1 -0
  280. package/dist/components/auth/authorize/adapters/drizzle-casbin.d.ts +40 -0
  281. package/dist/components/auth/authorize/adapters/drizzle-casbin.d.ts.map +1 -0
  282. package/dist/components/auth/authorize/adapters/drizzle-casbin.js +99 -0
  283. package/dist/components/auth/authorize/adapters/drizzle-casbin.js.map +1 -0
  284. package/dist/components/auth/authorize/adapters/index.d.ts +3 -0
  285. package/dist/components/auth/authorize/adapters/index.d.ts.map +1 -0
  286. package/dist/components/auth/authorize/adapters/index.js +19 -0
  287. package/dist/components/auth/authorize/adapters/index.js.map +1 -0
  288. package/dist/components/auth/authorize/common/constants.d.ts +37 -4
  289. package/dist/components/auth/authorize/common/constants.d.ts.map +1 -1
  290. package/dist/components/auth/authorize/common/constants.js +61 -9
  291. package/dist/components/auth/authorize/common/constants.js.map +1 -1
  292. package/dist/components/auth/authorize/common/keys.d.ts +1 -2
  293. package/dist/components/auth/authorize/common/keys.d.ts.map +1 -1
  294. package/dist/components/auth/authorize/common/keys.js +3 -2
  295. package/dist/components/auth/authorize/common/keys.js.map +1 -1
  296. package/dist/components/auth/authorize/common/types.d.ts +78 -98
  297. package/dist/components/auth/authorize/common/types.d.ts.map +1 -1
  298. package/dist/components/auth/authorize/component.d.ts +1 -0
  299. package/dist/components/auth/authorize/component.d.ts.map +1 -1
  300. package/dist/components/auth/authorize/component.js +12 -34
  301. package/dist/components/auth/authorize/component.js.map +1 -1
  302. package/dist/components/auth/authorize/enforcers/casbin.enforcer.d.ts +45 -11
  303. package/dist/components/auth/authorize/enforcers/casbin.enforcer.d.ts.map +1 -1
  304. package/dist/components/auth/authorize/enforcers/casbin.enforcer.js +198 -39
  305. package/dist/components/auth/authorize/enforcers/casbin.enforcer.js.map +1 -1
  306. package/dist/components/auth/authorize/enforcers/enforcer-registry.d.ts +12 -6
  307. package/dist/components/auth/authorize/enforcers/enforcer-registry.d.ts.map +1 -1
  308. package/dist/components/auth/authorize/enforcers/enforcer-registry.js +30 -14
  309. package/dist/components/auth/authorize/enforcers/enforcer-registry.js.map +1 -1
  310. package/dist/components/auth/authorize/enforcers/index.d.ts +0 -1
  311. package/dist/components/auth/authorize/enforcers/index.d.ts.map +1 -1
  312. package/dist/components/auth/authorize/enforcers/index.js +0 -1
  313. package/dist/components/auth/authorize/enforcers/index.js.map +1 -1
  314. package/dist/components/auth/authorize/index.d.ts +1 -0
  315. package/dist/components/auth/authorize/index.d.ts.map +1 -1
  316. package/dist/components/auth/authorize/index.js +1 -0
  317. package/dist/components/auth/authorize/index.js.map +1 -1
  318. package/dist/components/auth/authorize/middlewares/authorize.middleware.d.ts.map +1 -1
  319. package/dist/components/auth/authorize/middlewares/authorize.middleware.js +0 -2
  320. package/dist/components/auth/authorize/middlewares/authorize.middleware.js.map +1 -1
  321. package/dist/components/auth/authorize/models/abilities/index.d.ts +3 -0
  322. package/dist/components/auth/authorize/models/abilities/index.d.ts.map +1 -0
  323. package/dist/components/auth/authorize/models/abilities/index.js +19 -0
  324. package/dist/components/auth/authorize/models/abilities/index.js.map +1 -0
  325. package/dist/components/auth/authorize/models/abilities/string-action.model.d.ts +14 -0
  326. package/dist/components/auth/authorize/models/abilities/string-action.model.d.ts.map +1 -0
  327. package/dist/components/auth/authorize/models/abilities/string-action.model.js +23 -0
  328. package/dist/components/auth/authorize/models/abilities/string-action.model.js.map +1 -0
  329. package/dist/components/auth/authorize/models/abilities/string-resource.model.d.ts +13 -0
  330. package/dist/components/auth/authorize/models/abilities/string-resource.model.d.ts.map +1 -0
  331. package/dist/components/auth/authorize/models/abilities/string-resource.model.js +19 -0
  332. package/dist/components/auth/authorize/models/abilities/string-resource.model.js.map +1 -0
  333. package/dist/components/auth/authorize/models/authorization-role.model.d.ts.map +1 -1
  334. package/dist/components/auth/authorize/models/authorization-role.model.js +0 -1
  335. package/dist/components/auth/authorize/models/authorization-role.model.js.map +1 -1
  336. package/dist/components/auth/authorize/models/index.d.ts +1 -0
  337. package/dist/components/auth/authorize/models/index.d.ts.map +1 -1
  338. package/dist/components/auth/authorize/models/index.js +1 -0
  339. package/dist/components/auth/authorize/models/index.js.map +1 -1
  340. package/dist/components/auth/authorize/providers/authorization.provider.d.ts.map +1 -1
  341. package/dist/components/auth/authorize/providers/authorization.provider.js +48 -43
  342. package/dist/components/auth/authorize/providers/authorization.provider.js.map +1 -1
  343. package/dist/components/auth/base/abstract-auth-registry.d.ts +1 -0
  344. package/dist/components/auth/base/abstract-auth-registry.d.ts.map +1 -1
  345. package/dist/components/auth/base/abstract-auth-registry.js +3 -3
  346. package/dist/components/auth/base/abstract-auth-registry.js.map +1 -1
  347. package/dist/components/auth/context-variables.d.ts +14 -0
  348. package/dist/components/auth/context-variables.d.ts.map +1 -0
  349. package/dist/components/auth/context-variables.js +3 -0
  350. package/dist/components/auth/context-variables.js.map +1 -0
  351. package/dist/components/auth/index.d.ts +1 -0
  352. package/dist/components/auth/index.d.ts.map +1 -1
  353. package/dist/components/auth/index.js +1 -0
  354. package/dist/components/auth/index.js.map +1 -1
  355. package/dist/components/auth/models/entities/index.d.ts +1 -2
  356. package/dist/components/auth/models/entities/index.d.ts.map +1 -1
  357. package/dist/components/auth/models/entities/index.js +1 -2
  358. package/dist/components/auth/models/entities/index.js.map +1 -1
  359. package/dist/components/auth/models/entities/permission.model.d.ts +0 -1
  360. package/dist/components/auth/models/entities/permission.model.d.ts.map +1 -1
  361. package/dist/components/auth/models/entities/permission.model.js +0 -2
  362. package/dist/components/auth/models/entities/permission.model.js.map +1 -1
  363. package/dist/components/auth/models/entities/policy-definition.model.d.ts +24 -0
  364. package/dist/components/auth/models/entities/policy-definition.model.d.ts.map +1 -0
  365. package/dist/components/auth/models/entities/policy-definition.model.js +39 -0
  366. package/dist/components/auth/models/entities/policy-definition.model.js.map +1 -0
  367. package/dist/components/auth/models/entities/role.model.d.ts +3 -1
  368. package/dist/components/auth/models/entities/role.model.d.ts.map +1 -1
  369. package/dist/components/auth/models/entities/role.model.js +4 -2
  370. package/dist/components/auth/models/entities/role.model.js.map +1 -1
  371. package/dist/components/auth/models/entities/user.model.d.ts +4 -2
  372. package/dist/components/auth/models/entities/user.model.d.ts.map +1 -1
  373. package/dist/components/auth/models/entities/user.model.js +5 -4
  374. package/dist/components/auth/models/entities/user.model.js.map +1 -1
  375. package/dist/components/health-check/controller.d.ts.map +1 -1
  376. package/dist/components/health-check/controller.js +0 -1
  377. package/dist/components/health-check/controller.js.map +1 -1
  378. package/dist/components/mail/common/types.d.ts +1 -1
  379. package/dist/components/mail/common/types.d.ts.map +1 -1
  380. package/dist/components/mail/component.d.ts.map +1 -1
  381. package/dist/components/mail/component.js +2 -7
  382. package/dist/components/mail/component.js.map +1 -1
  383. package/dist/components/mail/helpers/executors/bull-mq-executor.helper.d.ts +2 -1
  384. package/dist/components/mail/helpers/executors/bull-mq-executor.helper.d.ts.map +1 -1
  385. package/dist/components/mail/helpers/executors/bull-mq-executor.helper.js +7 -7
  386. package/dist/components/mail/helpers/executors/bull-mq-executor.helper.js.map +1 -1
  387. package/dist/components/mail/helpers/executors/direct-executor.helper.d.ts +1 -1
  388. package/dist/components/mail/helpers/executors/direct-executor.helper.d.ts.map +1 -1
  389. package/dist/components/mail/helpers/executors/direct-executor.helper.js +3 -3
  390. package/dist/components/mail/helpers/executors/direct-executor.helper.js.map +1 -1
  391. package/dist/components/mail/helpers/executors/internal-queue-executor.helper.d.ts +1 -1
  392. package/dist/components/mail/helpers/executors/internal-queue-executor.helper.d.ts.map +1 -1
  393. package/dist/components/mail/helpers/executors/internal-queue-executor.helper.js +4 -4
  394. package/dist/components/mail/helpers/executors/internal-queue-executor.helper.js.map +1 -1
  395. package/dist/components/mail/helpers/transporters/mailgun-transporter.helper.d.ts +1 -1
  396. package/dist/components/mail/helpers/transporters/mailgun-transporter.helper.d.ts.map +1 -1
  397. package/dist/components/mail/helpers/transporters/mailgun-transporter.helper.js +3 -3
  398. package/dist/components/mail/helpers/transporters/mailgun-transporter.helper.js.map +1 -1
  399. package/dist/components/mail/helpers/transporters/nodemail-transporter.helper.d.ts +1 -1
  400. package/dist/components/mail/helpers/transporters/nodemail-transporter.helper.d.ts.map +1 -1
  401. package/dist/components/mail/helpers/transporters/nodemail-transporter.helper.js +3 -3
  402. package/dist/components/mail/helpers/transporters/nodemail-transporter.helper.js.map +1 -1
  403. package/dist/components/mail/providers/mail-queue-executor.provider.d.ts.map +1 -1
  404. package/dist/components/mail/providers/mail-queue-executor.provider.js +8 -8
  405. package/dist/components/mail/providers/mail-queue-executor.provider.js.map +1 -1
  406. package/dist/components/mail/providers/mail-transporter.provider.d.ts.map +1 -1
  407. package/dist/components/mail/providers/mail-transporter.provider.js +6 -6
  408. package/dist/components/mail/providers/mail-transporter.provider.js.map +1 -1
  409. package/dist/components/mail/services/mail.service.d.ts +1 -1
  410. package/dist/components/mail/services/mail.service.d.ts.map +1 -1
  411. package/dist/components/mail/services/mail.service.js +9 -9
  412. package/dist/components/mail/services/mail.service.js.map +1 -1
  413. package/dist/components/mail/services/template.service.d.ts +1 -1
  414. package/dist/components/mail/services/template.service.d.ts.map +1 -1
  415. package/dist/components/mail/services/template.service.js +4 -4
  416. package/dist/components/mail/services/template.service.js.map +1 -1
  417. package/dist/components/mail/utilities/type.utility.d.ts +1 -1
  418. package/dist/components/mail/utilities/type.utility.d.ts.map +1 -1
  419. package/dist/components/socket-io/component.d.ts.map +1 -1
  420. package/dist/components/socket-io/component.js +0 -4
  421. package/dist/components/socket-io/component.js.map +1 -1
  422. package/dist/components/static-asset/common/constants.d.ts +1 -0
  423. package/dist/components/static-asset/common/constants.d.ts.map +1 -1
  424. package/dist/components/static-asset/common/constants.js +2 -1
  425. package/dist/components/static-asset/common/constants.js.map +1 -1
  426. package/dist/components/static-asset/common/types.d.ts +43 -1
  427. package/dist/components/static-asset/common/types.d.ts.map +1 -1
  428. package/dist/components/static-asset/controller/base.definition.d.ts +81 -80
  429. package/dist/components/static-asset/controller/base.definition.d.ts.map +1 -1
  430. package/dist/components/static-asset/controller/base.definition.js +1 -2
  431. package/dist/components/static-asset/controller/base.definition.js.map +1 -1
  432. package/dist/components/static-asset/controller/factory.d.ts +2 -6
  433. package/dist/components/static-asset/controller/factory.d.ts.map +1 -1
  434. package/dist/components/static-asset/controller/factory.js +41 -42
  435. package/dist/components/static-asset/controller/factory.js.map +1 -1
  436. package/dist/components/static-asset/models/base.model.d.ts +49 -19
  437. package/dist/components/static-asset/models/base.model.d.ts.map +1 -1
  438. package/dist/components/static-asset/models/base.model.js +3 -7
  439. package/dist/components/static-asset/models/base.model.js.map +1 -1
  440. package/dist/components/swagger/ui-factory.d.ts.map +1 -1
  441. package/dist/components/swagger/ui-factory.js +0 -2
  442. package/dist/components/swagger/ui-factory.js.map +1 -1
  443. package/dist/components/websocket/component.d.ts.map +1 -1
  444. package/dist/components/websocket/component.js +0 -3
  445. package/dist/components/websocket/component.js.map +1 -1
  446. package/dist/helpers/base-helper.d.ts +2 -0
  447. package/dist/helpers/base-helper.d.ts.map +1 -0
  448. package/dist/helpers/base-helper.js +6 -0
  449. package/dist/helpers/base-helper.js.map +1 -0
  450. package/dist/helpers/index.d.ts +2 -1
  451. package/dist/helpers/index.d.ts.map +1 -1
  452. package/dist/helpers/index.js +1 -1
  453. package/dist/helpers/index.js.map +1 -1
  454. package/dist/helpers/inversion/common/keys.d.ts.map +1 -1
  455. package/dist/helpers/inversion/common/keys.js +0 -2
  456. package/dist/helpers/inversion/common/keys.js.map +1 -1
  457. package/dist/helpers/inversion/common/types.d.ts +18 -56
  458. package/dist/helpers/inversion/common/types.d.ts.map +1 -1
  459. package/dist/helpers/inversion/container.d.ts.map +1 -1
  460. package/dist/helpers/inversion/container.js +0 -1
  461. package/dist/helpers/inversion/container.js.map +1 -1
  462. package/dist/helpers/inversion/index.d.ts +1 -1
  463. package/dist/helpers/inversion/index.d.ts.map +1 -1
  464. package/dist/helpers/inversion/index.js +5 -1
  465. package/dist/helpers/inversion/index.js.map +1 -1
  466. package/dist/helpers/inversion/mixins/controller.mixin.d.ts.map +1 -1
  467. package/dist/helpers/inversion/mixins/controller.mixin.js +0 -3
  468. package/dist/helpers/inversion/mixins/controller.mixin.js.map +1 -1
  469. package/dist/helpers/inversion/mixins/datasource.mixin.d.ts.map +1 -1
  470. package/dist/helpers/inversion/mixins/datasource.mixin.js +0 -3
  471. package/dist/helpers/inversion/mixins/datasource.mixin.js.map +1 -1
  472. package/dist/helpers/inversion/mixins/model.mixin.d.ts +29 -1
  473. package/dist/helpers/inversion/mixins/model.mixin.d.ts.map +1 -1
  474. package/dist/helpers/inversion/mixins/model.mixin.js +66 -2
  475. package/dist/helpers/inversion/mixins/model.mixin.js.map +1 -1
  476. package/dist/helpers/inversion/mixins/repository.mixin.d.ts.map +1 -1
  477. package/dist/helpers/inversion/mixins/repository.mixin.js +0 -2
  478. package/dist/helpers/inversion/mixins/repository.mixin.js.map +1 -1
  479. package/dist/helpers/inversion/registry.d.ts +22 -0
  480. package/dist/helpers/inversion/registry.d.ts.map +1 -1
  481. package/dist/utilities/jsx.utility.d.ts +2 -16
  482. package/dist/utilities/jsx.utility.d.ts.map +1 -1
  483. package/dist/utilities/jsx.utility.js +2 -16
  484. package/dist/utilities/jsx.utility.js.map +1 -1
  485. package/dist/utilities/schema.utility.d.ts.map +1 -1
  486. package/dist/utilities/schema.utility.js +0 -2
  487. package/dist/utilities/schema.utility.js.map +1 -1
  488. package/package.json +97 -71
  489. package/dist/components/auth/authenticate/services/basic-token.service.d.ts +0 -60
  490. package/dist/components/auth/authenticate/services/basic-token.service.d.ts.map +0 -1
  491. package/dist/components/auth/authenticate/services/basic-token.service.js.map +0 -1
  492. package/dist/components/auth/authenticate/services/jwt-token.service.d.ts +0 -34
  493. package/dist/components/auth/authenticate/services/jwt-token.service.d.ts.map +0 -1
  494. package/dist/components/auth/authenticate/services/jwt-token.service.js +0 -218
  495. package/dist/components/auth/authenticate/services/jwt-token.service.js.map +0 -1
  496. package/dist/components/auth/authenticate/strategies/jwt.strategy.d.ts.map +0 -1
  497. package/dist/components/auth/authenticate/strategies/jwt.strategy.js.map +0 -1
  498. package/dist/components/auth/authorize/enforcers/default.enforcer.d.ts +0 -37
  499. package/dist/components/auth/authorize/enforcers/default.enforcer.d.ts.map +0 -1
  500. package/dist/components/auth/authorize/enforcers/default.enforcer.js +0 -125
  501. package/dist/components/auth/authorize/enforcers/default.enforcer.js.map +0 -1
  502. package/dist/components/auth/models/entities/permission-mapping.model.d.ts +0 -26
  503. package/dist/components/auth/models/entities/permission-mapping.model.d.ts.map +0 -1
  504. package/dist/components/auth/models/entities/permission-mapping.model.js +0 -33
  505. package/dist/components/auth/models/entities/permission-mapping.model.js.map +0 -1
  506. package/dist/components/auth/models/entities/user-role.model.d.ts +0 -17
  507. package/dist/components/auth/models/entities/user-role.model.d.ts.map +0 -1
  508. package/dist/components/auth/models/entities/user-role.model.js +0 -34
  509. package/dist/components/auth/models/entities/user-role.model.js.map +0 -1
package/README.md CHANGED
@@ -1,65 +1,2794 @@
1
- # @venizia/ignis
1
+ <div align="center">
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@venizia/ignis.svg)](https://www.npmjs.com/package/@venizia/ignis)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ # :fire: IGNIS - @venizia/ignis
5
4
 
6
- The core package of the **Ignis Framework** - a TypeScript Server Infrastructure combining enterprise-grade patterns with high performance, built on [Hono](https://hono.dev/).
5
+ **High-performance TypeScript server infrastructure combining enterprise-grade architecture with Hono speed.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/@venizia/ignis.svg?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@venizia/ignis)
8
+ [![License](https://img.shields.io/badge/License-MIT-3DA639.svg?style=flat-square)](https://opensource.org/licenses/MIT)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6.svg?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
10
+ [![Hono](https://img.shields.io/badge/Hono-4.x-E36002.svg?style=flat-square&logo=hono&logoColor=white)](https://hono.dev/)
11
+ [![Drizzle ORM](https://img.shields.io/badge/Drizzle_ORM-0.45-C5F74F.svg?style=flat-square)](https://orm.drizzle.team/)
12
+
13
+ Ignis brings together the structured, enterprise development experience of **LoopBack 4** with the blazing speed and simplicity of **Hono**, giving you the best of both worlds: decorator-based DI, repository pattern, DataSource abstraction, component system, boot conventions — running on Hono's ~140k req/s engine with Drizzle ORM's type-safe SQL.
14
+
15
+ [Installation](#installation) &#8226; [Quick Start](#quick-start) &#8226; [API Reference](#controllers) &#8226; [Documentation](https://venizia-ai.github.io/ignis)
16
+
17
+ </div>
18
+
19
+ ## Highlights
20
+
21
+ | | Feature | |
22
+ | :---: | :--- | :--- |
23
+ | **1** | **Zero-Config CRUD** | 2-line repository gives you full create/read/update/delete |
24
+ | **2** | **Type-Safe SQL** | End-to-end TypeScript inference with Drizzle ORM |
25
+ | **3** | **Auto OpenAPI** | Every route produces Swagger documentation automatically |
26
+ | **4** | **~140k req/s** | Hono-powered HTTP with zero wrapper overhead |
27
+ | **5** | **9 Built-in Components** | Auth, Health, Swagger, Mail, Socket.IO, Static Assets, and more |
28
+ | **6** | **3 Route Patterns** | Decorator, imperative, or fluent -- your choice |
29
+
30
+ ---
31
+
32
+ ## At a Glance
33
+
34
+ ```typescript
35
+ import {
36
+ BaseApplication, // Your app extends this
37
+ BaseController, // Controllers extend this
38
+ DefaultCRUDRepository, // Repositories extend this
39
+ BaseEntity, // Models extend this
40
+ BaseDataSource, // DataSources extend this
41
+ } from '@venizia/ignis';
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Table of Contents
47
+
48
+ - [Installation](#installation)
49
+ - [Quick Start](#quick-start)
50
+ - [Application Lifecycle](#application-lifecycle)
51
+ - [Application Configuration](#application-configuration)
52
+ - [Controllers](#controllers)
53
+ - [Repositories](#repositories)
54
+ - [Models](#models)
55
+ - [DataSources](#datasources)
56
+ - [Services](#services)
57
+ - [Components](#components)
58
+ - [Request Context](#request-context)
59
+ - [Middleware System](#middleware-system)
60
+ - [Error Handling](#error-handling)
61
+ - [Decorators Reference](#decorators-reference)
62
+ - [Response Helpers](#response-helpers)
63
+ - [Real-World Patterns](#real-world-patterns)
64
+ - [Testing](#testing)
65
+ - [Performance Tips](#performance-tips)
66
+ - [License](#license)
67
+
68
+ ---
7
69
 
8
70
  ## Installation
9
71
 
10
72
  ```bash
11
73
  bun add @venizia/ignis
12
- # or
13
- npm install @venizia/ignis
14
74
  ```
15
75
 
16
- ### Peer Dependencies
76
+ ### Required Peer Dependencies
77
+
78
+ ```bash
79
+ bun add hono @hono/zod-openapi drizzle-orm drizzle-zod pg jose @asteasolutions/zod-to-openapi
80
+ ```
81
+
82
+ ### Optional Peer Dependencies
83
+
84
+ Install only what you use:
17
85
 
18
86
  ```bash
19
- bun add hono @hono/zod-openapi @scalar/hono-api-reference drizzle-orm drizzle-zod pg jose
87
+ # Swagger / API Reference UI
88
+ bun add @hono/swagger-ui @scalar/hono-api-reference
89
+
90
+ # Node.js runtime (if not using Bun)
91
+ bun add @hono/node-server
92
+
93
+ # Socket.IO real-time
94
+ bun add socket.io socket.io-client @socket.io/bun-engine
95
+
96
+ # Redis adapter for Socket.IO horizontal scaling
97
+ bun add @socket.io/redis-adapter @socket.io/redis-emitter
98
+
99
+ # Background job queues
100
+ bun add bullmq
101
+
102
+ # Authorization (Casbin RBAC)
103
+ bun add casbin
104
+
105
+ # Email
106
+ bun add nodemailer mailgun.js
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Quick Start
112
+
113
+ ### 1. Define a Model
114
+
115
+ ```typescript
116
+ // models/user.model.ts
117
+ import { BaseEntity, model, generateIdColumnDefs, generateTzColumnDefs } from '@venizia/ignis';
118
+ import { pgTable, text } from 'drizzle-orm/pg-core';
119
+
120
+ @model({
121
+ type: 'entity',
122
+ settings: {
123
+ hiddenProperties: ['password'],
124
+ },
125
+ })
126
+ export class User extends BaseEntity<typeof User.schema> {
127
+ static override schema = pgTable('User', {
128
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
129
+ ...generateTzColumnDefs(),
130
+ username: text('username').notNull().unique(),
131
+ email: text('email').notNull().unique(),
132
+ password: text('password'),
133
+ });
134
+
135
+ static override relations = () => [];
136
+ }
20
137
  ```
21
138
 
22
- ## Quick Example
139
+ ### 2. Define a DataSource
23
140
 
24
141
  ```typescript
25
- import { BaseApplication, BaseController, controller, get, HTTP, jsonContent } from "@venizia/ignis";
26
- import { z } from "@hono/zod-openapi";
142
+ // datasources/postgres.datasource.ts
143
+ import { BaseDataSource, datasource, ValueOrPromise } from '@venizia/ignis';
144
+ import { drizzle } from 'drizzle-orm/node-postgres';
145
+ import { Pool } from 'pg';
146
+
147
+ interface IDSConfigs {
148
+ host: string;
149
+ port: number;
150
+ database: string;
151
+ user: string;
152
+ password: string;
153
+ }
27
154
 
28
- @controller({ path: "/hello" })
29
- class HelloController extends BaseController {
155
+ @datasource({ driver: 'node-postgres' })
156
+ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
30
157
  constructor() {
31
- super({ scope: "HelloController", path: "/hello" });
158
+ super({
159
+ name: PostgresDataSource.name,
160
+ config: {
161
+ host: process.env.DB_HOST!,
162
+ port: +(process.env.DB_PORT ?? 5432),
163
+ database: process.env.DB_NAME!,
164
+ user: process.env.DB_USER!,
165
+ password: process.env.DB_PASSWORD!,
166
+ },
167
+ // Schema is auto-discovered from @repository bindings
168
+ });
169
+ }
170
+
171
+ override configure(): ValueOrPromise<void> {
172
+ const schema = this.getSchema();
173
+ this.pool = new Pool(this.settings);
174
+ this.connector = drizzle({ client: this.pool, schema });
175
+ }
176
+
177
+ override getConnectionString() {
178
+ const { host, port, user, password, database } = this.settings;
179
+ return `postgresql://${user}:${password}@${host}:${port}/${database}`;
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### 3. Define a Repository
185
+
186
+ ```typescript
187
+ // repositories/user.repository.ts
188
+ import { PersistableRepository, repository } from '@venizia/ignis';
189
+ import { User } from '../models/user.model';
190
+ import { PostgresDataSource } from '../datasources/postgres.datasource';
191
+
192
+ @repository({ model: User, dataSource: PostgresDataSource })
193
+ export class UserRepository extends PersistableRepository<typeof User.schema> {
194
+ // No constructor needed -- DataSource is auto-injected at param[0]
195
+ }
196
+ ```
197
+
198
+ ### 4. Define a Controller
199
+
200
+ ```typescript
201
+ // controllers/user.controller.ts
202
+ import {
203
+ BaseController, controller, get, post,
204
+ inject, jsonContent, jsonResponse, HTTP, TRouteContext,
205
+ } from '@venizia/ignis';
206
+ import { z } from '@hono/zod-openapi';
207
+ import { UserRepository } from '../repositories/user.repository';
208
+
209
+ @controller({ path: '/users' })
210
+ export class UserController extends BaseController {
211
+ constructor(
212
+ @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
213
+ ) {
214
+ super({ scope: UserController.name });
32
215
  }
33
216
 
34
217
  override binding() {}
35
218
 
36
219
  @get({
37
220
  configs: {
38
- path: "/",
39
- method: HTTP.Methods.GET,
40
- responses: {
41
- [HTTP.ResultCodes.RS_2.Ok]: jsonContent({
42
- description: "Says hello",
43
- schema: z.object({ message: z.string() }),
221
+ path: '/',
222
+ responses: jsonResponse({
223
+ schema: z.array(z.object({ id: z.string(), username: z.string(), email: z.string() })),
224
+ }),
225
+ },
226
+ })
227
+ async listUsers(context: TRouteContext) {
228
+ const users = await this.userRepo.find({ filter: {} });
229
+ return context.json(users, 200);
230
+ }
231
+
232
+ @post({
233
+ configs: {
234
+ path: '/',
235
+ request: {
236
+ body: jsonContent({
237
+ description: 'New user data',
238
+ schema: z.object({ username: z.string(), email: z.string(), password: z.string() }),
44
239
  }),
45
240
  },
241
+ responses: jsonResponse({
242
+ schema: z.object({ count: z.number(), data: z.any() }),
243
+ }),
46
244
  },
47
245
  })
48
- sayHello(c: Context) {
49
- return c.json({ message: "Hello from Ignis!" }, HTTP.ResultCodes.RS_2.Ok);
246
+ async createUser(context: TRouteContext) {
247
+ const body = context.req.valid<{ username: string; email: string; password: string }>('json');
248
+ const result = await this.userRepo.create({ data: body });
249
+ return context.json(result, 200);
50
250
  }
51
251
  }
52
252
  ```
53
253
 
54
- ## About Ignis
254
+ ### 5. Define the Application
55
255
 
56
- Ignis brings together the structured, enterprise development experience of **LoopBack 4** with the blazing speed and simplicity of **Hono** - giving you the best of both worlds.
256
+ ```typescript
257
+ // application.ts
258
+ import {
259
+ BaseApplication, IApplicationConfigs, IApplicationInfo,
260
+ HealthCheckComponent, SwaggerComponent, ValueOrPromise,
261
+ } from '@venizia/ignis';
262
+ import { PostgresDataSource } from './datasources/postgres.datasource';
263
+ import { UserRepository } from './repositories/user.repository';
264
+ import { UserController } from './controllers/user.controller';
57
265
 
58
- ## Documentation
266
+ const configs: IApplicationConfigs = {
267
+ host: 'localhost',
268
+ port: 3000,
269
+ path: { base: '/api', isStrict: true },
270
+ };
271
+
272
+ export class Application extends BaseApplication {
273
+ constructor() {
274
+ super({ scope: Application.name, config: configs });
275
+ this.init();
276
+ }
277
+
278
+ getAppInfo(): IApplicationInfo {
279
+ return { name: 'My App', version: '1.0.0', description: 'My Ignis application' };
280
+ }
281
+
282
+ staticConfigure() {}
283
+
284
+ preConfigure(): ValueOrPromise<void> {
285
+ // Register components
286
+ this.component(HealthCheckComponent);
287
+ this.component(SwaggerComponent);
288
+
289
+ // Register datasources, repositories, and controllers
290
+ this.dataSource(PostgresDataSource);
291
+ this.repository(UserRepository);
292
+ this.controller(UserController);
293
+ }
294
+
295
+ postConfigure(): ValueOrPromise<void> {}
296
+
297
+ setupMiddlewares(): ValueOrPromise<void> {
298
+ // Add CORS, body limit, etc.
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### 6. Start the Server
304
+
305
+ ```typescript
306
+ // index.ts
307
+ import { Application } from './application';
308
+
309
+ const app = new Application();
310
+ app.start();
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Application Lifecycle
316
+
317
+ `BaseApplication` extends the IoC `Container` and orchestrates a well-defined startup sequence:
318
+
319
+ ```
320
+ 1. init() Register core bindings (app instance, server, root router)
321
+ 2. start() Entry point -- calls initialize() then starts the server
322
+ |
323
+ +-- initialize()
324
+ | |
325
+ | +-- printStartUpInfo() Log environment, runtime, timezone, datasource info
326
+ | +-- validateEnvs() Validate required environment variables
327
+ | +-- registerDefaultMiddlewares() Error handler, async context, request tracker, favicon
328
+ | +-- staticConfigure() Pre-DI static setup (e.g., serve static files)
329
+ | +-- preConfigure() Register controllers, services, components, datasources
330
+ | +-- registerDataSources() Configure all datasources (auto-discover schemas)
331
+ | +-- registerComponents() Configure all components (can register more datasources)
332
+ | +-- registerControllers() Configure controllers, mount routes on root router
333
+ | +-- postConfigure() Post-registration hooks
334
+ |
335
+ +-- setupMiddlewares() Register Hono middlewares (CORS, body limit, etc.)
336
+ +-- mount root router Mount to base path
337
+ +-- startBunModule / startNodeModule Start HTTP server
338
+ +-- executePostStartHooks() Run any registered post-start hooks
339
+ ```
340
+
341
+ ### What Happens Inside Each Phase
342
+
343
+ **`registerDefaultMiddlewares()`** -- Automatically sets up:
344
+
345
+ - `appErrorHandler` -- Global error handler that catches all errors, formats them as JSON, handles ZodError validation errors (returns 422), recognizes PostgreSQL constraint violations (returns 400 instead of 500), and strips stack traces in production.
346
+ - `contextStorage()` -- Hono async context storage for accessing request context anywhere (enabled by default, controlled via `asyncContext.enable` config).
347
+ - `RequestTrackerComponent` -- Injects `x-request-id` header on every request and parses request body.
348
+ - `emojiFavicon` -- Returns a favicon emoji response (configurable via `favicon` config).
349
+ - `notFoundHandler` -- Returns a structured 404 response for unmatched routes.
350
+
351
+ **`registerDataSources()`** -- Iterates all bindings tagged `datasources`, calls `configure()` on each. Schema auto-discovery happens here.
352
+
353
+ **`registerComponents()`** -- Iterates all bindings tagged `components`, calls `configure()` on each. Components can register additional datasources during their configuration (the method re-fetches bindings after each component to pick up dynamically added datasources).
354
+
355
+ **`registerControllers()`** -- Iterates all bindings tagged `controllers`. For each: validates that `@controller` metadata has a `path`, calls `configure()` (which triggers `binding()` and `registerRoutesFromRegistry()`), then mounts the controller's router at its configured path on the root router.
356
+
357
+ ### Key Application Methods
358
+
359
+ | Method | Description |
360
+ | --- | --- |
361
+ | `controller(ctor)` | Register a controller class -- bound to `controllers.{Name}` |
362
+ | `service(ctor)` | Register a service class -- bound to `services.{Name}` |
363
+ | `repository(ctor)` | Register a repository class -- bound to `repositories.{Name}` |
364
+ | `dataSource(ctor)` | Register a datasource class (singleton) -- bound to `datasources.{Name}` |
365
+ | `component(ctor)` | Register a component class (singleton) -- bound to `components.{Name}` |
366
+ | `static({ folderPath })` | Serve static files (auto-detects Bun/Node runtime) |
367
+ | `getServer()` | Get the main `OpenAPIHono` instance |
368
+ | `getServerPort()` | Get the configured server port |
369
+ | `getServerHost()` | Get the configured server host |
370
+ | `getServerAddress()` | Get `host:port` string |
371
+ | `getRootRouter()` | Get the root router for direct route registration |
372
+ | `getProjectRoot()` | Get the project working directory |
373
+ | `getProjectConfigs()` | Get the full application configuration object |
374
+ | `getServerInstance()` | Get the underlying Bun.Server or Node HTTP server instance |
375
+ | `registerPostStartHook({ identifier, hook })` | Register a callback to run after server starts |
376
+ | `boot()` | Convention-based auto-discovery (controllers, services, repositories, datasources) |
377
+ | `stop()` | Gracefully stop the server |
378
+
379
+ ### `registerDynamicBindings()` -- Handling Late/Circular Registrations
380
+
381
+ The `registerDynamicBindings()` method is the engine behind `registerDataSources()`, `registerComponents()`, and `registerControllers()`. It handles the case where configuring one binding may register new bindings of the same type:
382
+
383
+ ```typescript
384
+ protected async registerDynamicBindings<T extends IConfigurable>(opts: {
385
+ namespace: TBindingNamespace;
386
+ onBeforeConfigure?: (opts: { binding: Binding<T> }) => Promise<void>;
387
+ onAfterConfigure?: (opts: { binding: Binding<T>; instance: T }) => Promise<void>;
388
+ }): Promise<void>;
389
+ ```
390
+
391
+ It works by:
392
+
393
+ 1. Fetching all bindings for the given namespace, excluding already-configured ones.
394
+ 2. Configuring each binding in sequence.
395
+ 3. After each configuration, re-fetching bindings to pick up any newly added ones.
396
+ 4. Repeating until no new bindings remain.
397
+
398
+ This is critical for components that register datasources during their own configuration.
399
+
400
+ ### `registerPostStartHook()` -- Running Code After Server Start
401
+
402
+ ```typescript
403
+ // In preConfigure() or postConfigure():
404
+ this.registerPostStartHook({
405
+ identifier: 'warmup-cache',
406
+ hook: async () => {
407
+ const cacheService = this.get<CacheService>({ key: 'services.CacheService' });
408
+ await cacheService.warmup();
409
+ console.log('Cache warmed up');
410
+ },
411
+ });
412
+
413
+ this.registerPostStartHook({
414
+ identifier: 'register-cron-jobs',
415
+ hook: async () => {
416
+ const cronService = this.get<CronService>({ key: 'services.CronService' });
417
+ cronService.startAll();
418
+ },
419
+ });
420
+ ```
421
+
422
+ Post-start hooks execute sequentially after the HTTP server is listening. Each hook is logged with its execution time.
423
+
424
+ ### Static File Serving
425
+
426
+ ```typescript
427
+ staticConfigure() {
428
+ // Serve files from ./public directory at all unmatched routes
429
+ this.static({ folderPath: './public' });
430
+
431
+ // Or serve at a specific path
432
+ this.static({ restPath: '/assets/*', folderPath: './static-assets' });
433
+ }
434
+ ```
435
+
436
+ Runtime-aware: uses `hono/bun` `serveStatic` on Bun, `@hono/node-server/serve-static` on Node.js.
437
+
438
+ ### Runtime Detection
439
+
440
+ Ignis auto-detects the runtime and starts the server accordingly:
441
+
442
+ ```typescript
443
+ // Bun (default)
444
+ Bun.serve({ port, hostname, fetch: server.fetch });
445
+
446
+ // Node.js (requires @hono/node-server)
447
+ import { serve } from '@hono/node-server';
448
+ serve({ fetch: server.fetch, port, hostname });
449
+ ```
450
+
451
+ The runtime is detected via `RuntimeModules.detect()` which checks for the presence of the global `Bun` object.
452
+
453
+ ---
454
+
455
+ ## Application Configuration
456
+
457
+ ```typescript
458
+ interface IApplicationConfigs {
459
+ host?: string; // Server host (default: 'localhost' or APP_ENV_SERVER_HOST env)
460
+ port?: number; // Server port (default: 3000 or PORT/APP_ENV_SERVER_PORT env)
461
+
462
+ path: {
463
+ base: string; // Base path for all routes (e.g., '/api')
464
+ isStrict: boolean; // When true, '/users' and '/users/' are different routes
465
+ };
466
+
467
+ requestId?: {
468
+ isStrict: boolean; // Enforce request ID on all requests
469
+ };
470
+
471
+ favicon?: string; // Emoji favicon (default: fire emoji)
472
+
473
+ error?: {
474
+ rootKey: string; // Wrap error responses in this key (e.g., 'error')
475
+ };
476
+
477
+ asyncContext?: {
478
+ enable: boolean; // Enable Hono async context storage (default: true)
479
+ };
480
+
481
+ bootOptions?: IBootOptions; // Convention-based auto-discovery options
482
+
483
+ debug?: {
484
+ shouldShowRoutes?: boolean; // Print all registered routes on startup
485
+ };
486
+ }
487
+ ```
488
+
489
+ ```typescript
490
+ interface IApplicationInfo {
491
+ name: string;
492
+ version: string;
493
+ description: string;
494
+ author?: { name: string; email: string; url?: string };
495
+ }
496
+ ```
497
+
498
+ ---
499
+
500
+ ## Controllers
501
+
502
+ ### BaseController
503
+
504
+ All controllers extend `BaseController`, which provides:
505
+
506
+ - An `OpenAPIHono` router instance
507
+ - Route registration methods (`defineRoute`, `bindRoute`, `defineJSXRoute`)
508
+ - Automatic authentication and authorization middleware injection
509
+ - OpenAPI schema generation and route tagging
510
+ - Zod-based request validation with automatic 422 error responses
511
+
512
+ ```typescript
513
+ abstract class BaseController extends AbstractController {
514
+ // Register routes -- override this method
515
+ abstract binding(): ValueOrPromise<void>;
516
+
517
+ // Imperative route definition
518
+ defineRoute({ configs, handler, hook? });
519
+
520
+ // Fluent two-step route definition
521
+ bindRoute({ configs }).to({ handler });
522
+
523
+ // JSX/HTML route definition (server-side rendering)
524
+ defineJSXRoute({ configs, handler });
525
+
526
+ // Get the router for this controller
527
+ getRouter(): OpenAPIHono;
528
+ }
529
+ ```
530
+
531
+ ### Three Route Definition Patterns
532
+
533
+ #### 1. Decorator Pattern
534
+
535
+ Use `@get`, `@post`, `@put`, `@patch`, `@del`, or the generic `@api` decorators. Decorator-based routes are automatically registered during `configure()` via `registerRoutesFromRegistry()`:
536
+
537
+ ```typescript
538
+ @controller({ path: '/products' })
539
+ class ProductController extends BaseController {
540
+ constructor(
541
+ @inject({ key: 'repositories.ProductRepository' }) private productRepo: ProductRepository,
542
+ @inject({ key: 'services.InventoryService' }) private inventoryService: InventoryService,
543
+ ) {
544
+ super({ scope: ProductController.name });
545
+ }
546
+
547
+ override binding() {} // decorator routes are auto-registered
548
+
549
+ @get({
550
+ configs: {
551
+ path: '/',
552
+ description: 'List all products with pagination',
553
+ responses: jsonResponse({
554
+ schema: z.array(z.object({
555
+ id: z.number(),
556
+ name: z.string(),
557
+ price: z.number(),
558
+ category: z.string(),
559
+ })),
560
+ description: 'Array of products',
561
+ }),
562
+ },
563
+ })
564
+ async list(context: TRouteContext) {
565
+ const products = await this.productRepo.find({
566
+ filter: { order: ['createdAt DESC'], limit: 20 },
567
+ });
568
+ return context.json(products, 200);
569
+ }
570
+
571
+ @get({
572
+ configs: {
573
+ path: '/{id}',
574
+ request: {
575
+ params: z.object({ id: z.string().pipe(z.coerce.number()) }),
576
+ },
577
+ responses: jsonResponse({
578
+ schema: z.object({
579
+ id: z.number(),
580
+ name: z.string(),
581
+ price: z.number(),
582
+ stock: z.number(),
583
+ }),
584
+ }),
585
+ },
586
+ })
587
+ async getById(context: TRouteContext) {
588
+ const { id } = context.req.valid<{ id: number }>('param');
589
+ const product = await this.productRepo.findById({ id });
590
+ if (!product) {
591
+ return context.json({ message: 'Product not found' }, 404);
592
+ }
593
+ return context.json(product, 200);
594
+ }
595
+
596
+ @post({
597
+ configs: {
598
+ path: '/',
599
+ authenticate: { strategies: ['jwt'] },
600
+ request: {
601
+ body: jsonContent({
602
+ schema: z.object({
603
+ name: z.string().min(1).max(255),
604
+ price: z.number().positive(),
605
+ category: z.string(),
606
+ description: z.string().optional(),
607
+ }),
608
+ description: 'New product data',
609
+ }),
610
+ },
611
+ responses: jsonResponse({
612
+ schema: z.object({ count: z.number(), data: z.any() }),
613
+ }),
614
+ },
615
+ })
616
+ async create(context: TRouteContext) {
617
+ const data = context.req.valid<{
618
+ name: string;
619
+ price: number;
620
+ category: string;
621
+ description?: string;
622
+ }>('json');
623
+ const result = await this.productRepo.create({ data });
624
+ return context.json(result, 200);
625
+ }
626
+ }
627
+ ```
628
+
629
+ #### 2. Imperative Pattern
630
+
631
+ Define routes directly inside `binding()`:
632
+
633
+ ```typescript
634
+ override binding() {
635
+ this.defineRoute({
636
+ configs: {
637
+ path: '/',
638
+ method: 'get',
639
+ description: 'List products',
640
+ responses: jsonResponse({ schema: z.array(ProductSchema) }),
641
+ },
642
+ handler: async (context) => {
643
+ const products = await this.productRepo.find({ filter: {} });
644
+ return context.json(products, 200);
645
+ },
646
+ });
647
+
648
+ this.defineRoute({
649
+ configs: {
650
+ path: '/{id}',
651
+ method: 'delete',
652
+ authenticate: { strategies: ['jwt'] },
653
+ authorize: { action: 'delete', resource: 'Product' },
654
+ request: { params: idParamsSchema({ idType: 'number' }) },
655
+ responses: jsonResponse({ schema: z.object({ count: z.number() }) }),
656
+ },
657
+ handler: async (context) => {
658
+ const { id } = context.req.valid<{ id: number }>('param');
659
+ const result = await this.productRepo.deleteById({ id });
660
+ return context.json(result, 200);
661
+ },
662
+ });
663
+ }
664
+ ```
665
+
666
+ #### 3. Fluent Pattern
667
+
668
+ Two-step binding with `bindRoute().to()`:
669
+
670
+ ```typescript
671
+ override binding() {
672
+ this.bindRoute({
673
+ configs: {
674
+ path: '/{id}',
675
+ method: 'get',
676
+ request: { params: idParamsSchema({ idType: 'number' }) },
677
+ responses: jsonResponse({ schema: ProductSchema }),
678
+ },
679
+ }).to({
680
+ handler: async (context) => {
681
+ const { id } = context.req.valid<{ id: number }>('param');
682
+ const product = await this.productRepo.findById({ id });
683
+ return context.json(product, 200);
684
+ },
685
+ });
686
+ }
687
+ ```
688
+
689
+ ### `getRouteConfigs()` -- How Auth Middleware is Injected
690
+
691
+ When you specify `authenticate` or `authorize` on a route config, `getRouteConfigs()` automatically:
692
+
693
+ 1. Converts `authenticate.strategies` into OpenAPI security specs for documentation.
694
+ 2. Creates an `authenticate` middleware based on strategies and mode, and prepends it to the middleware chain.
695
+ 3. Creates an `authorize` middleware (if configured) and appends it after authenticate.
696
+ 4. Merges any custom `middleware` array from the config.
697
+ 5. Adds the controller's scope name as an OpenAPI tag.
698
+
699
+ This means you never manually wire auth middleware -- it is all declarative.
700
+
701
+ ### Middleware Chaining on Routes
702
+
703
+ You can pass additional Hono middleware to any route:
704
+
705
+ ```typescript
706
+ import { rateLimiter } from 'hono/rate-limiter';
707
+ import { cors } from 'hono/cors';
708
+
709
+ @post({
710
+ configs: {
711
+ path: '/upload',
712
+ middleware: [
713
+ rateLimiter({ windowMs: 60_000, limit: 10 }),
714
+ cors({ origin: 'https://myapp.com' }),
715
+ ],
716
+ authenticate: { strategies: ['jwt'] },
717
+ // ...
718
+ },
719
+ })
720
+ async uploadFile(context: TRouteContext) { /* ... */ }
721
+ ```
722
+
723
+ Middleware execution order: `authenticate` -> `authorize` -> custom middleware -> handler.
724
+
725
+ ### Request Validation with Zod
726
+
727
+ Routes automatically validate request parameters, query strings, headers, and body against Zod schemas. Invalid requests return a `422 Unprocessable Entity` with structured error details:
728
+
729
+ ```typescript
730
+ @post({
731
+ configs: {
732
+ path: '/',
733
+ request: {
734
+ body: jsonContent({
735
+ schema: z.object({
736
+ email: z.string().email('Invalid email format'),
737
+ age: z.number().int().min(18, 'Must be at least 18'),
738
+ role: z.enum(['admin', 'user', 'moderator']),
739
+ }),
740
+ }),
741
+ query: z.object({
742
+ dryRun: z.string().optional().transform(v => v === 'true'),
743
+ }),
744
+ headers: z.object({
745
+ 'x-api-key': z.string().min(1),
746
+ }),
747
+ },
748
+ responses: jsonResponse({ schema: UserSchema }),
749
+ },
750
+ })
751
+ async createUser(context: TRouteContext) {
752
+ const body = context.req.valid<{ email: string; age: number; role: string }>('json');
753
+ const { dryRun } = context.req.valid<{ dryRun?: boolean }>('query');
754
+ const apiKey = context.req.valid<{ 'x-api-key': string }>('header');
755
+ // All validated -- proceed safely
756
+ }
757
+ ```
758
+
759
+ On validation failure, the error handler returns:
760
+
761
+ ```json
762
+ {
763
+ "message": "ValidationError",
764
+ "statusCode": 422,
765
+ "requestId": "abc-123",
766
+ "details": {
767
+ "cause": [
768
+ { "path": "email", "message": "Invalid email format", "code": "invalid_string" },
769
+ { "path": "age", "message": "Must be at least 18", "code": "too_small" }
770
+ ]
771
+ }
772
+ }
773
+ ```
774
+
775
+ ### Accessing Hono Context
776
+
777
+ The `context` parameter (`TRouteContext`) provides full access to the Hono request/response:
778
+
779
+ ```typescript
780
+ async myHandler(context: TRouteContext) {
781
+ // Request data
782
+ const body = context.req.valid<MyType>('json');
783
+ const params = context.req.valid<{ id: number }>('param');
784
+ const query = context.req.valid<{ page: number }>('query');
785
+
786
+ // Raw request access
787
+ const url = context.req.url;
788
+ const method = context.req.method;
789
+ const path = context.req.path;
790
+ const userAgent = context.req.header('user-agent');
791
+ const allHeaders = context.req.raw.headers;
792
+
793
+ // Authenticated user (set by auth middleware)
794
+ const currentUser = context.get('auth.current.user');
795
+ const auditUserId = context.get('audit.user.id');
796
+
797
+ // Set response headers
798
+ context.header('X-Custom-Header', 'value');
799
+ context.header('Cache-Control', 'no-store');
800
+
801
+ // Response types
802
+ return context.json({ data: 'value' }, 200);
803
+ return context.text('plain text', 200);
804
+ return context.html('<h1>Hello</h1>');
805
+ return context.redirect('/other-page');
806
+ return context.body(null, 204); // No content
807
+ }
808
+ ```
809
+
810
+ ### File Upload Handling
811
+
812
+ ```typescript
813
+ @post({
814
+ configs: {
815
+ path: '/upload',
816
+ authenticate: { strategies: ['jwt'] },
817
+ responses: jsonResponse({ schema: z.object({ filename: z.string(), size: z.number() }) }),
818
+ },
819
+ })
820
+ async upload(context: TRouteContext) {
821
+ const body = await context.req.parseBody();
822
+ const file = body['file'];
823
+
824
+ if (file instanceof File) {
825
+ const buffer = await file.arrayBuffer();
826
+ // Process file...
827
+ return context.json({ filename: file.name, size: file.size }, 200);
828
+ }
829
+
830
+ return context.json({ message: 'No file provided' }, 400);
831
+ }
832
+ ```
833
+
834
+ ### Streaming Responses
835
+
836
+ ```typescript
837
+ @get({
838
+ configs: {
839
+ path: '/stream',
840
+ responses: { 200: { description: 'Streamed response' } },
841
+ },
842
+ })
843
+ async streamData(context: TRouteContext) {
844
+ return context.body(
845
+ new ReadableStream({
846
+ start(controller) {
847
+ controller.enqueue(new TextEncoder().encode('chunk 1\n'));
848
+ setTimeout(() => {
849
+ controller.enqueue(new TextEncoder().encode('chunk 2\n'));
850
+ controller.close();
851
+ }, 1000);
852
+ },
853
+ }),
854
+ 200,
855
+ { 'Content-Type': 'text/plain' },
856
+ );
857
+ }
858
+ ```
859
+
860
+ ### JSX Server-Side Rendering
861
+
862
+ ```typescript
863
+ this.defineJSXRoute({
864
+ configs: {
865
+ path: '/profile',
866
+ method: 'get',
867
+ description: 'User profile page',
868
+ authenticate: { strategies: ['jwt'] },
869
+ },
870
+ handler: (context) => {
871
+ const user = context.get('auth.current.user');
872
+ return context.html(<ProfilePage user={user} />);
873
+ },
874
+ });
875
+ ```
876
+
877
+ ### Route Decorators
878
+
879
+ | Decorator | Description |
880
+ | --- | --- |
881
+ | `@controller({ path, authenticate? })` | Class decorator -- registers controller path and optional default auth |
882
+ | `@get({ configs })` | GET route -- method is set automatically |
883
+ | `@post({ configs })` | POST route |
884
+ | `@put({ configs })` | PUT route |
885
+ | `@patch({ configs })` | PATCH route |
886
+ | `@del({ configs })` | DELETE route |
887
+ | `@api({ configs })` | Generic route -- specify method in configs |
888
+
889
+ ### Route Configuration
890
+
891
+ ```typescript
892
+ interface IAuthRouteConfig extends HonoRouteConfig {
893
+ path: string;
894
+ method: 'get' | 'post' | 'put' | 'patch' | 'delete';
895
+ description?: string;
896
+ tags?: string[];
897
+
898
+ // Authentication
899
+ authenticate?: {
900
+ strategies?: ('jwt' | 'basic')[];
901
+ mode?: 'any' | 'all';
902
+ };
903
+
904
+ // Authorization (Casbin RBAC)
905
+ authorize?: IAuthorizationSpec | IAuthorizationSpec[];
906
+
907
+ // Request schema validation
908
+ request?: {
909
+ body?: ContentConfig;
910
+ query?: ZodSchema;
911
+ params?: ZodSchema;
912
+ headers?: ZodSchema;
913
+ };
914
+
915
+ // Response schema
916
+ responses: Record<number | string, ResponseConfig>;
917
+
918
+ // Additional Hono middleware
919
+ middleware?: MiddlewareHandler[];
920
+ }
921
+ ```
922
+
923
+ ### Controller Factory
924
+
925
+ Auto-generate a full CRUD controller from an entity definition:
926
+
927
+ ```typescript
928
+ import { ControllerFactory } from '@venizia/ignis';
929
+
930
+ const UserCrudController = ControllerFactory.defineCrudController({
931
+ entity: User,
932
+ repository: { name: 'UserRepository' },
933
+ controller: {
934
+ name: 'UserCrudController',
935
+ basePath: '/users',
936
+ isStrict: {
937
+ path: true, // Strict path matching
938
+ requestSchema: true, // Strict Zod request validation
939
+ },
940
+ },
941
+ authenticate: { strategies: ['jwt'] },
942
+ authorize: { action: 'manage', resource: 'User' },
943
+ routes: {
944
+ find: { authenticate: { skip: true } }, // Public read -- also skips authorization
945
+ findById: { authenticate: { skip: true } }, // Public read
946
+ count: { authenticate: { skip: true } }, // Public read
947
+ create: {
948
+ request: { body: CustomCreateSchema }, // Override request body schema
949
+ },
950
+ deleteById: {
951
+ authorize: { action: 'delete', resource: 'User' }, // Override authorization
952
+ },
953
+ },
954
+ });
955
+ ```
956
+
957
+ This generates the following endpoints:
958
+
959
+ | Method | Path | Description |
960
+ | --- | --- | --- |
961
+ | `GET` | `/count` | Count records matching where condition |
962
+ | `GET` | `/` | Find all records (paginated, with Content-Range header) |
963
+ | `GET` | `/{id}` | Find record by ID |
964
+ | `GET` | `/find-one` | Find first matching record |
965
+ | `POST` | `/` | Create new record |
966
+ | `PATCH` | `/{id}` | Update record by ID |
967
+ | `PATCH` | `/` | Bulk update matching records |
968
+ | `DELETE` | `/{id}` | Delete record by ID |
969
+ | `DELETE` | `/` | Bulk delete matching records |
970
+
971
+ Each generated endpoint includes:
972
+
973
+ - OpenAPI schema documentation derived from entity Zod schemas (select, create, update).
974
+ - Conditional count response via `x-request-count` header -- send `x-request-count: false` to get data only without the wrapping `{ count, data }` object.
975
+ - Content-Range header for paginated find results (e.g., `records 0-19/150`).
976
+ - Authentication and authorization middleware from controller-level or route-level config.
977
+ - `X-Response-Count-Data` response header with the count of returned records.
978
+
979
+ #### Customizing Controller Factory Routes
980
+
981
+ Per-route auth configuration priority:
982
+
983
+ 1. If a route has `authenticate: { skip: true }` -- no authentication AND no authorization for that route.
984
+ 2. If a route has `authenticate: { strategies, mode }` -- uses these, overriding controller defaults.
985
+ 3. If a route has `authorize: { skip: true }` -- keeps authentication but skips authorization.
986
+ 4. Otherwise -- uses controller-level `authenticate` and `authorize`.
987
+
988
+ You can override request/response schemas per route:
989
+
990
+ ```typescript
991
+ routes: {
992
+ create: {
993
+ request: { body: CustomCreateSchema }, // Custom request body
994
+ response: { schema: CustomResponseSchema }, // Custom response schema
995
+ },
996
+ find: {
997
+ request: { query: CustomFilterQuerySchema }, // Custom query params
998
+ response: { headers: { 'X-Total': { description: 'Total count', schema: { type: 'string' } } } },
999
+ },
1000
+ }
1001
+ ```
1002
+
1003
+ ---
1004
+
1005
+ ## Repositories
1006
+
1007
+ ### Hierarchy
1008
+
1009
+ ```
1010
+ AbstractRepository
1011
+ extends DefaultFilterMixin(FieldsVisibilityMixin(BaseHelper))
1012
+ |
1013
+ +-- ReadableRepository (read operations only -- write operations throw errors)
1014
+ | |
1015
+ | +-- PersistableRepository (+ create, update, delete operations)
1016
+ | |
1017
+ | +-- DefaultCRUDRepository (alias -- identical to PersistableRepository)
1018
+ ```
1019
+
1020
+ `PersistableRepository` is the recommended base class for most use cases. `DefaultCRUDRepository` is a convenience alias. Use `ReadableRepository` when you need a repository that should only read data (e.g., reporting views, read replicas).
1021
+
1022
+ ### Defining a Repository
1023
+
1024
+ ```typescript
1025
+ // Zero boilerplate -- DataSource auto-injected from @repository metadata
1026
+ @repository({ model: User, dataSource: PostgresDataSource })
1027
+ export class UserRepository extends PersistableRepository<typeof User.schema> {
1028
+ // No constructor needed!
1029
+ }
1030
+
1031
+ // Or with explicit @inject for more control
1032
+ @repository({ model: User, dataSource: PostgresDataSource })
1033
+ export class UserRepository extends PersistableRepository<typeof User.schema> {
1034
+ constructor(
1035
+ @inject({ key: 'datasources.PostgresDataSource' }) dataSource: PostgresDataSource,
1036
+ ) {
1037
+ super(dataSource);
1038
+ }
1039
+
1040
+ // Custom methods
1041
+ async findByEmail(email: string) {
1042
+ return this.findOne({ filter: { where: { email } } });
1043
+ }
1044
+
1045
+ async findActiveUsers() {
1046
+ return this.find({
1047
+ filter: {
1048
+ where: { status: 'active' },
1049
+ order: ['createdAt DESC'],
1050
+ },
1051
+ });
1052
+ }
1053
+ }
1054
+ ```
1055
+
1056
+ **Important:** Both `model` AND `dataSource` are required in `@repository` for schema auto-discovery. Without both, the model will not be registered in the datasource schema and relational queries will fail.
1057
+
1058
+ ### Read Operations
1059
+
1060
+ #### `count()` -- Count Records
1061
+
1062
+ ```typescript
1063
+ // Simple count
1064
+ const { count } = await repo.count({ where: { status: 'active' } });
1065
+
1066
+ // Count with complex conditions
1067
+ const { count } = await repo.count({
1068
+ where: {
1069
+ and: [
1070
+ { role: { inq: ['admin', 'moderator'] } },
1071
+ { createdAt: { gte: new Date('2024-01-01') } },
1072
+ { or: [{ isVerified: true }, { score: { gt: 100 } }] },
1073
+ ],
1074
+ },
1075
+ });
1076
+
1077
+ // Count within a transaction
1078
+ const { count } = await repo.count({
1079
+ where: { status: 'pending' },
1080
+ options: { transaction: tx },
1081
+ });
1082
+ ```
1083
+
1084
+ #### `existsWith()` -- Check Existence
1085
+
1086
+ ```typescript
1087
+ const emailTaken = await repo.existsWith({
1088
+ where: { email: 'john@example.com' },
1089
+ });
1090
+
1091
+ if (emailTaken) {
1092
+ throw new Error('Email already in use');
1093
+ }
1094
+ ```
1095
+
1096
+ #### `find()` -- Find All Records
1097
+
1098
+ ```typescript
1099
+ // Basic find with filter
1100
+ const users = await repo.find({
1101
+ filter: {
1102
+ where: { status: 'active' },
1103
+ fields: ['id', 'name', 'email'],
1104
+ order: ['createdAt DESC'],
1105
+ limit: 20,
1106
+ skip: 0,
1107
+ },
1108
+ });
1109
+
1110
+ // Find with pagination range info
1111
+ const { data, range } = await repo.find({
1112
+ filter: { where: { status: 'active' }, limit: 20, skip: 40 },
1113
+ options: { shouldQueryRange: true },
1114
+ });
1115
+ // data = User[] (the 20 records)
1116
+ // range = { start: 40, end: 59, total: 150 }
1117
+
1118
+ // Find with relation inclusion (uses Query API)
1119
+ const usersWithPosts = await repo.find({
1120
+ filter: {
1121
+ where: { isActive: true },
1122
+ include: [
1123
+ { relation: 'posts', scope: { where: { isPublished: true }, limit: 5 } },
1124
+ ],
1125
+ },
1126
+ });
1127
+
1128
+ // Find all (bypass default filter for admin views)
1129
+ const allUsers = await repo.find({
1130
+ filter: {},
1131
+ options: { shouldSkipDefaultFilter: true },
1132
+ });
1133
+
1134
+ // Find with transaction
1135
+ const users = await repo.find({
1136
+ filter: { where: { batchId: currentBatch } },
1137
+ options: { transaction: tx },
1138
+ });
1139
+
1140
+ // Find with debug logging
1141
+ const users = await repo.find({
1142
+ filter: { where: { status: 'active' } },
1143
+ options: { log: { use: true, level: 'debug' } },
1144
+ });
1145
+ ```
1146
+
1147
+ #### `findOne()` vs `findById()` -- Differences
1148
+
1149
+ `findOne()` accepts a full filter with `where`, `fields`, `include`, and `order`. It returns the first matching record:
1150
+
1151
+ ```typescript
1152
+ const user = await repo.findOne({
1153
+ filter: {
1154
+ where: { email: 'john@example.com' },
1155
+ fields: ['id', 'name', 'email'],
1156
+ include: [{ relation: 'profile' }],
1157
+ },
1158
+ });
1159
+ // Returns User | null
1160
+ ```
1161
+
1162
+ `findById()` is a convenience wrapper around `findOne()` that automatically sets `where: { id }`. It accepts an optional filter **without** the `where` clause:
1163
+
1164
+ ```typescript
1165
+ const user = await repo.findById({
1166
+ id: 42,
1167
+ filter: {
1168
+ fields: ['id', 'name', 'email'],
1169
+ include: [{ relation: 'posts' }],
1170
+ },
1171
+ });
1172
+ // Returns User | null
1173
+ // Equivalent to: findOne({ filter: { where: { id: 42 }, fields: [...], include: [...] } })
1174
+ ```
1175
+
1176
+ ### Write Operations
1177
+
1178
+ #### `create()` -- Create Single Record
1179
+
1180
+ ```typescript
1181
+ // Create and return the created record (default: shouldReturn = true)
1182
+ const { count, data } = await repo.create({
1183
+ data: { username: 'john', email: 'john@example.com', role: 'user' },
1184
+ });
1185
+ // count = 1, data = { id: 1, username: 'john', ... }
1186
+
1187
+ // Create without returning data (faster -- skips RETURNING clause)
1188
+ const { count } = await repo.create({
1189
+ data: { username: 'john', email: 'john@example.com' },
1190
+ options: { shouldReturn: false },
1191
+ });
1192
+ // count = 1, data = null
1193
+
1194
+ // Create within a transaction
1195
+ const { data: user } = await repo.create({
1196
+ data: { username: 'john', email: 'john@example.com' },
1197
+ options: { transaction: tx },
1198
+ });
1199
+ ```
1200
+
1201
+ #### `createAll()` -- Bulk Create
1202
+
1203
+ ```typescript
1204
+ // Bulk create and return all records
1205
+ const { count, data } = await repo.createAll({
1206
+ data: [
1207
+ { username: 'john', email: 'john@example.com' },
1208
+ { username: 'jane', email: 'jane@example.com' },
1209
+ { username: 'bob', email: 'bob@example.com' },
1210
+ ],
1211
+ });
1212
+ // count = 3, data = [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
1213
+
1214
+ // Bulk create without returning (faster for large inserts)
1215
+ const { count } = await repo.createAll({
1216
+ data: largeDataArray,
1217
+ options: { shouldReturn: false },
1218
+ });
1219
+ ```
1220
+
1221
+ #### `updateById()` -- Update Single Record
1222
+
1223
+ ```typescript
1224
+ // Update by ID and return the updated record
1225
+ const { count, data } = await repo.updateById({
1226
+ id: 42,
1227
+ data: { email: 'new@example.com', status: 'verified' },
1228
+ });
1229
+ // count = 1, data = { id: 42, email: 'new@example.com', ... }
1230
+
1231
+ // Update JSON fields using dot notation
1232
+ const { data } = await repo.updateById({
1233
+ id: 42,
1234
+ data: {
1235
+ 'metadata.theme': 'dark',
1236
+ 'metadata.notifications.email': false,
1237
+ },
1238
+ });
1239
+ ```
1240
+
1241
+ #### `updateAll()` / `updateBy()` -- Bulk Update
1242
+
1243
+ ```typescript
1244
+ // Update all matching records
1245
+ const { count, data } = await repo.updateAll({
1246
+ data: { status: 'inactive' },
1247
+ where: { lastLoginAt: { lt: new Date('2024-01-01') } },
1248
+ });
1249
+ // count = 25, data = [...25 updated records...]
1250
+
1251
+ // updateBy is an alias for updateAll
1252
+ const { count } = await repo.updateBy({
1253
+ data: { isNotified: true },
1254
+ where: { role: 'subscriber' },
1255
+ options: { shouldReturn: false },
1256
+ });
1257
+
1258
+ // SAFETY: Empty where throws an error to prevent accidental mass updates
1259
+ // Use force: true to explicitly allow it
1260
+ const { count } = await repo.updateAll({
1261
+ data: { version: 2 },
1262
+ where: {},
1263
+ options: { force: true },
1264
+ });
1265
+ ```
1266
+
1267
+ ### Delete Operations
1268
+
1269
+ ```typescript
1270
+ // Delete by ID (returns deleted record)
1271
+ const { count, data } = await repo.deleteById({ id: 42 });
1272
+ // count = 1, data = { id: 42, username: 'john', ... }
1273
+
1274
+ // Delete all matching records
1275
+ const { count, data } = await repo.deleteAll({
1276
+ where: { status: 'inactive' },
1277
+ });
1278
+
1279
+ // deleteBy is an alias for deleteAll
1280
+ const { count } = await repo.deleteBy({
1281
+ where: { expiresAt: { lt: new Date() } },
1282
+ options: { shouldReturn: false },
1283
+ });
1284
+
1285
+ // SAFETY: Empty where throws an error. Use force: true to allow.
1286
+ const { count } = await repo.deleteAll({
1287
+ where: {},
1288
+ options: { force: true, shouldReturn: false },
1289
+ });
1290
+ ```
1291
+
1292
+ ### Filter System
1293
+
1294
+ ```typescript
1295
+ interface TFilter<T> {
1296
+ where?: TWhere<T>; // Query conditions
1297
+ fields?: TFields; // Column selection
1298
+ include?: TInclusion[]; // Relation loading
1299
+ order?: string[]; // Sorting (e.g., ['createdAt DESC', 'name ASC'])
1300
+ limit?: number; // Max results (default: 10)
1301
+ skip?: number; // Offset
1302
+ offset?: number; // Alias for skip
1303
+ }
1304
+ ```
1305
+
1306
+ #### Where Operators -- Complete Reference
1307
+
1308
+ **Comparison operators:**
1309
+
1310
+ | Operator | Description | Example |
1311
+ | --- | --- | --- |
1312
+ | _(equality)_ | Exact match | `{ status: 'active' }` |
1313
+ | `eq` | Equal | `{ age: { eq: 25 } }` |
1314
+ | `ne` / `neq` | Not equal | `{ role: { neq: 'guest' } }` |
1315
+ | `gt` | Greater than | `{ score: { gt: 90 } }` |
1316
+ | `gte` | Greater than or equal | `{ age: { gte: 18 } }` |
1317
+ | `lt` | Less than | `{ price: { lt: 100 } }` |
1318
+ | `lte` | Less than or equal | `{ priority: { lte: 5 } }` |
1319
+
1320
+ **Pattern matching operators:**
1321
+
1322
+ | Operator | Description | Example |
1323
+ | --- | --- | --- |
1324
+ | `like` | SQL LIKE (case-sensitive) | `{ name: { like: '%john%' } }` |
1325
+ | `ilike` | Case-insensitive LIKE | `{ email: { ilike: '%@GMAIL.COM' } }` |
1326
+ | `nlike` | NOT LIKE | `{ name: { nlike: '%test%' } }` |
1327
+ | `nilike` | NOT ILIKE | `{ email: { nilike: '%spam%' } }` |
1328
+ | `regexp` | POSIX regex (case-sensitive) | `{ code: { regexp: '^[A-Z]{3}' } }` |
1329
+ | `iregexp` | POSIX regex (case-insensitive) | `{ name: { iregexp: '^john' } }` |
1330
+
1331
+ **Array/set operators:**
1332
+
1333
+ | Operator | Description | Example |
1334
+ | --- | --- | --- |
1335
+ | `inq` / `in` | IN array | `{ status: { inq: ['active', 'pending'] } }` |
1336
+ | `nin` | NOT IN array | `{ role: { nin: ['banned', 'deleted'] } }` |
1337
+ | `between` | BETWEEN two values | `{ age: { between: [18, 65] } }` |
1338
+ | `notBetween` | NOT BETWEEN | `{ score: { notBetween: [0, 10] } }` |
1339
+
1340
+ **Null check operators:**
1341
+
1342
+ | Operator | Description | Example |
1343
+ | --- | --- | --- |
1344
+ | `is` | IS NULL (when value is null) | `{ deletedAt: { is: null } }` |
1345
+ | `isn` | IS NOT NULL (when value is null) | `{ email: { isn: null } }` |
1346
+
1347
+ **Logical operators:**
1348
+
1349
+ | Operator | Description | Example |
1350
+ | --- | --- | --- |
1351
+ | `and` | Logical AND | `{ and: [{ status: 'active' }, { role: 'admin' }] }` |
1352
+ | `or` | Logical OR | `{ or: [{ role: 'admin' }, { role: 'moderator' }] }` |
1353
+
1354
+ **PostgreSQL array column operators** (for columns defined as `text[]`, `integer[]`, etc.):
1355
+
1356
+ | Operator | SQL | Description | Example |
1357
+ | --- | --- | --- | --- |
1358
+ | `contains` | `@>` | Array contains all elements | `{ tags: { contains: ['urgent', 'bug'] } }` |
1359
+ | `containedBy` | `<@` | Array is contained by | `{ tags: { containedBy: ['a', 'b', 'c'] } }` |
1360
+ | `overlaps` | `&&` | Arrays have common elements | `{ categories: { overlaps: ['tech', 'science'] } }` |
1361
+
1362
+ **JSON path queries** (for `json`/`jsonb` columns):
1363
+
1364
+ ```typescript
1365
+ // Query nested JSON fields using dot notation
1366
+ const users = await repo.find({
1367
+ filter: {
1368
+ where: {
1369
+ 'metadata.theme': 'dark',
1370
+ 'settings.notifications.email': true,
1371
+ 'preferences.items[0].enabled': { eq: true },
1372
+ 'metadata.score': { gt: 50, lte: 100 },
1373
+ },
1374
+ },
1375
+ });
1376
+ ```
1377
+
1378
+ JSON path queries automatically handle numeric casting: when a numeric comparison operator (`gt`, `gte`, `lt`, `lte`, `between`) is used with a numeric value, the extracted text is safely cast to `numeric` via a CASE expression.
1379
+
1380
+ **Sorting with JSON paths:**
1381
+
1382
+ ```typescript
1383
+ const products = await repo.find({
1384
+ filter: {
1385
+ order: [
1386
+ 'createdAt DESC',
1387
+ 'metadata.priority DESC',
1388
+ 'data.nested.score ASC',
1389
+ ],
1390
+ },
1391
+ });
1392
+ ```
1393
+
1394
+ #### Field Selection
1395
+
1396
+ ```typescript
1397
+ // Array format -- include only these columns
1398
+ const users = await repo.find({
1399
+ filter: { fields: ['id', 'name', 'email'] },
1400
+ });
1401
+
1402
+ // Object format -- include/exclude
1403
+ const users = await repo.find({
1404
+ filter: { fields: { id: true, name: true, password: false } },
1405
+ });
1406
+ ```
1407
+
1408
+ #### Relation Inclusion
1409
+
1410
+ ```typescript
1411
+ // Simple inclusion
1412
+ const users = await repo.find({
1413
+ filter: {
1414
+ include: [{ relation: 'posts' }],
1415
+ },
1416
+ });
1417
+
1418
+ // With nested filter (scope)
1419
+ const users = await repo.find({
1420
+ filter: {
1421
+ include: [{
1422
+ relation: 'posts',
1423
+ scope: {
1424
+ where: { isPublished: true },
1425
+ limit: 5,
1426
+ order: ['createdAt DESC'],
1427
+ include: [{ relation: 'comments' }], // Nested relations
1428
+ },
1429
+ }],
1430
+ },
1431
+ });
1432
+
1433
+ // Skip default filter on a specific relation
1434
+ const users = await repo.find({
1435
+ filter: {
1436
+ include: [{
1437
+ relation: 'archivedPosts',
1438
+ shouldSkipDefaultFilter: true, // Show soft-deleted posts
1439
+ }],
1440
+ },
1441
+ });
1442
+ ```
1443
+
1444
+ Note: Relations are defined on the model via `static relations`. The FilterBuilder resolves relation configurations from the MetadataRegistry and applies hidden property exclusion and default filters to included relations automatically.
1445
+
1446
+ ### `shouldQueryRange` -- Range Object
1447
+
1448
+ When `shouldQueryRange: true` is passed to `find()`, the method runs both the data fetch and a count query in parallel, then returns:
1449
+
1450
+ ```typescript
1451
+ const result = await repo.find({
1452
+ filter: { where: { status: 'active' }, limit: 10, skip: 20 },
1453
+ options: { shouldQueryRange: true },
1454
+ });
1455
+
1456
+ // result.data = User[] (the 10 records)
1457
+ // result.range = { start: 20, end: 29, total: 150 }
1458
+ ```
1459
+
1460
+ This follows the HTTP Content-Range header standard. The ControllerFactory uses this to set `Content-Range: records 20-29/150` headers.
1461
+
1462
+ ### `shouldSkipDefaultFilter` -- When and Why
1463
+
1464
+ The `shouldSkipDefaultFilter` option bypasses the model's `defaultFilter`. Common use cases:
1465
+
1466
+ ```typescript
1467
+ // Admin panel showing all records (including soft-deleted)
1468
+ const allUsers = await repo.find({
1469
+ filter: {},
1470
+ options: { shouldSkipDefaultFilter: true },
1471
+ });
1472
+
1473
+ // Data migration or cleanup script
1474
+ const deletedUsers = await repo.find({
1475
+ filter: { where: { isDeleted: true } },
1476
+ options: { shouldSkipDefaultFilter: true },
1477
+ });
1478
+
1479
+ // Export all data for backup
1480
+ const everything = await repo.find({
1481
+ filter: {},
1482
+ options: { shouldSkipDefaultFilter: true, shouldQueryRange: true },
1483
+ });
1484
+ ```
1485
+
1486
+ ### ExtraOptions
1487
+
1488
+ All repository operations accept an `options` parameter:
1489
+
1490
+ ```typescript
1491
+ interface IExtraOptions {
1492
+ transaction?: ITransaction; // Use within a transaction
1493
+ shouldReturn?: boolean; // Return data after create/update/delete (default: true)
1494
+ shouldQueryRange?: boolean; // Return { data, range: { total, start, end } }
1495
+ shouldSkipDefaultFilter?: boolean; // Bypass model's default filter
1496
+ log?: { use: boolean; level?: TLogLevel }; // Enable operation logging
1497
+ }
1498
+ ```
1499
+
1500
+ ### Mixins
1501
+
1502
+ #### FieldsVisibilityMixin
1503
+
1504
+ Automatically excludes properties listed in `@model({ settings: { hiddenProperties } })` from all query results at the SQL level -- not post-processing:
1505
+
1506
+ ```typescript
1507
+ @model({
1508
+ settings: { hiddenProperties: ['password', 'secretToken'] },
1509
+ })
1510
+ export class User extends BaseEntity<typeof User.schema> { ... }
1511
+
1512
+ // All repository queries automatically exclude 'password' and 'secretToken'
1513
+ const user = await userRepo.findById({ id: 1 });
1514
+ // user.password === undefined (never selected from DB)
1515
+
1516
+ // Hidden fields are excluded from:
1517
+ // - find() / findOne() / findById() SELECT queries
1518
+ // - create() RETURNING clauses
1519
+ // - updateById() / updateAll() RETURNING clauses
1520
+ // - deleteById() / deleteAll() RETURNING clauses
1521
+ // - Included relation queries (applied recursively)
1522
+ ```
1523
+
1524
+ The mixin caches the visible property set for performance. It computes it once from the schema columns minus hidden properties.
1525
+
1526
+ #### DefaultFilterMixin
1527
+
1528
+ Automatically applies a default filter to all queries. Common use case -- soft delete:
1529
+
1530
+ ```typescript
1531
+ @model({
1532
+ settings: { defaultFilter: { where: { isDeleted: false } } },
1533
+ })
1534
+ export class User extends BaseEntity<typeof User.schema> { ... }
1535
+
1536
+ // All queries automatically add WHERE is_deleted = false
1537
+ const users = await userRepo.find({ filter: {} });
1538
+
1539
+ // The default filter merges with user-provided filters:
1540
+ const activeAdmins = await userRepo.find({
1541
+ filter: { where: { role: 'admin' } },
1542
+ });
1543
+ // SQL: WHERE is_deleted = false AND role = 'admin'
1544
+
1545
+ // Bypass when needed (e.g., admin panel showing all records)
1546
+ const allUsers = await userRepo.find({
1547
+ filter: {},
1548
+ options: { shouldSkipDefaultFilter: true },
1549
+ });
1550
+ ```
1551
+
1552
+ Merge strategy: `where` conditions are deep-merged (user values override matching keys); all other filter fields (`limit`, `order`, etc.) -- user completely replaces default if provided.
1553
+
1554
+ ### Dual Query API
1555
+
1556
+ Repositories use two internal query paths:
1557
+
1558
+ - **Core API** (`connector.select().from()`): 15--20% faster. Used for queries without relation inclusion and without explicit field selection. Builds SQL directly via Drizzle's core select/where/orderBy/limit/offset.
1559
+ - **Query API** (`connector.query.EntityName.findMany()`): Supports `include` for relation loading and field selection via `columns`. Used when the filter contains `include` or `fields`.
1560
+
1561
+ The repository automatically selects the appropriate API based on whether `include` or `fields` are present in the filter via `canUseCoreAPI()`. You do not need to think about this -- it is transparent.
1562
+
1563
+ ### UpdateBuilder -- JSON Path Updates
1564
+
1565
+ The `UpdateBuilder` transforms data containing dot-notation JSON path keys into chained `jsonb_set()` calls:
1566
+
1567
+ ```typescript
1568
+ // Input data:
1569
+ { name: 'John', 'metadata.settings.theme': 'dark', 'metadata.version': 2 }
1570
+
1571
+ // Generates SQL:
1572
+ // UPDATE users SET
1573
+ // name = 'John',
1574
+ // metadata = jsonb_set(jsonb_set("metadata", '{settings,theme}', '"dark"'::jsonb, true), '{version}', '2'::jsonb, true)
1575
+ // WHERE id = 42
1576
+ ```
1577
+
1578
+ Multiple path updates to the same JSON column are chained into a single expression. The `create_missing` parameter is set to `true`, so intermediate keys are created if they do not exist.
1579
+
1580
+ ---
1581
+
1582
+ ## Models
1583
+
1584
+ ### BaseEntity
1585
+
1586
+ All entities extend `BaseEntity` and define a static `schema` using Drizzle's `pgTable`:
1587
+
1588
+ ```typescript
1589
+ import { BaseEntity, model } from '@venizia/ignis';
1590
+ import { pgTable, text, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
1591
+
1592
+ @model({
1593
+ type: 'entity',
1594
+ settings: {
1595
+ hiddenProperties: ['password', 'secretToken'],
1596
+ defaultFilter: { where: { isDeleted: false } },
1597
+ },
1598
+ })
1599
+ export class User extends BaseEntity<typeof User.schema> {
1600
+ static override schema = pgTable('User', {
1601
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1602
+ ...generateTzColumnDefs({
1603
+ deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
1604
+ }),
1605
+ ...generateUserAuditColumnDefs(),
1606
+ username: text('username').notNull().unique(),
1607
+ email: text('email').notNull().unique(),
1608
+ password: text('password'),
1609
+ secretToken: text('secret_token'),
1610
+ role: text('role').default('user'),
1611
+ isDeleted: boolean('is_deleted').default(false),
1612
+ metadata: jsonb('metadata').$type<{ theme?: string; score?: number }>(),
1613
+ });
1614
+
1615
+ static override relations = () => [
1616
+ { name: 'posts', type: 'many', schema: Post.schema, metadata: { fields: [Post.schema.authorId], references: [User.schema.id] } },
1617
+ { name: 'profile', type: 'one', schema: Profile.schema, metadata: { fields: [Profile.schema.userId], references: [User.schema.id] } },
1618
+ ];
1619
+
1620
+ static TABLE_NAME = 'User';
1621
+ }
1622
+ ```
1623
+
1624
+ ### @model Decorator
1625
+
1626
+ ```typescript
1627
+ @model({
1628
+ type: 'entity', // Entity type identifier
1629
+ settings: {
1630
+ hiddenProperties: ['password'], // Excluded from all queries at SQL level
1631
+ defaultFilter: { where: { isDeleted: false } }, // Auto-applied to all queries
1632
+ },
1633
+ })
1634
+ ```
1635
+
1636
+ When `@model` is applied, it registers the class with the `MetadataRegistry`, extracting:
1637
+
1638
+ - The static `schema` (pgTable definition)
1639
+ - The static `relations` (relation configuration array or resolver function)
1640
+ - The model metadata (type, settings)
1641
+
1642
+ ### Schema Generation
1643
+
1644
+ `BaseEntity` provides `getSchema()` for Zod schema generation from the Drizzle table using `drizzle-zod`:
1645
+
1646
+ ```typescript
1647
+ const entity = new User();
1648
+
1649
+ entity.getSchema({ type: 'select' }); // Zod schema for SELECT results -- all fields as returned from DB
1650
+ entity.getSchema({ type: 'create' }); // Zod schema for INSERT data -- required/optional based on column definitions
1651
+ entity.getSchema({ type: 'update' }); // Zod schema for UPDATE data -- all fields optional (partial)
1652
+ ```
1653
+
1654
+ These schemas are used by `ControllerFactory` to auto-generate OpenAPI documentation. The schema factory is lazily initialized and shared across all BaseEntity instances for performance.
1655
+
1656
+ ### Static `relations` Definition
1657
+
1658
+ Relations are defined as a static property or function returning an array of `TRelationConfig`:
1659
+
1660
+ ```typescript
1661
+ static override relations = () => [
1662
+ {
1663
+ name: 'posts',
1664
+ type: 'many', // RelationTypes.MANY
1665
+ schema: Post.schema,
1666
+ metadata: {
1667
+ fields: [Post.schema.authorId],
1668
+ references: [User.schema.id],
1669
+ },
1670
+ },
1671
+ {
1672
+ name: 'profile',
1673
+ type: 'one', // RelationTypes.ONE
1674
+ schema: Profile.schema,
1675
+ metadata: {
1676
+ fields: [Profile.schema.userId],
1677
+ references: [User.schema.id],
1678
+ },
1679
+ },
1680
+ ];
1681
+ ```
1682
+
1683
+ These relations are used by the `FilterBuilder` when processing `include` in filters, and by the DataSource's `discoverSchema()` to build Drizzle relation definitions.
1684
+
1685
+ ### Enrichers -- Complete Reference
1686
+
1687
+ Column definition helpers that add common patterns to your table schemas.
1688
+
1689
+ #### `generateIdColumnDefs()` -- ID Column
1690
+
1691
+ ```typescript
1692
+ // Auto-incrementing integer ID (default)
1693
+ ...generateIdColumnDefs()
1694
+ // Column: id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY
1695
+
1696
+ // Auto-incrementing integer ID (explicit)
1697
+ ...generateIdColumnDefs({ id: { dataType: 'number' } })
1698
+
1699
+ // UUID string ID (uses crypto.randomUUID() by default)
1700
+ ...generateIdColumnDefs({ id: { dataType: 'string' } })
1701
+
1702
+ // Custom string ID generator
1703
+ ...generateIdColumnDefs({
1704
+ id: { dataType: 'string', generator: () => mySnowflakeGenerator() },
1705
+ })
1706
+
1707
+ // BigInt ID (as JavaScript number)
1708
+ ...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'number' } })
1709
+
1710
+ // BigInt ID (as JavaScript bigint)
1711
+ ...generateIdColumnDefs({ id: { dataType: 'big-number', numberMode: 'bigint' } })
1712
+
1713
+ // With custom sequence options
1714
+ ...generateIdColumnDefs({
1715
+ id: { dataType: 'number', sequenceOptions: { startWith: 1000, increment: 1 } },
1716
+ })
1717
+ ```
1718
+
1719
+ #### `generateTzColumnDefs()` -- Timestamps
1720
+
1721
+ ```typescript
1722
+ // Default: createdAt + modifiedAt (with timezone, defaultNow)
1723
+ ...generateTzColumnDefs()
1724
+ // Columns: created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
1725
+ // modified_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL (auto-updates via $onUpdate)
1726
+
1727
+ // With deletedAt for soft delete
1728
+ ...generateTzColumnDefs({
1729
+ deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
1730
+ })
1731
+ // Adds: deleted_at TIMESTAMP WITH TIME ZONE (nullable)
1732
+
1733
+ // Disable modifiedAt
1734
+ ...generateTzColumnDefs({
1735
+ modified: { enable: false },
1736
+ })
1737
+ // Only creates: created_at
1738
+
1739
+ // Custom column names
1740
+ ...generateTzColumnDefs({
1741
+ created: { columnName: 'date_created', withTimezone: false },
1742
+ modified: { enable: true, columnName: 'date_modified', withTimezone: false },
1743
+ })
1744
+ ```
1745
+
1746
+ #### `generateUserAuditColumnDefs()` -- Audit Trail
1747
+
1748
+ Automatically populates `createdBy` and `modifiedBy` from the authenticated user in the request context:
1749
+
1750
+ ```typescript
1751
+ // Default: integer columns
1752
+ ...generateUserAuditColumnDefs()
1753
+ // Columns: created_by integer, modified_by integer
1754
+ // Auto-populated from context.get('audit.user.id') on create/update
1755
+
1756
+ // String IDs
1757
+ ...generateUserAuditColumnDefs({
1758
+ created: { dataType: 'string', columnName: 'created_by', allowAnonymous: true },
1759
+ modified: { dataType: 'string', columnName: 'modified_by', allowAnonymous: true },
1760
+ })
1761
+ ```
1762
+
1763
+ When `allowAnonymous: false` (default is `true`), an error is thrown if there is no authenticated user in the request context. This is useful for columns that must always have an audit trail.
1764
+
1765
+ **Important:** `createdBy` is only set on insert (`$default`). `modifiedBy` is set on both insert and update (`$default` + `$onUpdate`).
1766
+
1767
+ #### `generatePrincipalColumnDefs()` -- Polymorphic Relations
1768
+
1769
+ ```typescript
1770
+ // Default discriminator name ('principal')
1771
+ ...generatePrincipalColumnDefs({ polymorphicIdType: 'number' })
1772
+ // Columns: principal_id integer NOT NULL, principal_type text
1773
+
1774
+ // Custom discriminator
1775
+ ...generatePrincipalColumnDefs({
1776
+ discriminator: 'owner',
1777
+ polymorphicIdType: 'string',
1778
+ defaultPolymorphic: 'user',
1779
+ })
1780
+ // Columns: owner_id text NOT NULL, owner_type text DEFAULT 'user'
1781
+ ```
1782
+
1783
+ #### `generateDataTypeColumnDefs()` -- Multi-type Value Columns
1784
+
1785
+ For storing heterogeneous values (useful for key-value stores, settings tables):
1786
+
1787
+ ```typescript
1788
+ ...generateDataTypeColumnDefs()
1789
+ // Columns:
1790
+ // data_type text -- type discriminator
1791
+ // n_value double precision -- numeric values
1792
+ // t_value text -- text values
1793
+ // b_value bytea -- binary values
1794
+ // j_value jsonb -- JSON values
1795
+ // bo_value boolean -- boolean values
1796
+
1797
+ // With defaults
1798
+ ...generateDataTypeColumnDefs({
1799
+ defaultValue: { dataType: 'text', tValue: 'default' },
1800
+ })
1801
+ ```
1802
+
1803
+ ---
1804
+
1805
+ ## DataSources
1806
+
1807
+ ### BaseDataSource
1808
+
1809
+ DataSources manage database connections and provide Drizzle connectors:
1810
+
1811
+ ```typescript
1812
+ abstract class BaseDataSource<Settings, Schema> extends AbstractDataSource {
1813
+ // Implemented by subclass
1814
+ abstract configure(): ValueOrPromise<void>;
1815
+ abstract getConnectionString(): ValueOrPromise<string>;
1816
+
1817
+ // Auto-discovers schema from @repository bindings
1818
+ getSchema(): Schema;
1819
+
1820
+ // Check if any repositories reference this datasource
1821
+ hasDiscoverableModels(): boolean;
1822
+
1823
+ // Transaction support
1824
+ beginTransaction(opts?: ITransactionOptions): Promise<ITransaction>;
1825
+ }
1826
+ ```
1827
+
1828
+ ### Complete DataSource Configuration
1829
+
1830
+ ```typescript
1831
+ @datasource({ driver: 'node-postgres' })
1832
+ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
1833
+ constructor() {
1834
+ super({
1835
+ name: PostgresDataSource.name,
1836
+ config: {
1837
+ host: process.env.DB_HOST!,
1838
+ port: +(process.env.DB_PORT ?? 5432),
1839
+ database: process.env.DB_NAME!,
1840
+ user: process.env.DB_USER!,
1841
+ password: process.env.DB_PASSWORD!,
1842
+ // pg Pool options:
1843
+ max: 20, // max pool connections
1844
+ idleTimeoutMillis: 30000, // close idle clients after 30s
1845
+ connectionTimeoutMillis: 5000, // timeout connecting after 5s
1846
+ },
1847
+ });
1848
+ }
1849
+
1850
+ override configure(): ValueOrPromise<void> {
1851
+ const schema = this.getSchema(); // Auto-discovers from repositories
1852
+ this.pool = new Pool(this.settings);
1853
+ this.connector = drizzle({ client: this.pool, schema });
1854
+ }
1855
+
1856
+ override getConnectionString() {
1857
+ const { host, port, user, password, database } = this.settings;
1858
+ return `postgresql://${user}:${password}@${host}:${port}/${database}`;
1859
+ }
1860
+ }
1861
+ ```
1862
+
1863
+ ### Schema Auto-Discovery
1864
+
1865
+ DataSources do not need manual schema configuration. When `getSchema()` is called during `configure()`, the DataSource:
1866
+
1867
+ 1. Queries the `MetadataRegistry` for all `@repository` bindings that reference this DataSource class.
1868
+ 2. For each binding, extracts the model's static `schema` (pgTable) and `relations`.
1869
+ 3. Combines them into a single schema object: `{ User: User.schema, Post: Post.schema, ...relations }`.
1870
+ 4. Caches the result.
1871
+
1872
+ This means adding a new model + repository automatically makes it available to all queries without touching the DataSource code.
1873
+
1874
+ ### Transaction Deep Dive
1875
+
1876
+ ```typescript
1877
+ const tx = await dataSource.beginTransaction({
1878
+ isolationLevel: 'READ COMMITTED', // default
1879
+ });
1880
+
1881
+ try {
1882
+ await userRepo.create({
1883
+ data: { username: 'john', email: 'john@example.com' },
1884
+ options: { transaction: tx },
1885
+ });
1886
+
1887
+ await auditRepo.create({
1888
+ data: { action: 'user_created', userId: newUser.id },
1889
+ options: { transaction: tx },
1890
+ });
1891
+
1892
+ await tx.commit();
1893
+ } catch (error) {
1894
+ await tx.rollback();
1895
+ throw error;
1896
+ }
1897
+ ```
1898
+
1899
+ **How transactions work internally:**
1900
+
1901
+ 1. `beginTransaction()` acquires a `PoolClient` from the `pg` Pool.
1902
+ 2. Executes `BEGIN TRANSACTION ISOLATION LEVEL <level>` on that client.
1903
+ 3. Creates a new Drizzle connector using that dedicated client.
1904
+ 4. Returns an `ITransaction` object with `connector`, `commit()`, `rollback()`, and `isActive`.
1905
+ 5. When `{ transaction: tx }` is passed to a repository method, `resolveConnector()` returns the transaction's connector instead of the default DataSource connector.
1906
+ 6. `commit()` runs `COMMIT`, sets `isActive = false`, and releases the client back to the pool.
1907
+ 7. `rollback()` runs `ROLLBACK`, sets `isActive = false`, and releases the client.
1908
+ 8. Attempting to use a committed/rolled-back transaction throws an error.
1909
+
1910
+ #### Isolation Levels
1911
+
1912
+ | Level | Constant | When to Use |
1913
+ | --- | --- | --- |
1914
+ | `READ COMMITTED` | `IsolationLevels.READ_COMMITTED` | Default. Each statement sees only data committed before it began. Sufficient for most CRUD operations. |
1915
+ | `REPEATABLE READ` | `IsolationLevels.REPEATABLE_READ` | All statements in the transaction see a snapshot from the start. Use for consistent reads across multiple queries (e.g., generating reports). |
1916
+ | `SERIALIZABLE` | `IsolationLevels.SERIALIZABLE` | Strictest. Transactions behave as if they ran sequentially. Use for financial operations or inventory management where absolute consistency is required. May cause serialization failures requiring retry. |
1917
+
1918
+ #### Connection Release
1919
+
1920
+ Connections are always released back to the pool in the `finally` block of both `commit()` and `rollback()`. This means even if the commit or rollback SQL fails, the connection is still released, preventing pool exhaustion.
1921
+
1922
+ ---
1923
+
1924
+ ## Services
1925
+
1926
+ Services encapsulate business logic and are registered in the DI container:
1927
+
1928
+ ```typescript
1929
+ import { BaseService, inject } from '@venizia/ignis';
1930
+
1931
+ export class UserService extends BaseService {
1932
+ constructor(
1933
+ @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
1934
+ @inject({ key: 'repositories.AuditRepository' }) private auditRepo: AuditRepository,
1935
+ ) {
1936
+ super({ scope: UserService.name });
1937
+ }
1938
+
1939
+ async createUser(data: CreateUserInput) {
1940
+ const tx = await this.userRepo.dataSource.beginTransaction();
1941
+ try {
1942
+ const { data: user } = await this.userRepo.create({
1943
+ data,
1944
+ options: { transaction: tx },
1945
+ });
1946
+
1947
+ await this.auditRepo.create({
1948
+ data: { action: 'user_created', userId: user.id },
1949
+ options: { transaction: tx },
1950
+ });
1951
+
1952
+ await tx.commit();
1953
+ return user;
1954
+ } catch (error) {
1955
+ await tx.rollback();
1956
+ throw error;
1957
+ }
1958
+ }
1959
+
1960
+ async deactivateInactiveUsers() {
1961
+ const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // 90 days
1962
+ const { count } = await this.userRepo.updateBy({
1963
+ data: { status: 'inactive' },
1964
+ where: { lastLoginAt: { lt: cutoff }, status: 'active' },
1965
+ });
1966
+ this.logger.info('Deactivated %d inactive users', count);
1967
+ return { count };
1968
+ }
1969
+ }
1970
+ ```
1971
+
1972
+ Register in the application:
1973
+
1974
+ ```typescript
1975
+ this.service(UserService);
1976
+ ```
1977
+
1978
+ `BaseService` extends `BaseHelper`, providing a scoped logger instance (`this.logger`).
1979
+
1980
+ ---
1981
+
1982
+ ## Components
1983
+
1984
+ Components are self-contained modules that register controllers, services, bindings, and middleware. They extend `BaseComponent` and participate in the application lifecycle.
1985
+
1986
+ ### Built-in Components
1987
+
1988
+ | Component | Import | Description |
1989
+ | --- | --- | --- |
1990
+ | **HealthCheckComponent** | `@venizia/ignis` | Health check endpoints (`GET /health`, `/health/live`, `/health/ready`) |
1991
+ | **SwaggerComponent** | `@venizia/ignis` | OpenAPI documentation with Swagger UI or Scalar UI |
1992
+ | **AuthenticateComponent** | `@venizia/ignis` | JWT + Basic authentication strategies, token services, auth middleware |
1993
+ | **AuthorizeComponent** | `@venizia/ignis` | Casbin-based RBAC authorization with enforcers |
1994
+ | **RequestTrackerComponent** | `@venizia/ignis` | `x-request-id` header injection, request body parsing |
1995
+ | **StaticAssetComponent** | `@venizia/ignis` | File upload/download CRUD with MinIO or disk storage |
1996
+ | **MailComponent** | `@venizia/ignis/mail` | Email sending via Nodemailer/Mailgun with Direct/BullMQ/InternalQueue executors |
1997
+ | **SocketIOComponent** | `@venizia/ignis/socket-io` | Socket.IO server with Redis adapter for horizontal scaling |
1998
+ | **WebSocketComponent** | `@venizia/ignis` | Native WebSocket support (Bun runtime) |
1999
+
2000
+ ### Health Check Component
2001
+
2002
+ ```typescript
2003
+ import { HealthCheckComponent, HealthCheckBindingKeys, IHealthCheckOptions } from '@venizia/ignis';
2004
+
2005
+ // Optional: customize path (default: /health)
2006
+ this.bind<IHealthCheckOptions>({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS }).toValue({
2007
+ restOptions: { path: '/health-check' },
2008
+ });
2009
+ this.component(HealthCheckComponent);
2010
+ ```
2011
+
2012
+ **Endpoints:**
2013
+
2014
+ | Endpoint | Description |
2015
+ | --- | --- |
2016
+ | `GET /health` | Basic health check -- returns `{ status: 'ok', uptime, timestamp }` |
2017
+ | `GET /health/live` | Liveness probe -- returns 200 if server is running |
2018
+ | `GET /health/ready` | Readiness probe -- returns 200 if server is ready to accept traffic |
2019
+
2020
+ ### Swagger / OpenAPI Component
2021
+
2022
+ ```typescript
2023
+ import { SwaggerComponent, SwaggerBindingKeys, ISwaggerOptions } from '@venizia/ignis';
2024
+
2025
+ this.bind<ISwaggerOptions>({ key: SwaggerBindingKeys.SWAGGER_OPTIONS }).toValue({
2026
+ restOptions: {
2027
+ base: { path: '/doc' },
2028
+ doc: { path: '/openapi.json' },
2029
+ ui: { path: '/explorer', type: 'scalar' }, // 'scalar' or 'swagger'
2030
+ },
2031
+ explorer: { openapi: '3.0.0' },
2032
+ });
2033
+ this.component(SwaggerComponent);
2034
+ ```
2035
+
2036
+ The component auto-populates `info` from `getAppInfo()` and registers JWT/Basic security schemes in the OpenAPI registry. When `type: 'scalar'` is used, it serves Scalar UI; when `type: 'swagger'`, it serves Swagger UI.
2037
+
2038
+ **Endpoints:**
2039
+
2040
+ | Endpoint | Description |
2041
+ | --- | --- |
2042
+ | `GET /doc/openapi.json` | Raw OpenAPI spec in JSON |
2043
+ | `GET /doc/explorer` | Interactive API explorer (Scalar or Swagger UI) |
2044
+
2045
+ ### Authentication Component
2046
+
2047
+ Supports JWT and Basic authentication strategies with encrypted JWT payloads:
2048
+
2049
+ ```typescript
2050
+ import {
2051
+ AuthenticateComponent, AuthenticateBindingKeys,
2052
+ Authentication, AuthenticationStrategyRegistry,
2053
+ JWTAuthenticationStrategy, BasicAuthenticationStrategy,
2054
+ IJWTTokenServiceOptions, IBasicTokenServiceOptions,
2055
+ TAuthenticationRestOptions,
2056
+ } from '@venizia/ignis';
2057
+
2058
+ // 1. Configure JWT options
2059
+ this.bind<IJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
2060
+ jwtSecret: process.env.JWT_SECRET!, // Secret for signing JWTs
2061
+ applicationSecret: process.env.APP_SECRET!, // Secret for AES encrypting payload fields
2062
+ headerAlgorithm: 'HS256', // JWT signing algorithm (default: HS256)
2063
+ aesAlgorithm: 'aes-256-cbc', // Payload encryption algorithm (default: aes-256-cbc)
2064
+ getTokenExpiresFn: () => 86400, // Token expiration in seconds (24 hours)
2065
+ });
2066
+
2067
+ // 2. Configure Basic auth options (optional)
2068
+ this.bind<IBasicTokenServiceOptions>({ key: AuthenticateBindingKeys.BASIC_OPTIONS }).toValue({
2069
+ verifyCredentials: async ({ credentials, context }) => {
2070
+ const user = await userRepo.findOne({
2071
+ filter: { where: { username: credentials.username } },
2072
+ });
2073
+ if (user && await verifyPassword(credentials.password, user.password)) {
2074
+ return { userId: user.id, roles: user.roles };
2075
+ }
2076
+ return null;
2077
+ },
2078
+ });
2079
+
2080
+ // 3. Optionally enable auth controller (sign-in, sign-up, change-password)
2081
+ this.bind<TAuthenticationRestOptions>({ key: AuthenticateBindingKeys.REST_OPTIONS }).toValue({
2082
+ useAuthController: true,
2083
+ controllerOpts: {
2084
+ restPath: '/auth',
2085
+ payload: {
2086
+ signIn: { request: { schema: SignInSchema }, response: { schema: TokenSchema } },
2087
+ signUp: { request: { schema: SignUpSchema }, response: { schema: UserSchema } },
2088
+ },
2089
+ },
2090
+ });
2091
+
2092
+ // 4. Register the component
2093
+ this.component(AuthenticateComponent);
2094
+
2095
+ // 5. Register strategies
2096
+ AuthenticationStrategyRegistry.getInstance().register({
2097
+ container: this,
2098
+ strategies: [
2099
+ { name: Authentication.STRATEGY_JWT, strategy: JWTAuthenticationStrategy },
2100
+ { name: Authentication.STRATEGY_BASIC, strategy: BasicAuthenticationStrategy },
2101
+ ],
2102
+ });
2103
+ ```
2104
+
2105
+ #### JWT Token Service API
2106
+
2107
+ ```typescript
2108
+ // Generate a token
2109
+ const token = await jwtTokenService.generate({
2110
+ payload: {
2111
+ userId: user.id,
2112
+ roles: [{ id: 1, identifier: 'admin', priority: 1 }],
2113
+ email: user.email,
2114
+ },
2115
+ });
2116
+
2117
+ // Verify a token
2118
+ const payload = await jwtTokenService.verify({
2119
+ type: 'Bearer',
2120
+ token: 'eyJhbGciOiJ...',
2121
+ });
2122
+ // payload = { userId: '...', roles: [...], email: '...' }
2123
+ ```
2124
+
2125
+ JWT payloads are AES-encrypted: all non-standard JWT fields (userId, roles, email, etc.) are encrypted with the `applicationSecret` before signing. This prevents payload inspection without the application secret.
2126
+
2127
+ #### Getting Current User from Context
2128
+
2129
+ After authentication middleware runs, the current user is available via context variables:
2130
+
2131
+ ```typescript
2132
+ @get({
2133
+ configs: {
2134
+ path: '/profile',
2135
+ authenticate: { strategies: ['jwt'], mode: 'any' },
2136
+ responses: jsonResponse({ schema: UserProfileSchema }),
2137
+ },
2138
+ })
2139
+ async getProfile(context: TRouteContext) {
2140
+ const user = context.get('auth.current.user');
2141
+ // user = { userId: '...', roles: [...], ... }
2142
+
2143
+ const auditId = context.get('audit.user.id');
2144
+ // auditId = user.userId (set automatically for UserAuditEnricher)
2145
+
2146
+ // ...
2147
+ }
2148
+ ```
2149
+
2150
+ #### Authentication Modes
2151
+
2152
+ | Mode | Behavior |
2153
+ | --- | --- |
2154
+ | `any` (default) | At least one strategy must succeed. Tries each strategy in order; first success wins. |
2155
+ | `all` | All specified strategies must succeed. All are tried; all must pass. |
2156
+
2157
+ #### Route-Level vs Controller-Level Authentication
2158
+
2159
+ ```typescript
2160
+ // Controller-level (via ControllerFactory)
2161
+ const UserController = ControllerFactory.defineCrudController({
2162
+ authenticate: { strategies: ['jwt'] }, // Applied to ALL routes
2163
+ routes: {
2164
+ find: { authenticate: { skip: true } }, // Override: public
2165
+ },
2166
+ });
2167
+
2168
+ // Route-level (via decorators)
2169
+ @controller({ path: '/products' })
2170
+ class ProductController extends BaseController {
2171
+ @get({
2172
+ configs: {
2173
+ path: '/',
2174
+ // No authenticate -- public route
2175
+ responses: jsonResponse({ schema: z.array(ProductSchema) }),
2176
+ },
2177
+ })
2178
+ async list(c: TRouteContext) { /* ... */ }
2179
+
2180
+ @post({
2181
+ configs: {
2182
+ path: '/',
2183
+ authenticate: { strategies: ['jwt'] }, // Protected route
2184
+ responses: jsonResponse({ schema: ProductSchema }),
2185
+ },
2186
+ })
2187
+ async create(c: TRouteContext) { /* ... */ }
2188
+ }
2189
+ ```
2190
+
2191
+ ### Authorization Component
2192
+
2193
+ Casbin-based RBAC authorization:
2194
+
2195
+ ```typescript
2196
+ import {
2197
+ AuthorizeComponent, AuthorizeBindingKeys, IAuthorizeOptions,
2198
+ CasbinAuthorizationEnforcer,
2199
+ } from '@venizia/ignis';
2200
+
2201
+ this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
2202
+ enforcer: CasbinAuthorizationEnforcer,
2203
+ alwaysAllowRoles: ['superadmin'], // Roles that bypass all authorization
2204
+ casbinOptions: {
2205
+ model: '/path/to/model.conf', // Casbin model file
2206
+ adapter: myAdapter, // e.g., FileAdapter, PostgresAdapter
2207
+ },
2208
+ });
2209
+ this.component(AuthorizeComponent);
2210
+ ```
2211
+
2212
+ Use on routes:
2213
+
2214
+ ```typescript
2215
+ @get({
2216
+ configs: {
2217
+ path: '/admin/users',
2218
+ authenticate: { strategies: ['jwt'] },
2219
+ authorize: { action: 'read', resource: 'User' },
2220
+ responses: jsonResponse({ schema: z.array(UserSchema) }),
2221
+ },
2222
+ })
2223
+ async listUsers(context: TRouteContext) { /* ... */ }
2224
+
2225
+ // Multiple authorization specs (all must pass)
2226
+ @del({
2227
+ configs: {
2228
+ path: '/admin/users/{id}',
2229
+ authenticate: { strategies: ['jwt'] },
2230
+ authorize: [
2231
+ { action: 'delete', resource: 'User' },
2232
+ { action: 'manage', resource: 'Admin' },
2233
+ ],
2234
+ // ...
2235
+ },
2236
+ })
2237
+ async deleteUser(context: TRouteContext) { /* ... */ }
2238
+ ```
2239
+
2240
+ ### Static Asset Component
2241
+
2242
+ File upload/download with MinIO or disk storage:
2243
+
2244
+ ```typescript
2245
+ import {
2246
+ StaticAssetComponent, StaticAssetComponentBindingKeys,
2247
+ StaticAssetStorageTypes, TStaticAssetsComponentOptions,
2248
+ } from '@venizia/ignis';
2249
+ import { MinioHelper, DiskHelper } from '@venizia/ignis-helpers';
2250
+
2251
+ // MinIO backend
2252
+ this.bind<TStaticAssetsComponentOptions>({
2253
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
2254
+ }).toValue({
2255
+ staticAsset: {
2256
+ controller: { name: 'AssetController', basePath: '/assets' },
2257
+ storage: StaticAssetStorageTypes.MINIO,
2258
+ helper: new MinioHelper({
2259
+ endPoint: 'localhost',
2260
+ port: 9000,
2261
+ accessKey: 'minioadmin',
2262
+ secretKey: 'minioadmin',
2263
+ useSSL: false,
2264
+ }),
2265
+ },
2266
+ });
2267
+ this.component(StaticAssetComponent);
2268
+
2269
+ // Disk backend
2270
+ this.bind<TStaticAssetsComponentOptions>({
2271
+ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS,
2272
+ }).toValue({
2273
+ staticAsset: {
2274
+ controller: { name: 'AssetController', basePath: '/assets' },
2275
+ storage: StaticAssetStorageTypes.DISK,
2276
+ helper: new DiskHelper({ basePath: './uploads' }),
2277
+ },
2278
+ });
2279
+ ```
2280
+
2281
+ ### Mail Component
2282
+
2283
+ ```typescript
2284
+ import { MailComponent } from '@venizia/ignis/mail';
2285
+ ```
2286
+
2287
+ Supports:
2288
+
2289
+ - **Transporters**: Nodemailer (SMTP), Mailgun (API)
2290
+ - **Executors**: Direct (synchronous send), BullMQ (background queue with Redis), InternalQueue (in-memory queue)
2291
+
2292
+ ### Socket.IO Component
2293
+
2294
+ ```typescript
2295
+ import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
2296
+
2297
+ this.bind({ key: SocketIOBindingKeys.OPTIONS }).toValue({
2298
+ cors: { origin: '*' },
2299
+ adapter: redisAdapter, // Optional: Redis adapter for scaling
2300
+ });
2301
+ this.component(SocketIOComponent);
2302
+ ```
2303
+
2304
+ Provides Socket.IO server integration with:
2305
+
2306
+ - Bun and Node.js runtime handlers (auto-detected)
2307
+ - Redis adapter support for horizontal scaling across multiple server instances
2308
+
2309
+ ---
2310
+
2311
+ ## Request Context
2312
+
2313
+ Access the Hono request context from anywhere using `useRequestContext()`:
2314
+
2315
+ ```typescript
2316
+ import { useRequestContext } from '@venizia/ignis';
2317
+
2318
+ function getCurrentRequestId(): string | undefined {
2319
+ const context = useRequestContext();
2320
+ return context?.get('requestId');
2321
+ }
2322
+
2323
+ function getCurrentUser(): IAuthUser | undefined {
2324
+ const context = useRequestContext();
2325
+ return context?.get('auth.current.user');
2326
+ }
2327
+ ```
2328
+
2329
+ This uses Hono's `contextStorage()` which stores the context in `AsyncLocalStorage`. It is available anywhere within the request lifecycle -- services, repositories, helpers, enrichers, etc.
2330
+
2331
+ **Note:** Requires `asyncContext.enable: true` in application config (the default).
2332
+
2333
+ ---
2334
+
2335
+ ## Middleware System
2336
+
2337
+ ### Registering Custom Middleware
2338
+
2339
+ Add middleware in `setupMiddlewares()` -- these run on every request:
2340
+
2341
+ ```typescript
2342
+ setupMiddlewares(): ValueOrPromise<void> {
2343
+ const server = this.getServer();
2344
+
2345
+ // CORS
2346
+ server.use('*', cors({
2347
+ origin: ['https://myapp.com', 'https://admin.myapp.com'],
2348
+ allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
2349
+ allowHeaders: ['Content-Type', 'Authorization'],
2350
+ credentials: true,
2351
+ }));
2352
+
2353
+ // Body size limit
2354
+ server.use('*', bodyLimit({ maxSize: 50 * 1024 * 1024 })); // 50MB
2355
+
2356
+ // Custom logging middleware
2357
+ server.use('*', async (c, next) => {
2358
+ const start = Date.now();
2359
+ await next();
2360
+ const duration = Date.now() - start;
2361
+ console.log(`${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`);
2362
+ });
2363
+
2364
+ // Rate limiting on specific paths
2365
+ server.use('/api/auth/*', rateLimiter({
2366
+ windowMs: 15 * 60 * 1000, // 15 minutes
2367
+ limit: 100,
2368
+ }));
2369
+ }
2370
+ ```
2371
+
2372
+ ### Default Middleware Stack (registered automatically)
2373
+
2374
+ 1. `appErrorHandler` -- Global error handler
2375
+ 2. `contextStorage` -- Async context for `useRequestContext()`
2376
+ 3. `notFoundHandler` -- Structured 404 responses
2377
+ 4. `RequestTrackerComponent` -- Request ID injection + body parsing
2378
+ 5. `emojiFavicon` -- Favicon handler
2379
+
2380
+ ---
2381
+
2382
+ ## Error Handling
2383
+
2384
+ ### Error Propagation
2385
+
2386
+ Errors propagate through the layer stack and are caught by the global `appErrorHandler`:
2387
+
2388
+ ```
2389
+ Controller throws -> appErrorHandler catches -> JSON error response
2390
+ Service throws -> Controller doesn't catch -> appErrorHandler catches
2391
+ Repository throws -> Service doesn't catch -> Controller doesn't catch -> appErrorHandler
2392
+ ```
2393
+
2394
+ ### Error Response Format
2395
+
2396
+ ```json
2397
+ {
2398
+ "message": "Error description",
2399
+ "statusCode": 500,
2400
+ "requestId": "abc-123-def",
2401
+ "details": {
2402
+ "url": "http://localhost:3000/api/users",
2403
+ "path": "/api/users",
2404
+ "stack": "Error: ...\n at ...",
2405
+ "cause": { "code": "23505", "detail": "Key (email)=(john@example.com) already exists." }
2406
+ }
2407
+ }
2408
+ ```
2409
+
2410
+ In production (`NODE_ENV=production`), `stack` and `cause` are stripped from responses.
2411
+
2412
+ ### PostgreSQL Constraint Errors
2413
+
2414
+ The error handler automatically recognizes PostgreSQL constraint violations and returns HTTP 400 instead of 500:
2415
+
2416
+ | Error Code | Description |
2417
+ | --- | --- |
2418
+ | `23505` | Unique constraint violation |
2419
+ | `23503` | Foreign key constraint violation |
2420
+ | `23502` | Not null constraint violation |
2421
+ | `23514` | Check constraint violation |
2422
+ | `23P01` | Exclusion constraint violation |
2423
+ | `22P02` | Invalid text representation |
2424
+ | `22003` | Numeric value out of range |
2425
+ | `22001` | String data too long |
2426
+
2427
+ ### Throwing Application Errors
2428
+
2429
+ Use `getError()` from helpers to throw errors with specific status codes:
2430
+
2431
+ ```typescript
2432
+ import { getError, HTTP } from '@venizia/ignis-helpers';
2433
+
2434
+ throw getError({
2435
+ statusCode: HTTP.ResultCodes.RS_4.NotFound,
2436
+ message: 'User not found',
2437
+ });
2438
+
2439
+ throw getError({
2440
+ statusCode: HTTP.ResultCodes.RS_4.Forbidden,
2441
+ message: 'Insufficient permissions',
2442
+ });
2443
+ ```
2444
+
2445
+ ---
2446
+
2447
+ ## Decorators Reference
2448
+
2449
+ | Decorator | Target | Parameters | Description |
2450
+ | --- | --- | --- | --- |
2451
+ | `@model({ type?, settings? })` | Class | `type`: entity type string; `settings.hiddenProperties`: string[]; `settings.defaultFilter`: TFilter | Register entity model with hidden properties and default filters |
2452
+ | `@datasource({ driver?, autoDiscovery? })` | Class | `driver`: `'node-postgres'`; `autoDiscovery`: boolean (default true) | Register datasource with driver configuration |
2453
+ | `@repository({ model, dataSource })` | Class | `model`: entity class; `dataSource`: datasource class | Bind repository to model and datasource; auto-injects datasource at param[0] |
2454
+ | `@controller({ path, authenticate? })` | Class | `path`: base path string; `authenticate`: `{ strategies, mode }` | Register controller with base path and optional default auth |
2455
+ | `@get({ configs })` | Method | Full route config (path, request, responses, authenticate, authorize, middleware) | Define GET route |
2456
+ | `@post({ configs })` | Method | Same as `@get` | Define POST route |
2457
+ | `@put({ configs })` | Method | Same as `@get` | Define PUT route |
2458
+ | `@patch({ configs })` | Method | Same as `@get` | Define PATCH route |
2459
+ | `@del({ configs })` | Method | Same as `@get` | Define DELETE route |
2460
+ | `@api({ configs })` | Method | Same as `@get` + `method` field | Define route with explicit HTTP method |
2461
+ | `@inject({ key, isOptional? })` | Constructor param / Property | `key`: binding key string or symbol; `isOptional`: boolean (default false) | Inject dependency from IoC container |
2462
+ | `@injectable({ scope?, tags? })` | Class | `scope`: `'singleton'` or `'transient'`; `tags`: string[] | Mark class as injectable with scope and tags |
2463
+
2464
+ ---
2465
+
2466
+ ## Response Helpers
2467
+
2468
+ Utility functions for building OpenAPI-compliant response and request schemas:
2469
+
2470
+ ```typescript
2471
+ import { jsonContent, jsonResponse, htmlResponse, idParamsSchema } from '@venizia/ignis';
2472
+
2473
+ // JSON request body
2474
+ jsonContent({
2475
+ schema: z.object({ name: z.string(), email: z.string() }),
2476
+ description: 'User creation payload',
2477
+ });
2478
+ // => { description, content: { 'application/json': { schema } } }
2479
+
2480
+ // JSON response with automatic error fallback
2481
+ jsonResponse({
2482
+ schema: z.object({ id: z.number(), name: z.string() }),
2483
+ description: 'User object',
2484
+ headers: {
2485
+ 'x-request-id': { description: 'Request ID', schema: { type: 'string' } },
2486
+ },
2487
+ });
2488
+ // => { 200: { ... }, '4xx | 5xx': { ... ErrorSchema ... } }
2489
+
2490
+ // HTML response
2491
+ htmlResponse({ description: 'Rendered page' });
2492
+ // => { 200: { content: { 'text/html': { schema } } }, '4xx | 5xx': { ... } }
2493
+
2494
+ // Path parameter schema
2495
+ idParamsSchema({ idType: 'number' });
2496
+ // => z.object({ id: z.number() })
2497
+
2498
+ idParamsSchema({ idType: 'string' });
2499
+ // => z.object({ id: z.string() })
2500
+ ```
2501
+
2502
+ ---
2503
+
2504
+ ## Real-World Patterns
2505
+
2506
+ ### Complete User CRUD with Auth, Validation, Soft Delete, Pagination
2507
+
2508
+ ```typescript
2509
+ // models/user.model.ts
2510
+ @model({
2511
+ type: 'entity',
2512
+ settings: {
2513
+ hiddenProperties: ['password'],
2514
+ defaultFilter: { where: { isDeleted: false } },
2515
+ },
2516
+ })
2517
+ export class User extends BaseEntity<typeof User.schema> {
2518
+ static override schema = pgTable('User', {
2519
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
2520
+ ...generateTzColumnDefs({
2521
+ deleted: { enable: true, columnName: 'deleted_at', withTimezone: true },
2522
+ }),
2523
+ ...generateUserAuditColumnDefs({
2524
+ created: { dataType: 'string', columnName: 'created_by', allowAnonymous: true },
2525
+ modified: { dataType: 'string', columnName: 'modified_by', allowAnonymous: true },
2526
+ }),
2527
+ username: text('username').notNull().unique(),
2528
+ email: text('email').notNull().unique(),
2529
+ password: text('password'),
2530
+ role: text('role').default('user').notNull(),
2531
+ isDeleted: boolean('is_deleted').default(false).notNull(),
2532
+ metadata: jsonb('metadata').$type<Record<string, any>>(),
2533
+ });
2534
+
2535
+ static override relations = () => [];
2536
+ static TABLE_NAME = 'User';
2537
+ }
2538
+
2539
+ // repositories/user.repository.ts
2540
+ @repository({ model: User, dataSource: PostgresDataSource })
2541
+ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
2542
+
2543
+ // services/user.service.ts
2544
+ export class UserService extends BaseService {
2545
+ constructor(
2546
+ @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
2547
+ ) {
2548
+ super({ scope: UserService.name });
2549
+ }
2550
+
2551
+ async createUser(data: { username: string; email: string; password: string }) {
2552
+ const exists = await this.userRepo.existsWith({ where: { email: data.email } });
2553
+ if (exists) {
2554
+ throw getError({
2555
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
2556
+ message: 'Email already in use',
2557
+ });
2558
+ }
2559
+
2560
+ const hashedPassword = await Bun.password.hash(data.password);
2561
+ return this.userRepo.create({
2562
+ data: { ...data, password: hashedPassword },
2563
+ });
2564
+ }
2565
+
2566
+ async softDeleteUser(id: string) {
2567
+ return this.userRepo.updateById({
2568
+ id,
2569
+ data: { isDeleted: true, deletedAt: new Date() },
2570
+ });
2571
+ }
2572
+ }
2573
+
2574
+ // controllers/user.controller.ts
2575
+ @controller({ path: '/users' })
2576
+ export class UserController extends BaseController {
2577
+ constructor(
2578
+ @inject({ key: 'services.UserService' }) private userService: UserService,
2579
+ @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
2580
+ ) {
2581
+ super({ scope: UserController.name });
2582
+ }
2583
+
2584
+ override binding() {}
2585
+
2586
+ @get({
2587
+ configs: {
2588
+ path: '/',
2589
+ description: 'List users with pagination',
2590
+ request: {
2591
+ query: z.object({
2592
+ filter: FilterSchema,
2593
+ }),
2594
+ },
2595
+ responses: jsonResponse({
2596
+ schema: z.object({
2597
+ data: z.array(z.object({
2598
+ id: z.string(),
2599
+ username: z.string(),
2600
+ email: z.string(),
2601
+ role: z.string(),
2602
+ })),
2603
+ range: z.object({ start: z.number(), end: z.number(), total: z.number() }),
2604
+ }),
2605
+ }),
2606
+ },
2607
+ })
2608
+ async list(context: TRouteContext) {
2609
+ const { filter = {} } = context.req.valid<{ filter?: any }>('query');
2610
+ const result = await this.userRepo.find({
2611
+ filter: { ...filter, fields: ['id', 'username', 'email', 'role'] },
2612
+ options: { shouldQueryRange: true },
2613
+ });
2614
+ return context.json(result, 200);
2615
+ }
2616
+
2617
+ @post({
2618
+ configs: {
2619
+ path: '/',
2620
+ authenticate: { strategies: ['jwt'] },
2621
+ authorize: { action: 'create', resource: 'User' },
2622
+ request: {
2623
+ body: jsonContent({
2624
+ schema: z.object({
2625
+ username: z.string().min(3).max(50),
2626
+ email: z.string().email(),
2627
+ password: z.string().min(8),
2628
+ }),
2629
+ }),
2630
+ },
2631
+ responses: jsonResponse({
2632
+ schema: z.object({ count: z.number(), data: z.any() }),
2633
+ }),
2634
+ },
2635
+ })
2636
+ async create(context: TRouteContext) {
2637
+ const data = context.req.valid<{ username: string; email: string; password: string }>('json');
2638
+ const result = await this.userService.createUser(data);
2639
+ return context.json(result, 200);
2640
+ }
2641
+
2642
+ @del({
2643
+ configs: {
2644
+ path: '/{id}',
2645
+ authenticate: { strategies: ['jwt'] },
2646
+ authorize: { action: 'delete', resource: 'User' },
2647
+ request: { params: idParamsSchema({ idType: 'string' }) },
2648
+ responses: jsonResponse({ schema: z.object({ count: z.number() }) }),
2649
+ },
2650
+ })
2651
+ async softDelete(context: TRouteContext) {
2652
+ const { id } = context.req.valid<{ id: string }>('param');
2653
+ const result = await this.userService.softDeleteUser(id);
2654
+ return context.json({ count: result.count }, 200);
2655
+ }
2656
+ }
2657
+ ```
2658
+
2659
+ ---
2660
+
2661
+ ## Testing
2662
+
2663
+ ### Testing Repositories
2664
+
2665
+ ```typescript
2666
+ import { describe, test, expect } from 'bun:test';
2667
+
2668
+ describe('UserRepository', () => {
2669
+ let repo: UserRepository;
2670
+ let dataSource: PostgresDataSource;
2671
+
2672
+ beforeAll(async () => {
2673
+ dataSource = new PostgresDataSource();
2674
+ await dataSource.configure();
2675
+ repo = new UserRepository(dataSource, { entityClass: User });
2676
+ });
2677
+
2678
+ test('create and find user', async () => {
2679
+ const { data: user } = await repo.create({
2680
+ data: { username: 'test', email: 'test@example.com' },
2681
+ });
2682
+
2683
+ expect(user.id).toBeDefined();
2684
+ expect(user.username).toBe('test');
2685
+
2686
+ const found = await repo.findById({ id: user.id });
2687
+ expect(found).not.toBeNull();
2688
+ expect(found!.email).toBe('test@example.com');
2689
+ });
2690
+
2691
+ test('hidden fields are excluded', async () => {
2692
+ const { data: user } = await repo.create({
2693
+ data: { username: 'secret', email: 'secret@example.com', password: 'hash123' },
2694
+ });
2695
+
2696
+ // password should not be in the returned data
2697
+ expect(user.password).toBeUndefined();
2698
+ });
2699
+
2700
+ test('default filter excludes soft-deleted records', async () => {
2701
+ const { data: user } = await repo.create({
2702
+ data: { username: 'deleted', email: 'deleted@example.com', isDeleted: true },
2703
+ });
2704
+
2705
+ // Default filter: { where: { isDeleted: false } }
2706
+ const found = await repo.findById({ id: user.id });
2707
+ expect(found).toBeNull();
2708
+
2709
+ // Bypass default filter
2710
+ const foundAll = await repo.findById({
2711
+ id: user.id,
2712
+ options: { shouldSkipDefaultFilter: true },
2713
+ });
2714
+ expect(foundAll).not.toBeNull();
2715
+ });
2716
+ });
2717
+ ```
2718
+
2719
+ ### Testing Controllers
2720
+
2721
+ ```typescript
2722
+ import { describe, test, expect } from 'bun:test';
2723
+
2724
+ describe('UserController', () => {
2725
+ let app: Application;
2726
+
2727
+ beforeAll(async () => {
2728
+ app = new Application();
2729
+ await app.initialize();
2730
+ });
2731
+
2732
+ test('GET /api/users returns 200', async () => {
2733
+ const server = app.getServer();
2734
+ const res = await server.fetch(
2735
+ new Request('http://localhost/api/users'),
2736
+ );
2737
+ expect(res.status).toBe(200);
2738
+ const body = await res.json();
2739
+ expect(Array.isArray(body)).toBe(true);
2740
+ });
2741
+
2742
+ test('POST /api/users validates request body', async () => {
2743
+ const server = app.getServer();
2744
+ const res = await server.fetch(
2745
+ new Request('http://localhost/api/users', {
2746
+ method: 'POST',
2747
+ headers: { 'Content-Type': 'application/json' },
2748
+ body: JSON.stringify({ username: '' }), // Invalid: missing email, empty username
2749
+ }),
2750
+ );
2751
+ expect(res.status).toBe(422);
2752
+ const body = await res.json();
2753
+ expect(body.message).toBe('ValidationError');
2754
+ });
2755
+ });
2756
+ ```
2757
+
2758
+ ---
2759
+
2760
+ ## Performance Tips
2761
+
2762
+ 1. **Singleton DataSources** -- DataSources are registered with `BindingScopes.SINGLETON` by default. The connection pool is shared across all repository instances. Never create DataSource instances per-request.
2763
+
2764
+ 2. **Lazy Entity Resolution** -- Entity instances are resolved from metadata only on first access. This avoids unnecessary construction during application startup.
2765
+
2766
+ 3. **Hidden Fields at SQL Level** -- `hiddenProperties` are excluded in the SQL SELECT clause, not filtered post-query. This means sensitive data never leaves the database.
2767
+
2768
+ 4. **Core API vs Query API** -- The repository automatically uses the faster Core API (15--20% faster) when your filter does not include relations or explicit field selection. No manual optimization needed.
2769
+
2770
+ 5. **Visible Property Caching** -- The `FieldsVisibilityMixin` computes the visible property set once and caches it. Subsequent queries reuse the cached column selection.
2771
+
2772
+ 6. **Parallel Count + Data** -- When `shouldQueryRange: true`, the data fetch and count query run in parallel via `Promise.all`, not sequentially.
2773
+
2774
+ 7. **Schema Factory Sharing** -- `BaseEntity` uses a lazy singleton for the Drizzle-Zod schema factory, shared across all entity instances. Schema generation does not create redundant factory objects.
2775
+
2776
+ 8. **Avoid `shouldReturn: true` for Bulk Inserts** -- When inserting large batches, pass `shouldReturn: false` to skip the `RETURNING` clause, which significantly reduces response payload size and memory usage.
2777
+
2778
+ 9. **Use Transactions Wisely** -- Each transaction acquires a dedicated connection from the pool. Long-running transactions hold connections and can starve other requests. Keep transactions short and always release them (commit or rollback) in a try/finally block.
2779
+
2780
+ 10. **Column Cache** -- The `FilterBuilder` and `UpdateBuilder` use `getCachedColumns()` to avoid repeatedly parsing table schema metadata. Columns are computed once per table and cached globally.
2781
+
2782
+ ---
2783
+
2784
+ ## Documentation
2785
+
2786
+ - [Ignis Repository](https://github.com/VENIZIA-AI/ignis)
2787
+ - [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/packages/docs/wiki/get-started/index.md)
2788
+ - [Core Concepts](https://github.com/VENIZIA-AI/ignis/blob/main/packages/docs/wiki/get-started/core-concepts/application.md)
2789
+ - [Examples](https://github.com/VENIZIA-AI/ignis/tree/main/examples/vert)
59
2790
 
60
- - [Ignis Repository](https://github.com/venizia-ai/ignis)
61
- - [Getting Started](https://github.com/venizia-ai/ignis/blob/main/packages/docs/wiki/get-started/index.md)
62
- - [Core Concepts](https://github.com/venizia-ai/ignis/blob/main/packages/docs/wiki/get-started/core-concepts/application.md)
2791
+ ---
63
2792
 
64
2793
  ## License
65
2794