@venizia/ignis 0.0.10-1 → 0.0.10-3

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 (487) hide show
  1. package/README.md +469 -784
  2. package/dist/base/applications/base.d.ts +1 -2
  3. package/dist/base/applications/base.d.ts.map +1 -1
  4. package/dist/base/applications/base.js +5 -10
  5. package/dist/base/applications/base.js.map +1 -1
  6. package/dist/base/controllers/common/types.d.ts +47 -1
  7. package/dist/base/controllers/common/types.d.ts.map +1 -1
  8. package/dist/base/controllers/common/types.js.map +1 -1
  9. package/dist/base/controllers/factory/controller.d.ts +11 -44
  10. package/dist/base/controllers/factory/controller.d.ts.map +1 -1
  11. package/dist/base/controllers/factory/controller.js +26 -17
  12. package/dist/base/controllers/factory/controller.js.map +1 -1
  13. package/dist/base/controllers/factory/definition.d.ts +8 -7
  14. package/dist/base/controllers/factory/definition.d.ts.map +1 -1
  15. package/dist/base/controllers/factory/definition.js +24 -11
  16. package/dist/base/controllers/factory/definition.js.map +1 -1
  17. package/dist/base/controllers/grpc/abstract.d.ts.map +1 -1
  18. package/dist/base/controllers/grpc/abstract.js +13 -1
  19. package/dist/base/controllers/grpc/abstract.js.map +1 -1
  20. package/dist/base/controllers/grpc/adapter.d.ts.map +1 -1
  21. package/dist/base/controllers/grpc/adapter.js +7 -1
  22. package/dist/base/controllers/grpc/adapter.js.map +1 -1
  23. package/dist/base/controllers/rest/abstract.d.ts.map +1 -1
  24. package/dist/base/controllers/rest/abstract.js +6 -0
  25. package/dist/base/controllers/rest/abstract.js.map +1 -1
  26. package/dist/base/controllers/rest/base.d.ts +3 -1
  27. package/dist/base/controllers/rest/base.d.ts.map +1 -1
  28. package/dist/base/controllers/rest/base.js +4 -3
  29. package/dist/base/controllers/rest/base.js.map +1 -1
  30. package/dist/base/datasources/abstract-datasource.d.ts +27 -0
  31. package/dist/base/datasources/abstract-datasource.d.ts.map +1 -0
  32. package/dist/base/datasources/abstract-datasource.js +73 -0
  33. package/dist/base/datasources/abstract-datasource.js.map +1 -0
  34. package/dist/base/datasources/common/types.d.ts +22 -30
  35. package/dist/base/datasources/common/types.d.ts.map +1 -1
  36. package/dist/base/datasources/common/types.js +3 -17
  37. package/dist/base/datasources/common/types.js.map +1 -1
  38. package/dist/base/datasources/index.d.ts +1 -1
  39. package/dist/base/datasources/index.d.ts.map +1 -1
  40. package/dist/base/datasources/index.js +1 -1
  41. package/dist/base/datasources/index.js.map +1 -1
  42. package/dist/base/metadata/persistents.d.ts +2 -3
  43. package/dist/base/metadata/persistents.d.ts.map +1 -1
  44. package/dist/base/metadata/persistents.js +4 -0
  45. package/dist/base/metadata/persistents.js.map +1 -1
  46. package/dist/base/metadata/routes/rest.d.ts.map +1 -1
  47. package/dist/base/metadata/routes/rest.js.map +1 -1
  48. package/dist/base/metadata/routes/rpc.d.ts.map +1 -1
  49. package/dist/base/metadata/routes/rpc.js.map +1 -1
  50. package/dist/base/mixins/repository.mixin.d.ts +2 -3
  51. package/dist/base/mixins/repository.mixin.d.ts.map +1 -1
  52. package/dist/base/mixins/repository.mixin.js +1 -1
  53. package/dist/base/mixins/repository.mixin.js.map +1 -1
  54. package/dist/base/mixins/types.d.ts +1 -2
  55. package/dist/base/mixins/types.d.ts.map +1 -1
  56. package/dist/base/models/base.d.ts +10 -19
  57. package/dist/base/models/base.d.ts.map +1 -1
  58. package/dist/base/models/base.js +9 -33
  59. package/dist/base/models/base.js.map +1 -1
  60. package/dist/base/models/common/constants.d.ts +2 -1
  61. package/dist/base/models/common/constants.d.ts.map +1 -1
  62. package/dist/base/models/common/constants.js +4 -3
  63. package/dist/base/models/common/constants.js.map +1 -1
  64. package/dist/base/models/common/index.d.ts +0 -1
  65. package/dist/base/models/common/index.d.ts.map +1 -1
  66. package/dist/base/models/common/index.js +0 -1
  67. package/dist/base/models/common/index.js.map +1 -1
  68. package/dist/base/models/common/types.d.ts +3 -30
  69. package/dist/base/models/common/types.d.ts.map +1 -1
  70. package/dist/base/models/common/types.js +1 -5
  71. package/dist/base/models/common/types.js.map +1 -1
  72. package/dist/base/models/index.d.ts +0 -1
  73. package/dist/base/models/index.d.ts.map +1 -1
  74. package/dist/base/models/index.js +0 -1
  75. package/dist/base/models/index.js.map +1 -1
  76. package/dist/base/repositories/common/index.d.ts +1 -1
  77. package/dist/base/repositories/common/index.d.ts.map +1 -1
  78. package/dist/base/repositories/common/index.js +1 -1
  79. package/dist/base/repositories/common/index.js.map +1 -1
  80. package/dist/base/repositories/common/operators.d.ts +49 -0
  81. package/dist/base/repositories/common/operators.d.ts.map +1 -0
  82. package/dist/base/repositories/common/operators.js +118 -0
  83. package/dist/base/repositories/common/operators.js.map +1 -0
  84. package/dist/base/repositories/common/types.d.ts +57 -73
  85. package/dist/base/repositories/common/types.d.ts.map +1 -1
  86. package/dist/base/repositories/common/types.js +2 -5
  87. package/dist/base/repositories/common/types.js.map +1 -1
  88. package/dist/base/repositories/core/abstract-repository.d.ts +218 -0
  89. package/dist/base/repositories/core/abstract-repository.d.ts.map +1 -0
  90. package/dist/base/repositories/core/abstract-repository.js +107 -0
  91. package/dist/base/repositories/core/abstract-repository.js.map +1 -0
  92. package/dist/base/repositories/core/index.d.ts +1 -5
  93. package/dist/base/repositories/core/index.d.ts.map +1 -1
  94. package/dist/base/repositories/core/index.js +1 -5
  95. package/dist/base/repositories/core/index.js.map +1 -1
  96. package/dist/base/repositories/index.d.ts +0 -1
  97. package/dist/base/repositories/index.d.ts.map +1 -1
  98. package/dist/base/repositories/index.js +0 -1
  99. package/dist/base/repositories/index.js.map +1 -1
  100. package/dist/components/auth/authenticate/component.d.ts.map +1 -1
  101. package/dist/components/auth/authenticate/component.js +3 -2
  102. package/dist/components/auth/authenticate/component.js.map +1 -1
  103. package/dist/components/auth/authenticate/controllers/factory.d.ts.map +1 -1
  104. package/dist/components/auth/authenticate/controllers/factory.js +13 -9
  105. package/dist/components/auth/authenticate/controllers/factory.js.map +1 -1
  106. package/dist/components/auth/authenticate/providers/authentication.provider.js.map +1 -1
  107. package/dist/components/auth/authorize/adapters/base-filtered.d.ts +6 -20
  108. package/dist/components/auth/authorize/adapters/base-filtered.d.ts.map +1 -1
  109. package/dist/components/auth/authorize/adapters/base-filtered.js +2 -8
  110. package/dist/components/auth/authorize/adapters/base-filtered.js.map +1 -1
  111. package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.d.ts +19 -55
  112. package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.d.ts.map +1 -1
  113. package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.js +19 -55
  114. package/dist/components/auth/authorize/adapters/scoped-casbin.adapter.js.map +1 -1
  115. package/dist/components/auth/authorize/adapters/types.d.ts +16 -0
  116. package/dist/components/auth/authorize/adapters/types.d.ts.map +1 -1
  117. package/dist/components/auth/authorize/common/resolve-request-domain.js.map +1 -1
  118. package/dist/components/auth/models/entities/user.model.d.ts.map +1 -1
  119. package/dist/components/auth/models/entities/user.model.js +3 -3
  120. package/dist/components/auth/models/entities/user.model.js.map +1 -1
  121. package/dist/components/mail/providers/mail-transporter.provider.d.ts +1 -1
  122. package/dist/components/mail/providers/mail-transporter.provider.d.ts.map +1 -1
  123. package/dist/components/mail/providers/mail-transporter.provider.js +4 -3
  124. package/dist/components/mail/providers/mail-transporter.provider.js.map +1 -1
  125. package/dist/components/static-asset/common/types.d.ts +4 -4
  126. package/dist/components/static-asset/common/types.d.ts.map +1 -1
  127. package/dist/components/static-asset/models/base.model.d.ts +2 -2
  128. package/dist/components/static-asset/models/base.model.d.ts.map +1 -1
  129. package/dist/components/static-asset/models/base.model.js +2 -2
  130. package/dist/components/static-asset/models/base.model.js.map +1 -1
  131. package/dist/components/static-asset/repositories/base.repository.d.ts +2 -10
  132. package/dist/components/static-asset/repositories/base.repository.d.ts.map +1 -1
  133. package/dist/components/static-asset/repositories/base.repository.js +2 -10
  134. package/dist/components/static-asset/repositories/base.repository.js.map +1 -1
  135. package/dist/components/websocket/handlers/bun.handler.d.ts +1 -1
  136. package/dist/components/websocket/handlers/bun.handler.d.ts.map +1 -1
  137. package/dist/components/websocket/handlers/bun.handler.js.map +1 -1
  138. package/dist/connectors/index.d.ts +3 -0
  139. package/dist/connectors/index.d.ts.map +1 -0
  140. package/dist/{base/repositories/mixins → connectors}/index.js +2 -2
  141. package/dist/connectors/index.js.map +1 -0
  142. package/dist/connectors/memory/datasources/datasource.d.ts +35 -0
  143. package/dist/connectors/memory/datasources/datasource.d.ts.map +1 -0
  144. package/dist/connectors/memory/datasources/datasource.js +51 -0
  145. package/dist/connectors/memory/datasources/datasource.js.map +1 -0
  146. package/dist/connectors/memory/datasources/index.d.ts +2 -0
  147. package/dist/connectors/memory/datasources/index.d.ts.map +1 -0
  148. package/dist/connectors/memory/datasources/index.js +18 -0
  149. package/dist/connectors/memory/datasources/index.js.map +1 -0
  150. package/dist/connectors/memory/index.d.ts +3 -0
  151. package/dist/connectors/memory/index.d.ts.map +1 -0
  152. package/dist/connectors/memory/index.js +19 -0
  153. package/dist/connectors/memory/index.js.map +1 -0
  154. package/dist/connectors/memory/internal/index.d.ts +2 -0
  155. package/dist/connectors/memory/internal/index.d.ts.map +1 -0
  156. package/dist/connectors/memory/internal/index.js +18 -0
  157. package/dist/connectors/memory/internal/index.js.map +1 -0
  158. package/dist/connectors/memory/internal/where-matcher.d.ts +13 -0
  159. package/dist/connectors/memory/internal/where-matcher.d.ts.map +1 -0
  160. package/dist/connectors/memory/internal/where-matcher.js +326 -0
  161. package/dist/connectors/memory/internal/where-matcher.js.map +1 -0
  162. package/dist/connectors/memory/repositories/index.d.ts +2 -0
  163. package/dist/connectors/memory/repositories/index.d.ts.map +1 -0
  164. package/dist/connectors/memory/repositories/index.js +18 -0
  165. package/dist/connectors/memory/repositories/index.js.map +1 -0
  166. package/dist/connectors/memory/repositories/repository.d.ts +175 -0
  167. package/dist/connectors/memory/repositories/repository.d.ts.map +1 -0
  168. package/dist/connectors/memory/repositories/repository.js +345 -0
  169. package/dist/connectors/memory/repositories/repository.js.map +1 -0
  170. package/dist/connectors/postgres/datasources/abstract-datasource.d.ts +13 -0
  171. package/dist/connectors/postgres/datasources/abstract-datasource.d.ts.map +1 -0
  172. package/dist/connectors/postgres/datasources/abstract-datasource.js +12 -0
  173. package/dist/connectors/postgres/datasources/abstract-datasource.js.map +1 -0
  174. package/dist/connectors/postgres/datasources/base-datasource.d.ts +20 -0
  175. package/dist/connectors/postgres/datasources/base-datasource.d.ts.map +1 -0
  176. package/dist/{base/datasources/base.js → connectors/postgres/datasources/base-datasource.js} +11 -22
  177. package/dist/connectors/postgres/datasources/base-datasource.js.map +1 -0
  178. package/dist/connectors/postgres/datasources/common/guards.d.ts +5 -0
  179. package/dist/connectors/postgres/datasources/common/guards.d.ts.map +1 -0
  180. package/dist/connectors/postgres/datasources/common/guards.js +9 -0
  181. package/dist/connectors/postgres/datasources/common/guards.js.map +1 -0
  182. package/dist/connectors/postgres/datasources/common/index.d.ts +3 -0
  183. package/dist/connectors/postgres/datasources/common/index.d.ts.map +1 -0
  184. package/dist/connectors/postgres/datasources/common/index.js +19 -0
  185. package/dist/connectors/postgres/datasources/common/index.js.map +1 -0
  186. package/dist/connectors/postgres/datasources/common/types.d.ts +37 -0
  187. package/dist/connectors/postgres/datasources/common/types.d.ts.map +1 -0
  188. package/dist/connectors/postgres/datasources/common/types.js +19 -0
  189. package/dist/connectors/postgres/datasources/common/types.js.map +1 -0
  190. package/dist/connectors/postgres/datasources/index.d.ts +5 -0
  191. package/dist/connectors/postgres/datasources/index.d.ts.map +1 -0
  192. package/dist/connectors/postgres/datasources/index.js +24 -0
  193. package/dist/connectors/postgres/datasources/index.js.map +1 -0
  194. package/dist/connectors/postgres/index.d.ts +4 -0
  195. package/dist/connectors/postgres/index.d.ts.map +1 -0
  196. package/dist/connectors/postgres/index.js +20 -0
  197. package/dist/connectors/postgres/index.js.map +1 -0
  198. package/dist/connectors/postgres/models/base.d.ts +29 -0
  199. package/dist/connectors/postgres/models/base.d.ts.map +1 -0
  200. package/dist/connectors/postgres/models/base.js +45 -0
  201. package/dist/connectors/postgres/models/base.js.map +1 -0
  202. package/dist/connectors/postgres/models/common/columns.d.ts +13 -0
  203. package/dist/connectors/postgres/models/common/columns.d.ts.map +1 -0
  204. package/dist/{base → connectors/postgres}/models/common/columns.js +2 -12
  205. package/dist/connectors/postgres/models/common/columns.js.map +1 -0
  206. package/dist/connectors/postgres/models/common/index.d.ts +3 -0
  207. package/dist/connectors/postgres/models/common/index.d.ts.map +1 -0
  208. package/dist/connectors/postgres/models/common/index.js +19 -0
  209. package/dist/connectors/postgres/models/common/index.js.map +1 -0
  210. package/dist/connectors/postgres/models/common/types.d.ts +89 -0
  211. package/dist/connectors/postgres/models/common/types.d.ts.map +1 -0
  212. package/dist/connectors/postgres/models/common/types.js +8 -0
  213. package/dist/connectors/postgres/models/common/types.js.map +1 -0
  214. package/dist/{base → connectors/postgres}/models/enrichers/data-type.enricher.d.ts +1 -11
  215. package/dist/connectors/postgres/models/enrichers/data-type.enricher.d.ts.map +1 -0
  216. package/dist/connectors/postgres/models/enrichers/data-type.enricher.js.map +1 -0
  217. package/dist/{base → connectors/postgres}/models/enrichers/id.enricher.d.ts +2 -17
  218. package/dist/connectors/postgres/models/enrichers/id.enricher.d.ts.map +1 -0
  219. package/dist/connectors/postgres/models/enrichers/id.enricher.js.map +1 -0
  220. package/dist/connectors/postgres/models/enrichers/index.d.ts.map +1 -0
  221. package/dist/connectors/postgres/models/enrichers/index.js.map +1 -0
  222. package/dist/{base → connectors/postgres}/models/enrichers/principal.enricher.d.ts +1 -7
  223. package/dist/connectors/postgres/models/enrichers/principal.enricher.d.ts.map +1 -0
  224. package/dist/connectors/postgres/models/enrichers/principal.enricher.js.map +1 -0
  225. package/dist/{base → connectors/postgres}/models/enrichers/tz.enricher.d.ts +1 -21
  226. package/dist/connectors/postgres/models/enrichers/tz.enricher.d.ts.map +1 -0
  227. package/dist/{base → connectors/postgres}/models/enrichers/tz.enricher.js +0 -2
  228. package/dist/connectors/postgres/models/enrichers/tz.enricher.js.map +1 -0
  229. package/dist/{base → connectors/postgres}/models/enrichers/user-audit.enricher.d.ts +1 -11
  230. package/dist/connectors/postgres/models/enrichers/user-audit.enricher.d.ts.map +1 -0
  231. package/dist/{base → connectors/postgres}/models/enrichers/user-audit.enricher.js +1 -1
  232. package/dist/connectors/postgres/models/enrichers/user-audit.enricher.js.map +1 -0
  233. package/dist/connectors/postgres/models/index.d.ts +6 -0
  234. package/dist/connectors/postgres/models/index.d.ts.map +1 -0
  235. package/dist/connectors/postgres/models/index.js +25 -0
  236. package/dist/connectors/postgres/models/index.js.map +1 -0
  237. package/dist/connectors/postgres/repositories/common/index.d.ts +3 -0
  238. package/dist/connectors/postgres/repositories/common/index.d.ts.map +1 -0
  239. package/dist/connectors/postgres/repositories/common/index.js +19 -0
  240. package/dist/connectors/postgres/repositories/common/index.js.map +1 -0
  241. package/dist/connectors/postgres/repositories/common/shared.d.ts +5 -0
  242. package/dist/connectors/postgres/repositories/common/shared.d.ts.map +1 -0
  243. package/dist/connectors/postgres/repositories/common/shared.js.map +1 -0
  244. package/dist/connectors/postgres/repositories/common/types.d.ts +33 -0
  245. package/dist/connectors/postgres/repositories/common/types.d.ts.map +1 -0
  246. package/dist/connectors/postgres/repositories/common/types.js +3 -0
  247. package/dist/connectors/postgres/repositories/common/types.js.map +1 -0
  248. package/dist/{base/repositories/core/abstract.d.ts → connectors/postgres/repositories/core/base.d.ts} +45 -96
  249. package/dist/connectors/postgres/repositories/core/base.d.ts.map +1 -0
  250. package/dist/{base/repositories/core/abstract.js → connectors/postgres/repositories/core/base.js} +85 -74
  251. package/dist/connectors/postgres/repositories/core/base.js.map +1 -0
  252. package/dist/connectors/postgres/repositories/core/default-crud.d.ts +9 -0
  253. package/dist/connectors/postgres/repositories/core/default-crud.d.ts.map +1 -0
  254. package/dist/{base → connectors/postgres}/repositories/core/default-crud.js +2 -1
  255. package/dist/connectors/postgres/repositories/core/default-crud.js.map +1 -0
  256. package/dist/connectors/postgres/repositories/core/index.d.ts +6 -0
  257. package/dist/connectors/postgres/repositories/core/index.d.ts.map +1 -0
  258. package/dist/connectors/postgres/repositories/core/index.js +22 -0
  259. package/dist/connectors/postgres/repositories/core/index.js.map +1 -0
  260. package/dist/{base → connectors/postgres}/repositories/core/persistable.d.ts +8 -6
  261. package/dist/connectors/postgres/repositories/core/persistable.d.ts.map +1 -0
  262. package/dist/{base → connectors/postgres}/repositories/core/persistable.js +13 -6
  263. package/dist/connectors/postgres/repositories/core/persistable.js.map +1 -0
  264. package/dist/{base → connectors/postgres}/repositories/core/readable.d.ts +9 -7
  265. package/dist/connectors/postgres/repositories/core/readable.d.ts.map +1 -0
  266. package/dist/{base → connectors/postgres}/repositories/core/readable.js +15 -13
  267. package/dist/connectors/postgres/repositories/core/readable.js.map +1 -0
  268. package/dist/{base → connectors/postgres}/repositories/core/soft-deletable.d.ts +8 -31
  269. package/dist/connectors/postgres/repositories/core/soft-deletable.d.ts.map +1 -0
  270. package/dist/{base → connectors/postgres}/repositories/core/soft-deletable.js +32 -40
  271. package/dist/connectors/postgres/repositories/core/soft-deletable.js.map +1 -0
  272. package/dist/connectors/postgres/repositories/index.d.ts +4 -0
  273. package/dist/connectors/postgres/repositories/index.d.ts.map +1 -0
  274. package/dist/connectors/postgres/repositories/index.js +20 -0
  275. package/dist/connectors/postgres/repositories/index.js.map +1 -0
  276. package/dist/{base → connectors/postgres}/repositories/operators/filter.d.ts +5 -3
  277. package/dist/connectors/postgres/repositories/operators/filter.d.ts.map +1 -0
  278. package/dist/{base → connectors/postgres}/repositories/operators/filter.js +16 -14
  279. package/dist/connectors/postgres/repositories/operators/filter.js.map +1 -0
  280. package/dist/connectors/postgres/repositories/operators/index.d.ts +6 -0
  281. package/dist/connectors/postgres/repositories/operators/index.d.ts.map +1 -0
  282. package/dist/{base → connectors/postgres}/repositories/operators/index.js +4 -1
  283. package/dist/connectors/postgres/repositories/operators/index.js.map +1 -0
  284. package/dist/connectors/postgres/repositories/operators/internal/json-utils.d.ts.map +1 -0
  285. package/dist/connectors/postgres/repositories/operators/internal/json-utils.js.map +1 -0
  286. package/dist/connectors/postgres/repositories/operators/query.d.ts +31 -0
  287. package/dist/connectors/postgres/repositories/operators/query.d.ts.map +1 -0
  288. package/dist/{base → connectors/postgres}/repositories/operators/query.js +12 -114
  289. package/dist/connectors/postgres/repositories/operators/query.js.map +1 -0
  290. package/dist/{base → connectors/postgres}/repositories/operators/relation.d.ts +1 -1
  291. package/dist/connectors/postgres/repositories/operators/relation.d.ts.map +1 -0
  292. package/dist/connectors/postgres/repositories/operators/relation.js.map +1 -0
  293. package/dist/{base → connectors/postgres}/repositories/operators/update.d.ts +2 -9
  294. package/dist/connectors/postgres/repositories/operators/update.d.ts.map +1 -0
  295. package/dist/{base → connectors/postgres}/repositories/operators/update.js +1 -1
  296. package/dist/connectors/postgres/repositories/operators/update.js.map +1 -0
  297. package/dist/connectors/typesense/compiler.d.ts +7 -0
  298. package/dist/connectors/typesense/compiler.d.ts.map +1 -0
  299. package/dist/connectors/typesense/compiler.js +114 -0
  300. package/dist/connectors/typesense/compiler.js.map +1 -0
  301. package/dist/connectors/typesense/datasources/abstract-datasource.d.ts +16 -0
  302. package/dist/connectors/typesense/datasources/abstract-datasource.d.ts.map +1 -0
  303. package/dist/connectors/typesense/datasources/abstract-datasource.js +9 -0
  304. package/dist/connectors/typesense/datasources/abstract-datasource.js.map +1 -0
  305. package/dist/connectors/typesense/datasources/base-datasource.d.ts +21 -0
  306. package/dist/connectors/typesense/datasources/base-datasource.d.ts.map +1 -0
  307. package/dist/connectors/typesense/datasources/base-datasource.js +66 -0
  308. package/dist/connectors/typesense/datasources/base-datasource.js.map +1 -0
  309. package/dist/connectors/typesense/datasources/datasource.d.ts +27 -0
  310. package/dist/connectors/typesense/datasources/datasource.d.ts.map +1 -0
  311. package/dist/connectors/typesense/datasources/datasource.js +54 -0
  312. package/dist/connectors/typesense/datasources/datasource.js.map +1 -0
  313. package/dist/connectors/typesense/datasources/index.d.ts +4 -0
  314. package/dist/connectors/typesense/datasources/index.d.ts.map +1 -0
  315. package/dist/connectors/typesense/datasources/index.js +20 -0
  316. package/dist/connectors/typesense/datasources/index.js.map +1 -0
  317. package/dist/connectors/typesense/driver.d.ts +364 -0
  318. package/dist/connectors/typesense/driver.d.ts.map +1 -0
  319. package/dist/connectors/typesense/driver.js +624 -0
  320. package/dist/connectors/typesense/driver.js.map +1 -0
  321. package/dist/connectors/typesense/index.d.ts +8 -0
  322. package/dist/connectors/typesense/index.d.ts.map +1 -0
  323. package/dist/connectors/typesense/index.js +24 -0
  324. package/dist/connectors/typesense/index.js.map +1 -0
  325. package/dist/connectors/typesense/internal/driver-internal.d.ts +11 -0
  326. package/dist/connectors/typesense/internal/driver-internal.d.ts.map +1 -0
  327. package/dist/connectors/typesense/internal/driver-internal.js +45 -0
  328. package/dist/connectors/typesense/internal/driver-internal.js.map +1 -0
  329. package/dist/connectors/typesense/internal/index.d.ts +3 -0
  330. package/dist/connectors/typesense/internal/index.d.ts.map +1 -0
  331. package/dist/connectors/typesense/internal/index.js +19 -0
  332. package/dist/connectors/typesense/internal/index.js.map +1 -0
  333. package/dist/connectors/typesense/internal/search-driver-internal.d.ts +18 -0
  334. package/dist/connectors/typesense/internal/search-driver-internal.d.ts.map +1 -0
  335. package/dist/connectors/typesense/internal/search-driver-internal.js +37 -0
  336. package/dist/connectors/typesense/internal/search-driver-internal.js.map +1 -0
  337. package/dist/connectors/typesense/models/base.d.ts +28 -0
  338. package/dist/connectors/typesense/models/base.d.ts.map +1 -0
  339. package/dist/connectors/typesense/models/base.js +63 -0
  340. package/dist/connectors/typesense/models/base.js.map +1 -0
  341. package/dist/connectors/typesense/models/define-search-collection.d.ts +41 -0
  342. package/dist/connectors/typesense/models/define-search-collection.d.ts.map +1 -0
  343. package/dist/connectors/typesense/models/define-search-collection.js +90 -0
  344. package/dist/connectors/typesense/models/define-search-collection.js.map +1 -0
  345. package/dist/connectors/typesense/models/index.d.ts +5 -0
  346. package/dist/connectors/typesense/models/index.d.ts.map +1 -0
  347. package/dist/connectors/typesense/models/index.js +21 -0
  348. package/dist/connectors/typesense/models/index.js.map +1 -0
  349. package/dist/connectors/typesense/models/types.d.ts +57 -0
  350. package/dist/connectors/typesense/models/types.d.ts.map +1 -0
  351. package/dist/connectors/typesense/models/types.js +27 -0
  352. package/dist/connectors/typesense/models/types.js.map +1 -0
  353. package/dist/connectors/typesense/models/zod-derivation.d.ts +8 -0
  354. package/dist/connectors/typesense/models/zod-derivation.d.ts.map +1 -0
  355. package/dist/connectors/typesense/models/zod-derivation.js +67 -0
  356. package/dist/connectors/typesense/models/zod-derivation.js.map +1 -0
  357. package/dist/connectors/typesense/query-dialect.d.ts +25 -0
  358. package/dist/connectors/typesense/query-dialect.d.ts.map +1 -0
  359. package/dist/connectors/typesense/query-dialect.js +263 -0
  360. package/dist/connectors/typesense/query-dialect.js.map +1 -0
  361. package/dist/connectors/typesense/repositories/common/index.d.ts +2 -0
  362. package/dist/connectors/typesense/repositories/common/index.d.ts.map +1 -0
  363. package/dist/connectors/typesense/repositories/common/index.js +18 -0
  364. package/dist/connectors/typesense/repositories/common/index.js.map +1 -0
  365. package/dist/connectors/typesense/repositories/common/types.d.ts +24 -0
  366. package/dist/connectors/typesense/repositories/common/types.d.ts.map +1 -0
  367. package/dist/connectors/typesense/repositories/common/types.js +3 -0
  368. package/dist/connectors/typesense/repositories/common/types.js.map +1 -0
  369. package/dist/connectors/typesense/repositories/core/base-repository.d.ts +43 -0
  370. package/dist/connectors/typesense/repositories/core/base-repository.d.ts.map +1 -0
  371. package/dist/connectors/typesense/repositories/core/base-repository.js +87 -0
  372. package/dist/connectors/typesense/repositories/core/base-repository.js.map +1 -0
  373. package/dist/connectors/typesense/repositories/core/default-search.d.ts +41 -0
  374. package/dist/connectors/typesense/repositories/core/default-search.d.ts.map +1 -0
  375. package/dist/connectors/typesense/repositories/core/default-search.js +66 -0
  376. package/dist/connectors/typesense/repositories/core/default-search.js.map +1 -0
  377. package/dist/connectors/typesense/repositories/core/index.d.ts +5 -0
  378. package/dist/connectors/typesense/repositories/core/index.d.ts.map +1 -0
  379. package/dist/connectors/typesense/repositories/core/index.js +21 -0
  380. package/dist/connectors/typesense/repositories/core/index.js.map +1 -0
  381. package/dist/connectors/typesense/repositories/core/persistable.d.ts +100 -0
  382. package/dist/connectors/typesense/repositories/core/persistable.d.ts.map +1 -0
  383. package/dist/connectors/typesense/repositories/core/persistable.js +132 -0
  384. package/dist/connectors/typesense/repositories/core/persistable.js.map +1 -0
  385. package/dist/connectors/typesense/repositories/core/readable.d.ts +157 -0
  386. package/dist/connectors/typesense/repositories/core/readable.d.ts.map +1 -0
  387. package/dist/connectors/typesense/repositories/core/readable.js +114 -0
  388. package/dist/connectors/typesense/repositories/core/readable.js.map +1 -0
  389. package/dist/connectors/typesense/repositories/index.d.ts +3 -0
  390. package/dist/connectors/typesense/repositories/index.d.ts.map +1 -0
  391. package/dist/connectors/typesense/repositories/index.js +19 -0
  392. package/dist/connectors/typesense/repositories/index.js.map +1 -0
  393. package/dist/connectors/typesense/types.d.ts +68 -0
  394. package/dist/connectors/typesense/types.d.ts.map +1 -0
  395. package/dist/connectors/typesense/types.js +36 -0
  396. package/dist/connectors/typesense/types.js.map +1 -0
  397. package/dist/helpers/inversion/common/types.d.ts +15 -13
  398. package/dist/helpers/inversion/common/types.d.ts.map +1 -1
  399. package/dist/helpers/inversion/mixins/model.mixin.d.ts +6 -27
  400. package/dist/helpers/inversion/mixins/model.mixin.d.ts.map +1 -1
  401. package/dist/helpers/inversion/mixins/model.mixin.js +9 -28
  402. package/dist/helpers/inversion/mixins/model.mixin.js.map +1 -1
  403. package/dist/helpers/inversion/mixins/repository.mixin.d.ts +30 -54
  404. package/dist/helpers/inversion/mixins/repository.mixin.d.ts.map +1 -1
  405. package/dist/helpers/inversion/mixins/repository.mixin.js +31 -53
  406. package/dist/helpers/inversion/mixins/repository.mixin.js.map +1 -1
  407. package/dist/helpers/inversion/registry.d.ts +20 -14
  408. package/dist/helpers/inversion/registry.d.ts.map +1 -1
  409. package/dist/index.d.ts +1 -0
  410. package/dist/index.d.ts.map +1 -1
  411. package/dist/index.js +1 -0
  412. package/dist/index.js.map +1 -1
  413. package/dist/utilities/error.utility.d.ts +11 -0
  414. package/dist/utilities/error.utility.d.ts.map +1 -0
  415. package/dist/utilities/error.utility.js +19 -0
  416. package/dist/utilities/error.utility.js.map +1 -0
  417. package/dist/utilities/index.d.ts +1 -0
  418. package/dist/utilities/index.d.ts.map +1 -1
  419. package/dist/utilities/index.js +1 -0
  420. package/dist/utilities/index.js.map +1 -1
  421. package/package.json +24 -7
  422. package/dist/base/datasources/base.d.ts +0 -30
  423. package/dist/base/datasources/base.d.ts.map +0 -1
  424. package/dist/base/datasources/base.js.map +0 -1
  425. package/dist/base/models/common/columns.d.ts +0 -22
  426. package/dist/base/models/common/columns.d.ts.map +0 -1
  427. package/dist/base/models/common/columns.js.map +0 -1
  428. package/dist/base/models/enrichers/data-type.enricher.d.ts.map +0 -1
  429. package/dist/base/models/enrichers/data-type.enricher.js.map +0 -1
  430. package/dist/base/models/enrichers/id.enricher.d.ts.map +0 -1
  431. package/dist/base/models/enrichers/id.enricher.js.map +0 -1
  432. package/dist/base/models/enrichers/index.d.ts.map +0 -1
  433. package/dist/base/models/enrichers/index.js.map +0 -1
  434. package/dist/base/models/enrichers/principal.enricher.d.ts.map +0 -1
  435. package/dist/base/models/enrichers/principal.enricher.js.map +0 -1
  436. package/dist/base/models/enrichers/tz.enricher.d.ts.map +0 -1
  437. package/dist/base/models/enrichers/tz.enricher.js.map +0 -1
  438. package/dist/base/models/enrichers/user-audit.enricher.d.ts.map +0 -1
  439. package/dist/base/models/enrichers/user-audit.enricher.js.map +0 -1
  440. package/dist/base/repositories/common/shared.d.ts +0 -7
  441. package/dist/base/repositories/common/shared.d.ts.map +0 -1
  442. package/dist/base/repositories/common/shared.js.map +0 -1
  443. package/dist/base/repositories/core/abstract.d.ts.map +0 -1
  444. package/dist/base/repositories/core/abstract.js.map +0 -1
  445. package/dist/base/repositories/core/default-crud.d.ts +0 -7
  446. package/dist/base/repositories/core/default-crud.d.ts.map +0 -1
  447. package/dist/base/repositories/core/default-crud.js.map +0 -1
  448. package/dist/base/repositories/core/persistable.d.ts.map +0 -1
  449. package/dist/base/repositories/core/persistable.js.map +0 -1
  450. package/dist/base/repositories/core/readable.d.ts.map +0 -1
  451. package/dist/base/repositories/core/readable.js.map +0 -1
  452. package/dist/base/repositories/core/soft-deletable.d.ts.map +0 -1
  453. package/dist/base/repositories/core/soft-deletable.js.map +0 -1
  454. package/dist/base/repositories/mixins/default-filter.d.ts +0 -22
  455. package/dist/base/repositories/mixins/default-filter.d.ts.map +0 -1
  456. package/dist/base/repositories/mixins/default-filter.js +0 -54
  457. package/dist/base/repositories/mixins/default-filter.js.map +0 -1
  458. package/dist/base/repositories/mixins/fields-visibility.d.ts +0 -18
  459. package/dist/base/repositories/mixins/fields-visibility.d.ts.map +0 -1
  460. package/dist/base/repositories/mixins/fields-visibility.js +0 -65
  461. package/dist/base/repositories/mixins/fields-visibility.js.map +0 -1
  462. package/dist/base/repositories/mixins/index.d.ts +0 -3
  463. package/dist/base/repositories/mixins/index.d.ts.map +0 -1
  464. package/dist/base/repositories/mixins/index.js.map +0 -1
  465. package/dist/base/repositories/operators/filter.d.ts.map +0 -1
  466. package/dist/base/repositories/operators/filter.js.map +0 -1
  467. package/dist/base/repositories/operators/index.d.ts +0 -6
  468. package/dist/base/repositories/operators/index.d.ts.map +0 -1
  469. package/dist/base/repositories/operators/index.js.map +0 -1
  470. package/dist/base/repositories/operators/json-utils.d.ts.map +0 -1
  471. package/dist/base/repositories/operators/json-utils.js.map +0 -1
  472. package/dist/base/repositories/operators/query.d.ts +0 -75
  473. package/dist/base/repositories/operators/query.d.ts.map +0 -1
  474. package/dist/base/repositories/operators/query.js.map +0 -1
  475. package/dist/base/repositories/operators/relation.d.ts.map +0 -1
  476. package/dist/base/repositories/operators/relation.js.map +0 -1
  477. package/dist/base/repositories/operators/update.d.ts.map +0 -1
  478. package/dist/base/repositories/operators/update.js.map +0 -1
  479. /package/dist/{base → connectors/postgres}/models/enrichers/data-type.enricher.js +0 -0
  480. /package/dist/{base → connectors/postgres}/models/enrichers/id.enricher.js +0 -0
  481. /package/dist/{base → connectors/postgres}/models/enrichers/index.d.ts +0 -0
  482. /package/dist/{base → connectors/postgres}/models/enrichers/index.js +0 -0
  483. /package/dist/{base → connectors/postgres}/models/enrichers/principal.enricher.js +0 -0
  484. /package/dist/{base → connectors/postgres}/repositories/common/shared.js +0 -0
  485. /package/dist/{base/repositories/operators → connectors/postgres/repositories/operators/internal}/json-utils.d.ts +0 -0
  486. /package/dist/{base/repositories/operators → connectors/postgres/repositories/operators/internal}/json-utils.js +0 -0
  487. /package/dist/{base → connectors/postgres}/repositories/operators/relation.js +0 -0
package/README.md CHANGED
@@ -34,11 +34,13 @@ IGNIS brings together the structured, enterprise development experience of **Loo
34
34
  ```typescript
35
35
  import {
36
36
  BaseApplication, // Your app extends this
37
- BaseRestController, // Controllers extend this
38
- DefaultCRUDRepository, // Repositories extend this
39
- BaseEntity, // Models extend this
40
- BaseDataSource, // DataSources extend this
37
+ BaseRestController, // Controllers extend this
41
38
  } from '@venizia/ignis';
39
+ import {
40
+ DefaultCRUDRepository, // Repositories extend this
41
+ BasePostgresEntity, // Models extend this
42
+ BasePostgresDataSource, // DataSources extend this
43
+ } from '@venizia/ignis/postgres';
42
44
  ```
43
45
 
44
46
  ---
@@ -53,6 +55,8 @@ import {
53
55
  - [Repositories](#repositories)
54
56
  - [Models](#models)
55
57
  - [DataSources](#datasources)
58
+ - [Search](#search)
59
+ - [Memory Connector](#memory-connector)
56
60
  - [Services](#services)
57
61
  - [Components](#components)
58
62
  - [Request Context](#request-context)
@@ -63,6 +67,7 @@ import {
63
67
  - [Real-World Patterns](#real-world-patterns)
64
68
  - [Testing](#testing)
65
69
  - [Performance Tips](#performance-tips)
70
+ - [Documentation](#documentation)
66
71
  - [License](#license)
67
72
 
68
73
  ---
@@ -104,6 +109,12 @@ bun add casbin
104
109
 
105
110
  # Email
106
111
  bun add nodemailer mailgun.js
112
+
113
+ # Search (Typesense)
114
+ bun add typesense
115
+
116
+ # gRPC controller transport
117
+ bun add @connectrpc/connect
107
118
  ```
108
119
 
109
120
  ---
@@ -114,7 +125,8 @@ bun add nodemailer mailgun.js
114
125
 
115
126
  ```typescript
116
127
  // models/user.model.ts
117
- import { BaseEntity, model, generateIdColumnDefs, generateTzColumnDefs } from '@venizia/ignis';
128
+ import { model } from '@venizia/ignis';
129
+ import { BasePostgresEntity, generateIdColumnDefs, generateTzColumnDefs } from '@venizia/ignis/postgres';
118
130
  import { pgTable, text } from 'drizzle-orm/pg-core';
119
131
 
120
132
  @model({
@@ -123,7 +135,7 @@ import { pgTable, text } from 'drizzle-orm/pg-core';
123
135
  hiddenProperties: ['password'],
124
136
  },
125
137
  })
126
- export class User extends BaseEntity<typeof User.schema> {
138
+ export class User extends BasePostgresEntity<typeof User.schema> {
127
139
  static override schema = pgTable('User', {
128
140
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
129
141
  ...generateTzColumnDefs(),
@@ -140,7 +152,8 @@ export class User extends BaseEntity<typeof User.schema> {
140
152
 
141
153
  ```typescript
142
154
  // datasources/postgres.datasource.ts
143
- import { BaseDataSource, datasource, ValueOrPromise } from '@venizia/ignis';
155
+ import { datasource, ValueOrPromise } from '@venizia/ignis';
156
+ import { BasePostgresDataSource } from '@venizia/ignis/postgres';
144
157
  import { drizzle } from 'drizzle-orm/node-postgres';
145
158
  import { Pool } from 'pg';
146
159
 
@@ -150,10 +163,14 @@ interface IDSConfigs {
150
163
  database: string;
151
164
  user: string;
152
165
  password: string;
166
+ // Optional pg Pool tuning (see "Complete DataSource Configuration" below)
167
+ max?: number;
168
+ idleTimeoutMillis?: number;
169
+ connectionTimeoutMillis?: number;
153
170
  }
154
171
 
155
172
  @datasource({ driver: 'node-postgres' })
156
- export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
173
+ export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
157
174
  constructor() {
158
175
  super({
159
176
  name: PostgresDataSource.name,
@@ -185,7 +202,8 @@ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
185
202
 
186
203
  ```typescript
187
204
  // repositories/user.repository.ts
188
- import { PersistableRepository, repository } from '@venizia/ignis';
205
+ import { repository } from '@venizia/ignis';
206
+ import { PersistableRepository } from '@venizia/ignis/postgres';
189
207
  import { User } from '../models/user.model';
190
208
  import { PostgresDataSource } from '../datasources/postgres.datasource';
191
209
 
@@ -201,7 +219,7 @@ export class UserRepository extends PersistableRepository<typeof User.schema> {
201
219
  // controllers/user.controller.ts
202
220
  import {
203
221
  BaseRestController, controller, get, post,
204
- inject, jsonContent, jsonResponse, HTTP, TRouteContext,
222
+ inject, jsonContent, jsonResponse, TRouteContext,
205
223
  } from '@venizia/ignis';
206
224
  import { z } from '@hono/zod-openapi';
207
225
  import { UserRepository } from '../repositories/user.repository';
@@ -209,7 +227,7 @@ import { UserRepository } from '../repositories/user.repository';
209
227
  @controller({ path: '/users' })
210
228
  export class UserController extends BaseRestController {
211
229
  constructor(
212
- @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
230
+ @inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository,
213
231
  ) {
214
232
  super({ scope: UserController.name });
215
233
  }
@@ -225,7 +243,7 @@ export class UserController extends BaseRestController {
225
243
  },
226
244
  })
227
245
  async listUsers(context: TRouteContext) {
228
- const users = await this.userRepo.find({ filter: {} });
246
+ const users = await this.userRepository.find({ filter: {} });
229
247
  return context.json(users, 200);
230
248
  }
231
249
 
@@ -245,7 +263,7 @@ export class UserController extends BaseRestController {
245
263
  })
246
264
  async createUser(context: TRouteContext) {
247
265
  const body = context.req.valid<{ username: string; email: string; password: string }>('json');
248
- const result = await this.userRepo.create({ data: body });
266
+ const result = await this.userRepository.create({ data: body });
249
267
  return context.json(result, 200);
250
268
  }
251
269
  }
@@ -352,7 +370,7 @@ app.start();
352
370
 
353
371
  **`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
372
 
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.
373
+ **`registerControllers()`** -- Dispatches per configured transport (`configs.transports`, default `['rest']`): a `RestComponent` handles REST controllers (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), and, when `'grpc'` is included, a `GrpcComponent` handles gRPC controllers. A gRPC controller discovered without `'grpc'` in `transports` is logged as an error and skipped.
356
374
 
357
375
  ### Key Application Methods
358
376
 
@@ -456,14 +474,16 @@ The runtime is detected via `RuntimeModules.detect()` which checks for the prese
456
474
 
457
475
  ```typescript
458
476
  interface IApplicationConfigs {
459
- host?: string; // Server host (default: 'localhost' or APP_ENV_SERVER_HOST env)
477
+ host?: string; // Server host (default: 'localhost' or APP_ENV_SERVER_HOST/HOST env)
460
478
  port?: number; // Server port (default: 3000 or PORT/APP_ENV_SERVER_PORT env)
461
479
 
462
480
  path: {
463
481
  base: string; // Base path for all routes (e.g., '/api')
464
- isStrict: boolean; // When true, '/users' and '/users/' are different routes
482
+ isStrict: boolean; // Currently unused -- see strictPath below
465
483
  };
466
484
 
485
+ strictPath?: boolean; // When true (default), '/users' and '/users/' are different routes
486
+
467
487
  requestId?: {
468
488
  isStrict: boolean; // Enforce request ID on all requests
469
489
  };
@@ -480,12 +500,16 @@ interface IApplicationConfigs {
480
500
 
481
501
  bootOptions?: IBootOptions; // Convention-based auto-discovery options
482
502
 
503
+ transports?: ('rest' | 'grpc')[]; // Controller transports to enable (default: ['rest'])
504
+
483
505
  debug?: {
484
506
  shouldShowRoutes?: boolean; // Print all registered routes on startup
485
507
  };
486
508
  }
487
509
  ```
488
510
 
511
+ **Note:** the actual Hono strict-routing flag is read from the top-level `strictPath` (defaulting to `true`), not from `path.isStrict` -- `path.isStrict` is declared on the type but not currently read anywhere.
512
+
489
513
  ```typescript
490
514
  interface IApplicationInfo {
491
515
  name: string;
@@ -510,7 +534,7 @@ All controllers extend `BaseRestController`, which provides:
510
534
  - Zod-based request validation with automatic 422 error responses
511
535
 
512
536
  ```typescript
513
- abstract class BaseRestController extends AbstractController {
537
+ abstract class BaseRestController extends AbstractRestController {
514
538
  // Register routes -- override this method
515
539
  abstract binding(): ValueOrPromise<void>;
516
540
 
@@ -538,7 +562,7 @@ Use `@get`, `@post`, `@put`, `@patch`, `@del`, or the generic `@api` decorators.
538
562
  @controller({ path: '/products' })
539
563
  class ProductController extends BaseRestController {
540
564
  constructor(
541
- @inject({ key: 'repositories.ProductRepository' }) private productRepo: ProductRepository,
565
+ @inject({ key: 'repositories.ProductRepository' }) private productRepository: ProductRepository,
542
566
  @inject({ key: 'services.InventoryService' }) private inventoryService: InventoryService,
543
567
  ) {
544
568
  super({ scope: ProductController.name });
@@ -562,7 +586,7 @@ class ProductController extends BaseRestController {
562
586
  },
563
587
  })
564
588
  async list(context: TRouteContext) {
565
- const products = await this.productRepo.find({
589
+ const products = await this.productRepository.find({
566
590
  filter: { order: ['createdAt DESC'], limit: 20 },
567
591
  });
568
592
  return context.json(products, 200);
@@ -586,7 +610,7 @@ class ProductController extends BaseRestController {
586
610
  })
587
611
  async getById(context: TRouteContext) {
588
612
  const { id } = context.req.valid<{ id: number }>('param');
589
- const product = await this.productRepo.findById({ id });
613
+ const product = await this.productRepository.findById({ id });
590
614
  if (!product) {
591
615
  return context.json({ message: 'Product not found' }, 404);
592
616
  }
@@ -620,7 +644,7 @@ class ProductController extends BaseRestController {
620
644
  category: string;
621
645
  description?: string;
622
646
  }>('json');
623
- const result = await this.productRepo.create({ data });
647
+ const result = await this.productRepository.create({ data });
624
648
  return context.json(result, 200);
625
649
  }
626
650
  }
@@ -640,7 +664,7 @@ override binding() {
640
664
  responses: jsonResponse({ schema: z.array(ProductSchema) }),
641
665
  },
642
666
  handler: async (context) => {
643
- const products = await this.productRepo.find({ filter: {} });
667
+ const products = await this.productRepository.find({ filter: {} });
644
668
  return context.json(products, 200);
645
669
  },
646
670
  });
@@ -656,7 +680,7 @@ override binding() {
656
680
  },
657
681
  handler: async (context) => {
658
682
  const { id } = context.req.valid<{ id: number }>('param');
659
- const result = await this.productRepo.deleteById({ id });
683
+ const result = await this.productRepository.deleteById({ id });
660
684
  return context.json(result, 200);
661
685
  },
662
686
  });
@@ -679,7 +703,7 @@ override binding() {
679
703
  }).to({
680
704
  handler: async (context) => {
681
705
  const { id } = context.req.valid<{ id: number }>('param');
682
- const product = await this.productRepo.findById({ id });
706
+ const product = await this.productRepository.findById({ id });
683
707
  return context.json(product, 200);
684
708
  },
685
709
  });
@@ -703,7 +727,7 @@ This means you never manually wire auth middleware -- it is all declarative.
703
727
  You can pass additional Hono middleware to any route:
704
728
 
705
729
  ```typescript
706
- import { rateLimiter } from 'hono/rate-limiter';
730
+ import { rateLimiter } from 'hono-rate-limiter'; // separate package -- Hono ships no built-in rate limiter
707
731
  import { cors } from 'hono/cors';
708
732
 
709
733
  @post({
@@ -737,6 +761,7 @@ Routes automatically validate request parameters, query strings, headers, and bo
737
761
  age: z.number().int().min(18, 'Must be at least 18'),
738
762
  role: z.enum(['admin', 'user', 'moderator']),
739
763
  }),
764
+ description: 'New user data',
740
765
  }),
741
766
  query: z.object({
742
767
  dryRun: z.string().optional().transform(v => v === 'true'),
@@ -760,18 +785,23 @@ On validation failure, the error handler returns:
760
785
 
761
786
  ```json
762
787
  {
763
- "message": "ValidationError",
788
+ "message": "Invalid email format",
789
+ "messageCode": "invalid_format",
764
790
  "statusCode": 422,
765
791
  "requestId": "abc-123",
766
792
  "details": {
793
+ "url": "http://localhost:3000/api/users",
794
+ "path": "/api/users",
767
795
  "cause": [
768
- { "path": "email", "message": "Invalid email format", "code": "invalid_string" },
796
+ { "path": "email", "message": "Invalid email format", "code": "invalid_format" },
769
797
  { "path": "age", "message": "Must be at least 18", "code": "too_small" }
770
798
  ]
771
799
  }
772
800
  }
773
801
  ```
774
802
 
803
+ Top-level `message`/`messageCode` come from the first Zod issue (or a schema-supplied `params.code`, if present); `"ValidationError"` is only used as a fallback when no issue list can be parsed at all. `details.stack` is included alongside `cause` outside production.
804
+
775
805
  ### Accessing Hono Context
776
806
 
777
807
  The `context` parameter (`TRouteContext`) provides full access to the Hono request/response:
@@ -866,6 +896,7 @@ this.defineJSXRoute({
866
896
  method: 'get',
867
897
  description: 'User profile page',
868
898
  authenticate: { strategies: ['jwt'] },
899
+ responses: htmlResponse({ description: 'Rendered profile page' }),
869
900
  },
870
901
  handler: (context) => {
871
902
  const user = context.get('auth.current.user');
@@ -878,7 +909,7 @@ this.defineJSXRoute({
878
909
 
879
910
  | Decorator | Description |
880
911
  | --- | --- |
881
- | `@controller({ path, authenticate? })` | Class decorator -- registers controller path and optional default auth |
912
+ | `@controller({ path })` | Class decorator -- registers controller base path |
882
913
  | `@get({ configs })` | GET route -- method is set automatically |
883
914
  | `@post({ configs })` | POST route |
884
915
  | `@put({ configs })` | PUT route |
@@ -974,7 +1005,7 @@ Each generated endpoint includes:
974
1005
  - Conditional count response via `x-request-count` header -- send `x-request-count: false` to get data only without the wrapping `{ count, data }` object.
975
1006
  - Content-Range header for paginated find results (e.g., `records 0-19/150`).
976
1007
  - Authentication and authorization middleware from controller-level or route-level config.
977
- - `X-Response-Count-Data` response header with the count of returned records.
1008
+ - `X-Response-Count` response header with the count of returned records.
978
1009
 
979
1010
  #### Customizing Controller Factory Routes
980
1011
 
@@ -1007,17 +1038,22 @@ routes: {
1007
1038
  ### Hierarchy
1008
1039
 
1009
1040
  ```
1010
- AbstractRepository
1011
- extends DefaultFilterMixin(FieldsVisibilityMixin(BaseHelper))
1041
+ AbstractRepository<TDataObject, TPersistObject, TOptions>
1042
+ extends BaseHelper (engine-neutral: lazy dataSource/entity resolution, class-keyed
1043
+ | @model settings getters -- hiddenFields/defaultWhere/defaultLimit --
1044
+ | all CRUD verbs abstract)
1012
1045
  |
1013
- +-- ReadableRepository (read operations only -- write operations throw errors)
1014
- | |
1015
- | +-- PersistableRepository (+ create, update, delete operations)
1016
- | |
1017
- | +-- DefaultCRUDRepository (alias -- identical to PersistableRepository)
1046
+ +-- PostgresBaseRepository (+ FilterBuilder, hidden-column query/RETURNING plumbing, drizzle wiring)
1047
+ |
1048
+ +-- ReadableRepository (read operations only -- write operations throw errors)
1049
+ |
1050
+ +-- PersistableRepository (+ create, update, delete operations)
1051
+ |
1052
+ +-- DefaultCRUDRepository (alias -- identical to PersistableRepository)
1053
+ +-- SoftDeletableRepository (extends DefaultCRUDRepository -- soft-delete semantics)
1018
1054
  ```
1019
1055
 
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).
1056
+ `AbstractRepository` is the single engine-neutral base every connector's hierarchy extends directly -- the search branch (Typesense) re-parents its own `TypesenseBaseRepository` onto it the same way. `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
1057
 
1022
1058
  ### Defining a Repository
1023
1059
 
@@ -1061,10 +1097,10 @@ export class UserRepository extends PersistableRepository<typeof User.schema> {
1061
1097
 
1062
1098
  ```typescript
1063
1099
  // Simple count
1064
- const { count } = await repo.count({ where: { status: 'active' } });
1100
+ const { count } = await repository.count({ where: { status: 'active' } });
1065
1101
 
1066
1102
  // Count with complex conditions
1067
- const { count } = await repo.count({
1103
+ const { count } = await repository.count({
1068
1104
  where: {
1069
1105
  and: [
1070
1106
  { role: { inq: ['admin', 'moderator'] } },
@@ -1075,7 +1111,7 @@ const { count } = await repo.count({
1075
1111
  });
1076
1112
 
1077
1113
  // Count within a transaction
1078
- const { count } = await repo.count({
1114
+ const { count } = await repository.count({
1079
1115
  where: { status: 'pending' },
1080
1116
  options: { transaction: tx },
1081
1117
  });
@@ -1084,7 +1120,7 @@ const { count } = await repo.count({
1084
1120
  #### `existsWith()` -- Check Existence
1085
1121
 
1086
1122
  ```typescript
1087
- const emailTaken = await repo.existsWith({
1123
+ const emailTaken = await repository.existsWith({
1088
1124
  where: { email: 'john@example.com' },
1089
1125
  });
1090
1126
 
@@ -1097,7 +1133,7 @@ if (emailTaken) {
1097
1133
 
1098
1134
  ```typescript
1099
1135
  // Basic find with filter
1100
- const users = await repo.find({
1136
+ const users = await repository.find({
1101
1137
  filter: {
1102
1138
  where: { status: 'active' },
1103
1139
  fields: ['id', 'name', 'email'],
@@ -1108,7 +1144,7 @@ const users = await repo.find({
1108
1144
  });
1109
1145
 
1110
1146
  // Find with pagination range info
1111
- const { data, range } = await repo.find({
1147
+ const { data, range } = await repository.find({
1112
1148
  filter: { where: { status: 'active' }, limit: 20, skip: 40 },
1113
1149
  options: { shouldQueryRange: true },
1114
1150
  });
@@ -1116,7 +1152,7 @@ const { data, range } = await repo.find({
1116
1152
  // range = { start: 40, end: 59, total: 150 }
1117
1153
 
1118
1154
  // Find with relation inclusion (uses Query API)
1119
- const usersWithPosts = await repo.find({
1155
+ const usersWithPosts = await repository.find({
1120
1156
  filter: {
1121
1157
  where: { isActive: true },
1122
1158
  include: [
@@ -1126,21 +1162,21 @@ const usersWithPosts = await repo.find({
1126
1162
  });
1127
1163
 
1128
1164
  // Find all (bypass default filter for admin views)
1129
- const allUsers = await repo.find({
1165
+ const allUsers = await repository.find({
1130
1166
  filter: {},
1131
1167
  options: { shouldSkipDefaultFilter: true },
1132
1168
  });
1133
1169
 
1134
1170
  // Find with transaction
1135
- const users = await repo.find({
1171
+ const users = await repository.find({
1136
1172
  filter: { where: { batchId: currentBatch } },
1137
1173
  options: { transaction: tx },
1138
1174
  });
1139
1175
 
1140
- // Find with debug logging
1141
- const users = await repo.find({
1176
+ // Find with row-level locking (requires a transaction; incompatible with include/fields)
1177
+ const users = await repository.find({
1142
1178
  filter: { where: { status: 'active' } },
1143
- options: { log: { use: true, level: 'debug' } },
1179
+ options: { transaction: tx, lock: { strength: 'update' } },
1144
1180
  });
1145
1181
  ```
1146
1182
 
@@ -1149,7 +1185,7 @@ const users = await repo.find({
1149
1185
  `findOne()` accepts a full filter with `where`, `fields`, `include`, and `order`. It returns the first matching record:
1150
1186
 
1151
1187
  ```typescript
1152
- const user = await repo.findOne({
1188
+ const user = await repository.findOne({
1153
1189
  filter: {
1154
1190
  where: { email: 'john@example.com' },
1155
1191
  fields: ['id', 'name', 'email'],
@@ -1162,7 +1198,7 @@ const user = await repo.findOne({
1162
1198
  `findById()` is a convenience wrapper around `findOne()` that automatically sets `where: { id }`. It accepts an optional filter **without** the `where` clause:
1163
1199
 
1164
1200
  ```typescript
1165
- const user = await repo.findById({
1201
+ const user = await repository.findById({
1166
1202
  id: 42,
1167
1203
  filter: {
1168
1204
  fields: ['id', 'name', 'email'],
@@ -1179,20 +1215,20 @@ const user = await repo.findById({
1179
1215
 
1180
1216
  ```typescript
1181
1217
  // Create and return the created record (default: shouldReturn = true)
1182
- const { count, data } = await repo.create({
1218
+ const { count, data } = await repository.create({
1183
1219
  data: { username: 'john', email: 'john@example.com', role: 'user' },
1184
1220
  });
1185
1221
  // count = 1, data = { id: 1, username: 'john', ... }
1186
1222
 
1187
1223
  // Create without returning data (faster -- skips RETURNING clause)
1188
- const { count } = await repo.create({
1224
+ const { count } = await repository.create({
1189
1225
  data: { username: 'john', email: 'john@example.com' },
1190
1226
  options: { shouldReturn: false },
1191
1227
  });
1192
1228
  // count = 1, data = null
1193
1229
 
1194
1230
  // Create within a transaction
1195
- const { data: user } = await repo.create({
1231
+ const { data: user } = await repository.create({
1196
1232
  data: { username: 'john', email: 'john@example.com' },
1197
1233
  options: { transaction: tx },
1198
1234
  });
@@ -1202,7 +1238,7 @@ const { data: user } = await repo.create({
1202
1238
 
1203
1239
  ```typescript
1204
1240
  // Bulk create and return all records
1205
- const { count, data } = await repo.createAll({
1241
+ const { count, data } = await repository.createAll({
1206
1242
  data: [
1207
1243
  { username: 'john', email: 'john@example.com' },
1208
1244
  { username: 'jane', email: 'jane@example.com' },
@@ -1212,7 +1248,7 @@ const { count, data } = await repo.createAll({
1212
1248
  // count = 3, data = [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
1213
1249
 
1214
1250
  // Bulk create without returning (faster for large inserts)
1215
- const { count } = await repo.createAll({
1251
+ const { count } = await repository.createAll({
1216
1252
  data: largeDataArray,
1217
1253
  options: { shouldReturn: false },
1218
1254
  });
@@ -1222,14 +1258,14 @@ const { count } = await repo.createAll({
1222
1258
 
1223
1259
  ```typescript
1224
1260
  // Update by ID and return the updated record
1225
- const { count, data } = await repo.updateById({
1261
+ const { count, data } = await repository.updateById({
1226
1262
  id: 42,
1227
1263
  data: { email: 'new@example.com', status: 'verified' },
1228
1264
  });
1229
1265
  // count = 1, data = { id: 42, email: 'new@example.com', ... }
1230
1266
 
1231
1267
  // Update JSON fields using dot notation
1232
- const { data } = await repo.updateById({
1268
+ const { data } = await repository.updateById({
1233
1269
  id: 42,
1234
1270
  data: {
1235
1271
  'metadata.theme': 'dark',
@@ -1242,14 +1278,14 @@ const { data } = await repo.updateById({
1242
1278
 
1243
1279
  ```typescript
1244
1280
  // Update all matching records
1245
- const { count, data } = await repo.updateAll({
1281
+ const { count, data } = await repository.updateAll({
1246
1282
  data: { status: 'inactive' },
1247
1283
  where: { lastLoginAt: { lt: new Date('2024-01-01') } },
1248
1284
  });
1249
1285
  // count = 25, data = [...25 updated records...]
1250
1286
 
1251
1287
  // updateBy is an alias for updateAll
1252
- const { count } = await repo.updateBy({
1288
+ const { count } = await repository.updateBy({
1253
1289
  data: { isNotified: true },
1254
1290
  where: { role: 'subscriber' },
1255
1291
  options: { shouldReturn: false },
@@ -1257,7 +1293,7 @@ const { count } = await repo.updateBy({
1257
1293
 
1258
1294
  // SAFETY: Empty where throws an error to prevent accidental mass updates
1259
1295
  // Use force: true to explicitly allow it
1260
- const { count } = await repo.updateAll({
1296
+ const { count } = await repository.updateAll({
1261
1297
  data: { version: 2 },
1262
1298
  where: {},
1263
1299
  options: { force: true },
@@ -1268,22 +1304,22 @@ const { count } = await repo.updateAll({
1268
1304
 
1269
1305
  ```typescript
1270
1306
  // Delete by ID (returns deleted record)
1271
- const { count, data } = await repo.deleteById({ id: 42 });
1307
+ const { count, data } = await repository.deleteById({ id: 42 });
1272
1308
  // count = 1, data = { id: 42, username: 'john', ... }
1273
1309
 
1274
1310
  // Delete all matching records
1275
- const { count, data } = await repo.deleteAll({
1311
+ const { count, data } = await repository.deleteAll({
1276
1312
  where: { status: 'inactive' },
1277
1313
  });
1278
1314
 
1279
1315
  // deleteBy is an alias for deleteAll
1280
- const { count } = await repo.deleteBy({
1316
+ const { count } = await repository.deleteBy({
1281
1317
  where: { expiresAt: { lt: new Date() } },
1282
1318
  options: { shouldReturn: false },
1283
1319
  });
1284
1320
 
1285
1321
  // SAFETY: Empty where throws an error. Use force: true to allow.
1286
- const { count } = await repo.deleteAll({
1322
+ const { count } = await repository.deleteAll({
1287
1323
  where: {},
1288
1324
  options: { force: true, shouldReturn: false },
1289
1325
  });
@@ -1299,95 +1335,28 @@ interface TFilter<T> {
1299
1335
  order?: string[]; // Sorting (e.g., ['createdAt DESC', 'name ASC'])
1300
1336
  limit?: number; // Max results (default: 10)
1301
1337
  skip?: number; // Offset
1302
- offset?: number; // Alias for skip
1338
+ offset?: number; // Fallback for skip -- if both are set, skip wins
1303
1339
  }
1304
1340
  ```
1305
1341
 
1306
- #### Where Operators -- Complete Reference
1307
-
1308
- **Comparison operators:**
1342
+ #### Where Operators
1309
1343
 
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):
1344
+ Comparison (`eq`/`neq`/`gt`/`gte`/`lt`/`lte`), pattern matching (`like`/`ilike`/`regexp` and negations), array/set (`inq`/`nin`/`between`/`notBetween`), null checks (`is`/`isn`), logical (`and`/`or`), and PostgreSQL array-column operators (`contains`/`containedBy`/`overlaps`) are all supported. JSON path queries (dot notation into `json`/`jsonb` columns, with automatic numeric casting for comparison operators) and JSON path sorting work the same way. Full operator tables: [Filter System Quick Reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/filter-system/quick-reference.md).
1363
1345
 
1364
1346
  ```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
- },
1347
+ // Comparison + pattern matching
1348
+ const users = await repository.find({
1349
+ filter: { where: { age: { gte: 18 }, email: { ilike: '%@gmail.com' } } },
1375
1350
  });
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
1351
 
1380
- **Sorting with JSON paths:**
1352
+ // Array/set + logical
1353
+ const users = await repository.find({
1354
+ filter: { where: { and: [{ status: { inq: ['active', 'pending'] } }, { role: { neq: 'guest' } }] } },
1355
+ });
1381
1356
 
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
- },
1357
+ // JSON path query (dot notation into a jsonb column)
1358
+ const users = await repository.find({
1359
+ filter: { where: { 'metadata.score': { gt: 50, lte: 100 } } },
1391
1360
  });
1392
1361
  ```
1393
1362
 
@@ -1395,12 +1364,12 @@ const products = await repo.find({
1395
1364
 
1396
1365
  ```typescript
1397
1366
  // Array format -- include only these columns
1398
- const users = await repo.find({
1367
+ const users = await repository.find({
1399
1368
  filter: { fields: ['id', 'name', 'email'] },
1400
1369
  });
1401
1370
 
1402
1371
  // Object format -- include/exclude
1403
- const users = await repo.find({
1372
+ const users = await repository.find({
1404
1373
  filter: { fields: { id: true, name: true, password: false } },
1405
1374
  });
1406
1375
  ```
@@ -1409,14 +1378,14 @@ const users = await repo.find({
1409
1378
 
1410
1379
  ```typescript
1411
1380
  // Simple inclusion
1412
- const users = await repo.find({
1381
+ const users = await repository.find({
1413
1382
  filter: {
1414
1383
  include: [{ relation: 'posts' }],
1415
1384
  },
1416
1385
  });
1417
1386
 
1418
1387
  // With nested filter (scope)
1419
- const users = await repo.find({
1388
+ const users = await repository.find({
1420
1389
  filter: {
1421
1390
  include: [{
1422
1391
  relation: 'posts',
@@ -1431,7 +1400,7 @@ const users = await repo.find({
1431
1400
  });
1432
1401
 
1433
1402
  // Skip default filter on a specific relation
1434
- const users = await repo.find({
1403
+ const users = await repository.find({
1435
1404
  filter: {
1436
1405
  include: [{
1437
1406
  relation: 'archivedPosts',
@@ -1448,7 +1417,7 @@ Note: Relations are defined on the model via `static relations`. The FilterBuild
1448
1417
  When `shouldQueryRange: true` is passed to `find()`, the method runs both the data fetch and a count query in parallel, then returns:
1449
1418
 
1450
1419
  ```typescript
1451
- const result = await repo.find({
1420
+ const result = await repository.find({
1452
1421
  filter: { where: { status: 'active' }, limit: 10, skip: 20 },
1453
1422
  options: { shouldQueryRange: true },
1454
1423
  });
@@ -1465,19 +1434,19 @@ The `shouldSkipDefaultFilter` option bypasses the model's `defaultFilter`. Commo
1465
1434
 
1466
1435
  ```typescript
1467
1436
  // Admin panel showing all records (including soft-deleted)
1468
- const allUsers = await repo.find({
1437
+ const allUsers = await repository.find({
1469
1438
  filter: {},
1470
1439
  options: { shouldSkipDefaultFilter: true },
1471
1440
  });
1472
1441
 
1473
1442
  // Data migration or cleanup script
1474
- const deletedUsers = await repo.find({
1443
+ const deletedUsers = await repository.find({
1475
1444
  filter: { where: { isDeleted: true } },
1476
1445
  options: { shouldSkipDefaultFilter: true },
1477
1446
  });
1478
1447
 
1479
1448
  // Export all data for backup
1480
- const everything = await repo.find({
1449
+ const everything = await repository.find({
1481
1450
  filter: {},
1482
1451
  options: { shouldSkipDefaultFilter: true, shouldQueryRange: true },
1483
1452
  });
@@ -1485,32 +1454,41 @@ const everything = await repo.find({
1485
1454
 
1486
1455
  ### ExtraOptions
1487
1456
 
1488
- All repository operations accept an `options` parameter:
1457
+ All repository operations accept an `options` parameter. The base shape is `IExtraOptions`:
1489
1458
 
1490
1459
  ```typescript
1491
1460
  interface IExtraOptions {
1492
1461
  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 } }
1462
+ log?: { use: boolean; level?: TLogLevel }; // Enable operation logging (write operations only)
1495
1463
  shouldSkipDefaultFilter?: boolean; // Bypass model's default filter
1496
- log?: { use: boolean; level?: TLogLevel }; // Enable operation logging
1464
+ lock?: { // Row-level locking. Requires a transaction; incompatible
1465
+ strength: 'update' | 'no key update' | 'share' | 'key share'; // with include/fields (Query API)
1466
+ config?: { noWait?: true } | { skipLocked?: true };
1467
+ };
1497
1468
  }
1498
1469
  ```
1499
1470
 
1500
- ### Mixins
1471
+ `shouldReturn` and `shouldQueryRange` are **not** part of `IExtraOptions` -- they only exist as ad-hoc intersections on the specific methods that support them: `shouldReturn` on `create`/`createAll`/`updateById`/`updateAll`/`deleteById`/`deleteAll` (`options: IExtraOptions & { shouldReturn?: boolean }`), and `shouldQueryRange` only on `find` (`options: IExtraOptions & { shouldQueryRange?: boolean }`). `count`/`existsWith` accept neither.
1472
+
1473
+ `log: { use: true }` is only honored by write operations (`create`/`updateById`/`deleteById`/etc.) -- read operations (`find`/`findOne`/`count`/`existsWith`) never inspect it.
1474
+
1475
+ ### Hidden Fields and Default Filter
1476
+
1477
+ This behavior lives directly on the repository hierarchy now, not in separate mixins: `AbstractRepository` (`src/base/repositories/core/`) exposes the class-keyed `@model` settings as protected getters -- `hiddenFields`, `defaultWhere`, `defaultLimit` -- resolved once per entity class and memoized; `PostgresBaseRepository` (`src/connectors/postgres/repositories/core/`) is where those getters actually get applied to SQL, via its own `getHiddenProperties()`/`getVisibleProperties()`/`buildQuery()`/`applyDefaultFilter()`. The two mixins this section used to describe (`FieldsVisibilityMixin`, `DefaultFilterMixin`, under `src/connectors/postgres/repositories/mixins/`) are legacy -- no longer composed onto any repository class -- and are not exported from any barrel.
1501
1478
 
1502
- #### FieldsVisibilityMixin
1479
+ #### Hidden fields
1503
1480
 
1504
1481
  Automatically excludes properties listed in `@model({ settings: { hiddenProperties } })` from all query results at the SQL level -- not post-processing:
1505
1482
 
1506
1483
  ```typescript
1507
1484
  @model({
1485
+ type: 'entity',
1508
1486
  settings: { hiddenProperties: ['password', 'secretToken'] },
1509
1487
  })
1510
- export class User extends BaseEntity<typeof User.schema> { ... }
1488
+ export class User extends BasePostgresEntity<typeof User.schema> { ... }
1511
1489
 
1512
1490
  // All repository queries automatically exclude 'password' and 'secretToken'
1513
- const user = await userRepo.findById({ id: 1 });
1491
+ const user = await userRepository.findById({ id: 1 });
1514
1492
  // user.password === undefined (never selected from DB)
1515
1493
 
1516
1494
  // Hidden fields are excluded from:
@@ -1521,29 +1499,30 @@ const user = await userRepo.findById({ id: 1 });
1521
1499
  // - Included relation queries (applied recursively)
1522
1500
  ```
1523
1501
 
1524
- The mixin caches the visible property set for performance. It computes it once from the schema columns minus hidden properties.
1502
+ `PostgresBaseRepository.getVisibleProperties()` caches the visible property set for performance. It computes it once from the schema columns minus hidden properties.
1525
1503
 
1526
- #### DefaultFilterMixin
1504
+ #### Default filter
1527
1505
 
1528
1506
  Automatically applies a default filter to all queries. Common use case -- soft delete:
1529
1507
 
1530
1508
  ```typescript
1531
1509
  @model({
1510
+ type: 'entity',
1532
1511
  settings: { defaultFilter: { where: { isDeleted: false } } },
1533
1512
  })
1534
- export class User extends BaseEntity<typeof User.schema> { ... }
1513
+ export class User extends BasePostgresEntity<typeof User.schema> { ... }
1535
1514
 
1536
1515
  // All queries automatically add WHERE is_deleted = false
1537
- const users = await userRepo.find({ filter: {} });
1516
+ const users = await userRepository.find({ filter: {} });
1538
1517
 
1539
1518
  // The default filter merges with user-provided filters:
1540
- const activeAdmins = await userRepo.find({
1519
+ const activeAdmins = await userRepository.find({
1541
1520
  filter: { where: { role: 'admin' } },
1542
1521
  });
1543
1522
  // SQL: WHERE is_deleted = false AND role = 'admin'
1544
1523
 
1545
1524
  // Bypass when needed (e.g., admin panel showing all records)
1546
- const allUsers = await userRepo.find({
1525
+ const allUsers = await userRepository.find({
1547
1526
  filter: {},
1548
1527
  options: { shouldSkipDefaultFilter: true },
1549
1528
  });
@@ -1581,13 +1560,14 @@ Multiple path updates to the same JSON column are chained into a single expressi
1581
1560
 
1582
1561
  ## Models
1583
1562
 
1584
- ### BaseEntity
1563
+ ### BasePostgresEntity
1585
1564
 
1586
- All entities extend `BaseEntity` and define a static `schema` using Drizzle's `pgTable`:
1565
+ All entities extend `BasePostgresEntity` and define a static `schema` using Drizzle's `pgTable`:
1587
1566
 
1588
1567
  ```typescript
1589
- import { BaseEntity, model } from '@venizia/ignis';
1590
- import { pgTable, text, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
1568
+ import { model } from '@venizia/ignis';
1569
+ import { BasePostgresEntity, TRelationConfig } from '@venizia/ignis/postgres';
1570
+ import { pgTable, text, jsonb, boolean } from 'drizzle-orm/pg-core';
1591
1571
 
1592
1572
  @model({
1593
1573
  type: 'entity',
@@ -1596,7 +1576,7 @@ import { pgTable, text, integer, jsonb, boolean } from 'drizzle-orm/pg-core';
1596
1576
  defaultFilter: { where: { isDeleted: false } },
1597
1577
  },
1598
1578
  })
1599
- export class User extends BaseEntity<typeof User.schema> {
1579
+ export class User extends BasePostgresEntity<typeof User.schema> {
1600
1580
  static override schema = pgTable('User', {
1601
1581
  ...generateIdColumnDefs({ id: { dataType: 'string' } }),
1602
1582
  ...generateTzColumnDefs({
@@ -1612,12 +1592,14 @@ export class User extends BaseEntity<typeof User.schema> {
1612
1592
  metadata: jsonb('metadata').$type<{ theme?: string; score?: number }>(),
1613
1593
  });
1614
1594
 
1615
- static override relations = () => [
1616
- { name: 'posts', type: 'many', schema: Post.schema, metadata: { fields: [Post.schema.authorId], references: [User.schema.id] } },
1595
+ static override relations = (): TRelationConfig[] => [
1596
+ // 'many' side: no fields/references -- relationName points at the 'one' relation declared on Post
1597
+ { name: 'posts', type: 'many', schema: Post.schema, metadata: { relationName: 'author' } },
1598
+ // 'one' side: fields/references establish the FK, since it's declared on this table
1617
1599
  { name: 'profile', type: 'one', schema: Profile.schema, metadata: { fields: [Profile.schema.userId], references: [User.schema.id] } },
1618
1600
  ];
1619
1601
 
1620
- static TABLE_NAME = 'User';
1602
+ static override TABLE_NAME = 'User';
1621
1603
  }
1622
1604
  ```
1623
1605
 
@@ -1641,7 +1623,7 @@ When `@model` is applied, it registers the class with the `MetadataRegistry`, ex
1641
1623
 
1642
1624
  ### Schema Generation
1643
1625
 
1644
- `BaseEntity` provides `getSchema()` for Zod schema generation from the Drizzle table using `drizzle-zod`:
1626
+ `BasePostgresEntity` provides `getSchema()` for Zod schema generation from the Drizzle table using `drizzle-zod`:
1645
1627
 
1646
1628
  ```typescript
1647
1629
  const entity = new User();
@@ -1651,26 +1633,25 @@ entity.getSchema({ type: 'create' }); // Zod schema for INSERT data -- required
1651
1633
  entity.getSchema({ type: 'update' }); // Zod schema for UPDATE data -- all fields optional (partial)
1652
1634
  ```
1653
1635
 
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.
1636
+ These schemas are used by `ControllerFactory` to auto-generate OpenAPI documentation. The schema factory is lazily initialized and shared across all BasePostgresEntity instances for performance.
1655
1637
 
1656
1638
  ### Static `relations` Definition
1657
1639
 
1658
1640
  Relations are defined as a static property or function returning an array of `TRelationConfig`:
1659
1641
 
1660
1642
  ```typescript
1661
- static override relations = () => [
1643
+ static override relations = (): TRelationConfig[] => [
1662
1644
  {
1663
1645
  name: 'posts',
1664
- type: 'many', // RelationTypes.MANY
1665
- schema: Post.schema,
1646
+ type: 'many', // RelationTypes.MANY -- no fields/references; relationName points
1647
+ schema: Post.schema, // at the 'one' relation declared on the other side (Post)
1666
1648
  metadata: {
1667
- fields: [Post.schema.authorId],
1668
- references: [User.schema.id],
1649
+ relationName: 'author',
1669
1650
  },
1670
1651
  },
1671
1652
  {
1672
1653
  name: 'profile',
1673
- type: 'one', // RelationTypes.ONE
1654
+ type: 'one', // RelationTypes.ONE -- the FK-owning side declares fields/references
1674
1655
  schema: Profile.schema,
1675
1656
  metadata: {
1676
1657
  fields: [Profile.schema.userId],
@@ -1682,134 +1663,30 @@ static override relations = () => [
1682
1663
 
1683
1664
  These relations are used by the `FilterBuilder` when processing `include` in filters, and by the DataSource's `discoverSchema()` to build Drizzle relation definitions.
1684
1665
 
1685
- ### Enrichers -- Complete Reference
1666
+ ### Enrichers
1686
1667
 
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
1668
+ Column definition helpers that spread common column patterns into a `pgTable` schema:
1695
1669
 
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
1670
+ | Enricher | Adds |
1671
+ | --- | --- |
1672
+ | `generateIdColumnDefs()` | Primary key column -- auto-increment integer (default), UUID string, big-number, or custom string generator |
1673
+ | `generateTzColumnDefs()` | `created_at`/`modified_at` timestamps (with timezone, auto `$onUpdate`), optional `deleted_at` for soft delete |
1674
+ | `generateUserAuditColumnDefs()` | `created_by`/`modified_by`, auto-populated from `context.get('audit.user.id')` on create/update |
1675
+ | `generatePrincipalColumnDefs()` | Polymorphic discriminator pair (`principal_id`/`principal_type` by default) |
1676
+ | `generateDataTypeColumnDefs()` | Multi-type value columns (`data_type` + `n_value`/`t_value`/`b_value`/`j_value`/`bo_value`) for key-value/settings tables |
1796
1677
 
1797
- // With defaults
1798
- ...generateDataTypeColumnDefs({
1799
- defaultValue: { dataType: 'text', tValue: 'default' },
1800
- })
1801
- ```
1678
+ Full options and generated-column tables: [Models reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/models.md).
1802
1679
 
1803
1680
  ---
1804
1681
 
1805
1682
  ## DataSources
1806
1683
 
1807
- ### BaseDataSource
1684
+ ### BasePostgresDataSource
1808
1685
 
1809
1686
  DataSources manage database connections and provide Drizzle connectors:
1810
1687
 
1811
1688
  ```typescript
1812
- abstract class BaseDataSource<Settings, Schema> extends AbstractDataSource {
1689
+ abstract class BasePostgresDataSource<Settings, Schema> extends AbstractPostgresDataSource {
1813
1690
  // Implemented by subclass
1814
1691
  abstract configure(): ValueOrPromise<void>;
1815
1692
  abstract getConnectionString(): ValueOrPromise<string>;
@@ -1820,16 +1697,21 @@ abstract class BaseDataSource<Settings, Schema> extends AbstractDataSource {
1820
1697
  // Check if any repositories reference this datasource
1821
1698
  hasDiscoverableModels(): boolean;
1822
1699
 
1823
- // Transaction support
1824
- beginTransaction(opts?: ITransactionOptions): Promise<ITransaction>;
1700
+ // Transaction support -- postgres's own IDatabaseTransactionOptions/IDatabaseTransaction<Schema>,
1701
+ // narrowing the neutral ITransactionOptions/ITransaction from AbstractDataSource
1702
+ beginTransaction(opts?: IDatabaseTransactionOptions): Promise<IDatabaseTransaction<Schema>>;
1825
1703
  }
1826
1704
  ```
1827
1705
 
1706
+ #### Capability Probe
1707
+
1708
+ Every datasource -- regardless of connector -- exposes `getCapabilities(): { transactions: boolean }`. `AbstractDataSource` (the engine-neutral root every connector extends) defaults it to `{ transactions: false }`, paired with a `beginTransaction()` default that throws the standardized NotSupported error (`core.not_supported`, HTTP 501). `BasePostgresDataSource` (postgres) is the one connector that overrides both: `getCapabilities()` reports `{ transactions: true }`, and `beginTransaction()` is a real implementation (below) instead of the inherited throw. `TypesenseDataSource` adds no override -- it inherits the neutral NotSupported default from `AbstractDataSource` directly. Check `getCapabilities().transactions` before calling `beginTransaction()` on a datasource whose connector isn't known ahead of time.
1709
+
1828
1710
  ### Complete DataSource Configuration
1829
1711
 
1830
1712
  ```typescript
1831
1713
  @datasource({ driver: 'node-postgres' })
1832
- export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
1714
+ export class PostgresDataSource extends BasePostgresDataSource<IDSConfigs> {
1833
1715
  constructor() {
1834
1716
  super({
1835
1717
  name: PostgresDataSource.name,
@@ -1879,12 +1761,12 @@ const tx = await dataSource.beginTransaction({
1879
1761
  });
1880
1762
 
1881
1763
  try {
1882
- await userRepo.create({
1764
+ await userRepository.create({
1883
1765
  data: { username: 'john', email: 'john@example.com' },
1884
1766
  options: { transaction: tx },
1885
1767
  });
1886
1768
 
1887
- await auditRepo.create({
1769
+ await auditRepository.create({
1888
1770
  data: { action: 'user_created', userId: newUser.id },
1889
1771
  options: { transaction: tx },
1890
1772
  });
@@ -1896,16 +1778,7 @@ try {
1896
1778
  }
1897
1779
  ```
1898
1780
 
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.
1781
+ `beginTransaction()` acquires a dedicated `PoolClient` and returns an `IDatabaseTransaction<Schema>` (`connector`, `commit()`, `rollback()`, `isolationLevel`, `isActive`); passing `{ transaction: tx }` to a repository method routes it through that dedicated connector instead of the DataSource's shared pool, and `commit()`/`rollback()` always release the client back to the pool. Full internals: [Transactions guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/persistent/transactions.md).
1909
1782
 
1910
1783
  #### Isolation Levels
1911
1784
 
@@ -1921,6 +1794,191 @@ Connections are always released back to the pool in the `finally` block of both
1921
1794
 
1922
1795
  ---
1923
1796
 
1797
+ ## Search
1798
+
1799
+ IGNIS has a second, engine-neutral branch for search-only entities backed by Typesense instead of Postgres. It mirrors the SQL branch -- same `@model`/`@repository` decorators, same `TFilter`/where-operator vocabulary, same hidden-field/default-filter/default-limit behavior -- but returns a slightly different envelope in a few places (see below).
1800
+
1801
+ ### Search Entities
1802
+
1803
+ Search entities extend `BaseSearchEntity` and define a static `definition` via `defineSearchCollection` + the `field` DSL, instead of Drizzle's `pgTable`. Same as `BasePostgresEntity`, apply `@model` so `hiddenProperties`/`defaultFilter`/`defaultLimit` are registered:
1804
+
1805
+ ```typescript
1806
+ import { model } from '@venizia/ignis';
1807
+ import { BaseSearchEntity, defineSearchCollection, field } from '@venizia/ignis/typesense';
1808
+
1809
+ @model({
1810
+ type: 'entity',
1811
+ settings: { hiddenProperties: ['internalNotes'], defaultLimit: 20 },
1812
+ })
1813
+ export class Product extends BaseSearchEntity {
1814
+ static override definition = defineSearchCollection({
1815
+ name: 'products',
1816
+ fields: [
1817
+ field.id(),
1818
+ field.string('name', { searchable: true, sortable: true }),
1819
+ field.number('price', { filterable: true, sortable: true }),
1820
+ field.string('internalNotes', { optional: true }),
1821
+ ],
1822
+ defaultSort: 'price',
1823
+ });
1824
+ }
1825
+ ```
1826
+
1827
+ `field.id()` is optional -- `defineSearchCollection` prepends it automatically when missing. `defaultSort` must reference a `number` field; Typesense's `default_sorting_field` only accepts a scalar numeric field, never a string or array.
1828
+
1829
+ ### Search DataSources
1830
+
1831
+ `TypesenseDataSource` is a `BaseSearchDataSource` subclass, imported from a dedicated sub-path so the root `@venizia/ignis` barrel never pulls in the `typesense` client:
1832
+
1833
+ ```typescript
1834
+ import { DataSourceDrivers, datasource } from '@venizia/ignis';
1835
+ import { TypesenseDataSource } from '@venizia/ignis/typesense';
1836
+
1837
+ @datasource({ driver: DataSourceDrivers.TYPESENSE })
1838
+ export class ProductSearchDataSource extends TypesenseDataSource {
1839
+ constructor() {
1840
+ super({
1841
+ name: ProductSearchDataSource.name,
1842
+ config: {
1843
+ nodes: [{ host: process.env.TYPESENSE_HOST!, port: +(process.env.TYPESENSE_PORT ?? 8108) }],
1844
+ apiKey: process.env.TYPESENSE_API_KEY!,
1845
+ },
1846
+ });
1847
+ }
1848
+ }
1849
+ ```
1850
+
1851
+ `typesense` is an optional peer dependency -- install it yourself (`bun add typesense`) if you use the search branch.
1852
+
1853
+ Same as the postgres branch, collections are discovered from `@repository` bindings and auto-provisioned (via `ensureCollection()`, additive-only) when the DataSource's `configure()` runs during boot. Opt out with:
1854
+
1855
+ - `autoProvision: false` in the `super({ ... })` call -- skips provisioning, discovery still runs.
1856
+ - `@datasource({ autoDiscovery: false })` -- skips discovery entirely (`getSchema()` returns `{}`).
1857
+
1858
+ `getClient()` returns the raw typesense `Client`, an escape hatch symmetric to the SQL branch's `pg.Pool` access:
1859
+
1860
+ ```typescript
1861
+ const client = productSearchDataSource.getClient();
1862
+ ```
1863
+
1864
+ ### Search Repositories
1865
+
1866
+ Same tiered hierarchy as the SQL branch, over Typesense instead of Postgres:
1867
+
1868
+ ```
1869
+ ReadableSearchRepository (read operations only)
1870
+ |
1871
+ +-- PersistableSearchRepository (+ create, update)
1872
+ |
1873
+ +-- DefaultSearchRepository (+ delete)
1874
+ ```
1875
+
1876
+ `TInferSearchDocument<typeof X.schema>` derives the document type straight from the collection definition -- same idea as postgres's `typeof User.schema` inference, no hand-written interface needed:
1877
+
1878
+ ```typescript
1879
+ import { repository } from '@venizia/ignis';
1880
+ import { DefaultSearchRepository, TInferSearchDocument } from '@venizia/ignis/typesense';
1881
+
1882
+ type ProductDocument = TInferSearchDocument<typeof Product.schema>;
1883
+ // -> { id: string; name: string; price: number; internalNotes?: string }
1884
+
1885
+ @repository({ model: Product, dataSource: ProductSearchDataSource })
1886
+ export class ProductSearchRepository extends DefaultSearchRepository<ProductDocument> {
1887
+ // No constructor needed -- dataSource auto-injected from @repository metadata
1888
+ }
1889
+ ```
1890
+
1891
+ `TInferSearchDocument` only resolves field-level types when the collection is captured through `defineSearchCollection`'s `const` type param -- true as long as you call it directly on a `static schema = defineSearchCollection({ ... })` assignment, as above. `id` is always inferred as a required `string`; fields marked `{ optional: true }` become optional properties, everything else is required.
1892
+
1893
+ `@repository`, `TFilter`, and the where operators (`eq`, `gt`, `inq`, `and`/`or`, ...) work the same as the SQL branch -- `TFilter`/`TWhere` is translated into Typesense search params by `TypesenseQueryDialect`. Operators that can't be expressed as a Typesense `filter_by` throw instead of silently degrading:
1894
+
1895
+ - `like` / `ilike` (and their negations)
1896
+ - JSON-path fields (e.g. `'metadata.score'`)
1897
+ - `include` (relations)
1898
+
1899
+ Reach for the repository's `search()` method when you need one of these -- it's a raw passthrough to the driver (no dialect translation, no default filter applied).
1900
+
1901
+ `hiddenProperties`, `defaultFilter`, and `defaultLimit` from `@model` settings are honored the same way they are for the SQL branch: hidden fields become Typesense `exclude_fields`, `defaultFilter` is AND-merged into every query, and an omitted `limit` falls back to `defaultLimit` then `DEFAULT_LIMIT` (10).
1902
+
1903
+ Note "the same way" covers hidden-fields/limit fallback, not the `where` merge policy: the search branch (like memory) always AND-merges `defaultFilter.where` with the user's `where`, while Postgres deep-merges with the user filter winning key-by-key (`merge({}, defaultWhere, userWhere)`, `filter.ts`) -- see the memory connector section below for the concrete difference.
1904
+
1905
+ ### Envelope Differences vs SQL
1906
+
1907
+ `create()`/`createAll()` are overloaded by `shouldReturn` the same way on both branches, and every write verb now returns `{ count, data }` on both branches too -- but Typesense has no `RETURNING` equivalent, so a few return shapes and constraints still differ underneath that shared envelope:
1908
+
1909
+ | Aspect | SQL | Search |
1910
+ | --- | --- | --- |
1911
+ | `deleteById()` `data` | The deleted row, straight from `RETURNING` | Populated only when the model has a `defaultFilter` -- its guard read (a `findById()` done to check the row isn't excluded) is the only document Typesense ever hands back. No `defaultFilter` means no guard read, so `data` is `null` even with `shouldReturn: true` |
1912
+ | `deleteAll()` / `deleteBy()` `data` | The deleted rows via `RETURNING`, always in lockstep with `count` | A `find()` snapshot taken *before* the delete, capped at `defaultLimit`/`DEFAULT_LIMIT` (10) like any other read -- `count` (the engine's real deleted-row count) can exceed `data.length` when more rows matched than the snapshot's page size. Truncating (no effective `where`) reports `{ count: 0, data: [] }` -- Typesense's truncate reports no per-document count at all |
1913
+ | `updateAll()` / `updateBy()` `data` | The updated rows via `RETURNING`, always in lockstep with `count` | A `find()` snapshot taken *after* the update, subject to the same pagination cap as `deleteAll()` -- can likewise diverge from `count` |
1914
+ | `skip` | Any offset | Must be a multiple of `limit` -- Typesense paginates by page, not row offset |
1915
+ | Transactions / row-level locks | `beginTransaction()` / `{ transaction }`, `{ lock }` in options | Neither is supported -- both throw the standardized NotSupported error |
1916
+
1917
+ ### Adding a Search Engine
1918
+
1919
+ A new engine is one folder under `src/connectors/<engine>/` -- `DataSource`, `Driver`, a paired `QueryDialect` (never mixed with another engine's driver), a compiler (neutral DSL -> engine schema), a repository tier re-parented onto the shared `AbstractRepository`, and an internal error classifier -- sub-path exported (`@venizia/ignis/<engine>`) with the client as an optional peer dependency, the same convention `typesense/` follows today. Full contract (fixed `ISearchDriver` verb set, escape-hatch extras, transaction posture): [Connectors reference](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/references/base/connectors.md).
1920
+
1921
+ ---
1922
+
1923
+ ## Memory Connector
1924
+
1925
+ `MemoryDataSource` + `MemoryRepository` (`src/connectors/memory/`) are a zero-dependency, Map-backed engine for prototyping and tests -- the same role LoopBack 4's memory connector plays. No client library, no network, no peer dependency. Import it from the root (`@venizia/ignis`) or from its explicit sub-path (`@venizia/ignis/memory`) - both doors resolve to the same modules. Only optional-peer connectors (typesense) are sub-path-only:
1926
+
1927
+ ```typescript
1928
+ import {
1929
+ AbstractEntity,
1930
+ SchemaTypes,
1931
+ TSchemaType,
1932
+ model,
1933
+ repository,
1934
+ } from '@venizia/ignis';
1935
+ import { MemoryDataSource, MemoryRepository } from '@venizia/ignis/memory';
1936
+ import { z } from '@hono/zod-openapi';
1937
+
1938
+ const ProductSelectSchema = z.object({ id: z.string(), title: z.string(), price: z.number() });
1939
+ type ProductDocument = z.infer<typeof ProductSelectSchema>;
1940
+
1941
+ @model({ type: 'entity', settings: { defaultLimit: 20 } })
1942
+ class Product extends AbstractEntity {
1943
+ static readonly COLLECTION_NAME = 'products';
1944
+
1945
+ constructor() {
1946
+ super({ name: Product.COLLECTION_NAME });
1947
+ }
1948
+
1949
+ getSchema(opts: { type: TSchemaType }) {
1950
+ switch (opts.type) {
1951
+ case SchemaTypes.SELECT:
1952
+ return ProductSelectSchema;
1953
+ default:
1954
+ return ProductSelectSchema;
1955
+ }
1956
+ }
1957
+ }
1958
+
1959
+ class ProductDataSource extends MemoryDataSource {
1960
+ constructor() {
1961
+ super({ name: ProductDataSource.name });
1962
+ }
1963
+ }
1964
+
1965
+ @repository({ model: Product, dataSource: ProductDataSource })
1966
+ class ProductRepository extends MemoryRepository<ProductDocument> {
1967
+ // No constructor needed -- dataSource auto-injected from @repository metadata
1968
+ }
1969
+
1970
+ const productRepository = new ProductRepository(new ProductDataSource());
1971
+ const products = await productRepository.find({ filter: { where: { price: { gt: 10 } }, order: ['price DESC'] } });
1972
+ ```
1973
+
1974
+ A static `COLLECTION_NAME` on the model is this connector's discoverable-model convention -- the equivalent of postgres's static `schema` or the search branch's static `definition`. `MemoryRepository` is a single, untiered class implementing every `AbstractRepository` verb (no Readable/Persistable/DefaultCRUD split -- there's no connection/dialect plumbing to layer progressively), supporting equality, comparison (`gt`/`gte`/`lt`/`lte`/`between`), pattern (`like`/`ilike` via anchored `RegExp`), array (`inq`/`nin`), and logical (`and`/`or`) operators, plus `order`/`skip`/`limit`/`fields`. Operators with no faithful plain-JS meaning (`is`/`isn`, `regexp`/`iregexp`, the PostgreSQL-only array operators, `include`) throw instead of guessing.
1975
+
1976
+ `hiddenProperties`, `defaultFilter`, and `defaultLimit` from `@model` settings are honored the same way every connector honors them, with the same AND-merge default-filter policy as the search branch (narrows rather than overrides, unlike postgres's key-by-key deep merge). No transactions or row locks -- both reject with the standardized NotSupported error. Ids default to `crypto.randomUUID()` when a document is created without one.
1977
+
1978
+ Full operator matrix and internals: [Memory Connector guide](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/persistent/memory-connector.md).
1979
+
1980
+ ---
1981
+
1924
1982
  ## Services
1925
1983
 
1926
1984
  Services encapsulate business logic and are registered in the DI container:
@@ -1930,21 +1988,21 @@ import { BaseService, inject } from '@venizia/ignis';
1930
1988
 
1931
1989
  export class UserService extends BaseService {
1932
1990
  constructor(
1933
- @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
1934
- @inject({ key: 'repositories.AuditRepository' }) private auditRepo: AuditRepository,
1991
+ @inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository,
1992
+ @inject({ key: 'repositories.AuditRepository' }) private auditRepository: AuditRepository,
1935
1993
  ) {
1936
1994
  super({ scope: UserService.name });
1937
1995
  }
1938
1996
 
1939
1997
  async createUser(data: CreateUserInput) {
1940
- const tx = await this.userRepo.dataSource.beginTransaction();
1998
+ const tx = await this.userRepository.dataSource.beginTransaction();
1941
1999
  try {
1942
- const { data: user } = await this.userRepo.create({
2000
+ const { data: user } = await this.userRepository.create({
1943
2001
  data,
1944
2002
  options: { transaction: tx },
1945
2003
  });
1946
2004
 
1947
- await this.auditRepo.create({
2005
+ await this.auditRepository.create({
1948
2006
  data: { action: 'user_created', userId: user.id },
1949
2007
  options: { transaction: tx },
1950
2008
  });
@@ -1959,7 +2017,7 @@ export class UserService extends BaseService {
1959
2017
 
1960
2018
  async deactivateInactiveUsers() {
1961
2019
  const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // 90 days
1962
- const { count } = await this.userRepo.updateBy({
2020
+ const { count } = await this.userRepository.updateBy({
1963
2021
  data: { status: 'inactive' },
1964
2022
  where: { lastLoginAt: { lt: cutoff }, status: 'active' },
1965
2023
  });
@@ -1987,35 +2045,29 @@ Components are self-contained modules that register controllers, services, bindi
1987
2045
 
1988
2046
  | Component | Import | Description |
1989
2047
  | --- | --- | --- |
1990
- | **HealthCheckComponent** | `@venizia/ignis` | Health check endpoints (`GET /health`, `/health/live`, `/health/ready`) |
2048
+ | **HealthCheckComponent** | `@venizia/ignis` | Health check endpoints (`GET /health`, `POST /health/ping`) |
1991
2049
  | **SwaggerComponent** | `@venizia/ignis` | OpenAPI documentation with Swagger UI or Scalar UI |
1992
- | **AuthenticateComponent** | `@venizia/ignis` | JWT + Basic authentication strategies, token services, auth middleware |
2050
+ | **AuthenticateComponent** | `@venizia/ignis` | JWS/JWKS + Basic authentication strategies, token services, auth middleware |
1993
2051
  | **AuthorizeComponent** | `@venizia/ignis` | Casbin-based RBAC authorization with enforcers |
1994
2052
  | **RequestTrackerComponent** | `@venizia/ignis` | `x-request-id` header injection, request body parsing |
1995
- | **StaticAssetComponent** | `@venizia/ignis` | File upload/download CRUD with MinIO or disk storage |
2053
+ | **StaticAssetComponent** | `@venizia/ignis/static-asset` | File upload/download CRUD with MinIO, Disk, or Bun S3 storage |
1996
2054
  | **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) |
2055
+ | **SocketIOComponent** | `@venizia/ignis/socket-io` | Socket.IO server with a mandatory Redis adapter for horizontal scaling |
2056
+ | **WebSocketComponent** | `@venizia/ignis/websocket` | Native WebSocket support (Bun runtime) |
2057
+ | **GrpcComponent** | `@venizia/ignis/grpc` | Auto-registered by `BaseApplication` (like `RestComponent`) -- mounts controllers whose `@controller` metadata has `transport: 'grpc'` via ConnectRPC; never registered manually with `this.component()` |
1999
2058
 
2000
2059
  ### Health Check Component
2001
2060
 
2002
2061
  ```typescript
2003
2062
  import { HealthCheckComponent, HealthCheckBindingKeys, IHealthCheckOptions } from '@venizia/ignis';
2004
2063
 
2005
- // Optional: customize path (default: /health)
2006
2064
  this.bind<IHealthCheckOptions>({ key: HealthCheckBindingKeys.HEALTH_CHECK_OPTIONS }).toValue({
2007
- restOptions: { path: '/health-check' },
2065
+ restOptions: { path: '/health-check' }, // optional, default: /health
2008
2066
  });
2009
2067
  this.component(HealthCheckComponent);
2010
2068
  ```
2011
2069
 
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 |
2070
+ `GET /health` (basic status) and `POST /health/ping` (ping/pong echo) -- no separate liveness/readiness probe pair. Details: [Health Check](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/health-check.md).
2019
2071
 
2020
2072
  ### Swagger / OpenAPI Component
2021
2073
 
@@ -2023,288 +2075,82 @@ this.component(HealthCheckComponent);
2023
2075
  import { SwaggerComponent, SwaggerBindingKeys, ISwaggerOptions } from '@venizia/ignis';
2024
2076
 
2025
2077
  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' },
2078
+ restOptions: { base: { path: '/doc' }, doc: { path: '/openapi.json' }, ui: { path: '/explorer', type: 'scalar' } },
2032
2079
  });
2033
2080
  this.component(SwaggerComponent);
2034
2081
  ```
2035
2082
 
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) |
2083
+ Auto-populates `info` from `getAppInfo()`, registers JWT/Basic security schemes, and serves Scalar or Swagger UI depending on `ui.type`. Details: [Swagger](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/swagger.md).
2044
2084
 
2045
2085
  ### Authentication Component
2046
2086
 
2047
- Supports JWT and Basic authentication strategies with encrypted JWT payloads:
2087
+ Basic auth plus Bearer tokens under two JOSE standards -- **JWS** (HMAC-signed, single shared secret) and **JWKS** (asymmetric, issuer/verifier split, `/certs` publishing) -- with optional AES-encrypted payloads:
2048
2088
 
2049
2089
  ```typescript
2050
- import {
2051
- AuthenticateComponent, AuthenticateBindingKeys,
2052
- Authentication, AuthenticationStrategyRegistry,
2053
- JWTAuthenticationStrategy, BasicAuthenticationStrategy,
2054
- IJWTTokenServiceOptions, IBasicTokenServiceOptions,
2055
- TAuthenticationRestOptions,
2056
- } from '@venizia/ignis';
2090
+ import { AuthenticateComponent, AuthenticateBindingKeys, JOSEStandards, TJWTTokenServiceOptions } from '@venizia/ignis';
2057
2091
 
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)
2092
+ this.bind<TJWTTokenServiceOptions>({ key: AuthenticateBindingKeys.JWT_OPTIONS }).toValue({
2093
+ standard: JOSEStandards.JWS,
2094
+ options: { jwtSecret: process.env.JWT_SECRET!, headerAlgorithm: 'HS256', getTokenExpiresFn: () => 86400 },
2065
2095
  });
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
2096
  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
2097
  ```
2149
2098
 
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 BaseRestController {
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
- ```
2099
+ Strategies (`jwt`/`basic`) register via `AuthenticationStrategyRegistry`; the current user is available via `context.get('auth.current.user')` after the middleware runs. Authentication mode is `any` (first success wins) or `all` (every strategy must pass). Full setup (JWKS issuer/verifier mode, token service API, route vs controller-level auth): [Authentication](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/authentication/index.md).
2190
2100
 
2191
2101
  ### Authorization Component
2192
2102
 
2193
- Casbin-based RBAC authorization:
2103
+ Casbin-based RBAC. `IAuthorizeOptions` carries the component-wide decision policy; the enforcer is registered separately through `AuthorizationEnforcerRegistry`:
2194
2104
 
2195
2105
  ```typescript
2196
- import {
2197
- AuthorizeComponent, AuthorizeBindingKeys, IAuthorizeOptions,
2198
- CasbinAuthorizationEnforcer,
2199
- } from '@venizia/ignis';
2106
+ import { AuthorizeComponent, AuthorizeBindingKeys, AuthorizationDecisions } from '@venizia/ignis';
2200
2107
 
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
- },
2108
+ this.bind({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
2109
+ defaultDecision: AuthorizationDecisions.DENY,
2110
+ alwaysAllowRoles: ['superadmin'],
2208
2111
  });
2209
2112
  this.component(AuthorizeComponent);
2210
2113
  ```
2211
2114
 
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
- ```
2115
+ Apply with `authorize: { action, resource }` (or an array, all must pass) in a route's `configs`. Full enforcer registration and Casbin model setup: [Authorization](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/authorization/index.md).
2239
2116
 
2240
2117
  ### Static Asset Component
2241
2118
 
2242
- File upload/download with MinIO or disk storage:
2119
+ File upload/download with MinIO, Disk, or Bun S3 storage. Sub-path only, like `mail`/`socket-io`/`websocket`:
2243
2120
 
2244
2121
  ```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);
2122
+ import { StaticAssetComponent, StaticAssetComponentBindingKeys, StaticAssetStorageTypes } from '@venizia/ignis/static-asset';
2123
+ import { DiskHelper } from '@venizia/ignis-helpers';
2268
2124
 
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
- },
2125
+ this.bind({ key: StaticAssetComponentBindingKeys.STATIC_ASSET_COMPONENT_OPTIONS }).toValue({
2126
+ staticAsset: { controller: { name: 'AssetController', basePath: '/assets' }, storage: StaticAssetStorageTypes.DISK, helper: new DiskHelper({ basePath: './uploads' }) },
2278
2127
  });
2128
+ this.component(StaticAssetComponent);
2279
2129
  ```
2280
2130
 
2131
+ MinIO and Bun S3 backends follow the same shape, swapping `storage` and `helper`. Details: [Static Asset](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/static-asset/index.md).
2132
+
2281
2133
  ### Mail Component
2282
2134
 
2283
2135
  ```typescript
2284
2136
  import { MailComponent } from '@venizia/ignis/mail';
2285
2137
  ```
2286
2138
 
2287
- Supports:
2288
-
2289
- - **Transporters**: Nodemailer (SMTP), Mailgun (API)
2290
- - **Executors**: Direct (synchronous send), BullMQ (background queue with Redis), InternalQueue (in-memory queue)
2139
+ Transporters: Nodemailer (SMTP), Mailgun (API). Executors: Direct (synchronous), BullMQ (Redis-backed queue), InternalQueue (in-memory). Details: [Mail](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/mail/index.md).
2291
2140
 
2292
2141
  ### Socket.IO Component
2293
2142
 
2294
2143
  ```typescript
2295
2144
  import { SocketIOComponent, SocketIOBindingKeys } from '@venizia/ignis/socket-io';
2296
2145
 
2297
- this.bind({ key: SocketIOBindingKeys.OPTIONS }).toValue({
2298
- cors: { origin: '*' },
2299
- adapter: redisAdapter, // Optional: Redis adapter for scaling
2300
- });
2146
+ this.bind({ key: SocketIOBindingKeys.SERVER_OPTIONS }).toValue({ cors: { origin: '*' } });
2147
+ // A Redis connection and an authenticate handler are both mandatory (unlike most other components).
2148
+ this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION }).toValue(redisHelper);
2149
+ this.bind({ key: SocketIOBindingKeys.AUTHENTICATE_HANDLER }).toValue(authenticateSocket);
2301
2150
  this.component(SocketIOComponent);
2302
2151
  ```
2303
2152
 
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
2153
+ Bun and Node.js runtime handlers (auto-detected), Redis adapter for horizontal scaling. Details: [Socket.IO](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/extensions/components/socket-io/index.md).
2308
2154
 
2309
2155
  ---
2310
2156
 
@@ -2313,7 +2159,7 @@ Provides Socket.IO server integration with:
2313
2159
  Access the Hono request context from anywhere using `useRequestContext()`:
2314
2160
 
2315
2161
  ```typescript
2316
- import { useRequestContext } from '@venizia/ignis';
2162
+ import { useRequestContext, IAuthUser } from '@venizia/ignis';
2317
2163
 
2318
2164
  function getCurrentRequestId(): string | undefined {
2319
2165
  const context = useRequestContext();
@@ -2383,7 +2229,14 @@ setupMiddlewares(): ValueOrPromise<void> {
2383
2229
 
2384
2230
  ### Error Propagation
2385
2231
 
2386
- Errors propagate through the layer stack and are caught by the global `appErrorHandler`:
2232
+ Errors propagate through the layer stack and are caught by the global `appErrorHandler` (Hono's `onError`), which routes each error to one of four shapes:
2233
+
2234
+ ```
2235
+ ZodError -> 422, via formatZodError()
2236
+ DB client error (SQLSTATE class 22/23/44) -> 400, via isDatabaseClientError()
2237
+ Transient DB conflict (40001/40P01) -> 409, via isRetryableDatabaseError()
2238
+ Everything else (incl. getError()) -> its own statusCode, default 500
2239
+ ```
2387
2240
 
2388
2241
  ```
2389
2242
  Controller throws -> appErrorHandler catches -> JSON error response
@@ -2396,8 +2249,10 @@ Repository throws -> Service doesn't catch -> Controller doesn't catch -> appErr
2396
2249
  ```json
2397
2250
  {
2398
2251
  "message": "Error description",
2252
+ "messageCode": "some.message.code",
2399
2253
  "statusCode": 500,
2400
2254
  "requestId": "abc-123-def",
2255
+ "extra": { "any": "extra context attached via getError({ extra })" },
2401
2256
  "details": {
2402
2257
  "url": "http://localhost:3000/api/users",
2403
2258
  "path": "/api/users",
@@ -2407,22 +2262,20 @@ Repository throws -> Service doesn't catch -> Controller doesn't catch -> appErr
2407
2262
  }
2408
2263
  ```
2409
2264
 
2410
- In production (`NODE_ENV=production`), `stack` and `cause` are stripped from responses.
2265
+ In production (`NODE_ENV=production`), `stack` and `cause` are stripped from responses, and an unexpected (non-`getError`) 500 has its `message` replaced with a generic `"Internal Server Error"` so raw driver/connection errors never leak.
2411
2266
 
2412
2267
  ### PostgreSQL Constraint Errors
2413
2268
 
2414
- The error handler automatically recognizes PostgreSQL constraint violations and returns HTTP 400 instead of 500:
2269
+ The error handler classifies by SQLSTATE **class** (the code's first two characters), not an exhaustive list of individual codes:
2415
2270
 
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 |
2271
+ | SQLSTATE class | Meaning | Resolved status |
2272
+ | --- | --- | --- |
2273
+ | `22` | Data exception (e.g. `22001` string too long, `22003` numeric out of range, `22P02` invalid text representation) | 400 |
2274
+ | `23` | Integrity constraint violation (e.g. `23505` unique, `23503` foreign key, `23502` not null, `23514` check, `23P01` exclusion) | 400 |
2275
+ | `44` | WITH CHECK OPTION violation | 400 |
2276
+ | `40001` / `40P01` (exact codes, not a class) | Serialization failure / deadlock detected -- transient, safe to retry | 409, `messageCode: 'database.conflict'` |
2277
+
2278
+ In production, only the generic per-code message is returned; outside production, `detail`/`table`/`constraint` from the driver are appended for debugging.
2426
2279
 
2427
2280
  ### Throwing Application Errors
2428
2281
 
@@ -2442,16 +2295,18 @@ throw getError({
2442
2295
  });
2443
2296
  ```
2444
2297
 
2298
+ `throwNotSupported()` (`src/utilities/error.utility.ts`) is the standardized way connectors reject a capability they deliberately don't implement (e.g. Typesense/Memory transactions and locks): it logs a warning, then throws via `getError()` with `messageCode: 'core.not_supported'` and HTTP 501.
2299
+
2445
2300
  ---
2446
2301
 
2447
2302
  ## Decorators Reference
2448
2303
 
2449
2304
  | Decorator | Target | Parameters | Description |
2450
2305
  | --- | --- | --- | --- |
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 |
2306
+ | `@model({ type, settings?, tableName?, skipMigrate? })` | Class | `type`: `'entity' \| 'view'` (required); `settings.hiddenProperties`: string[]; `settings.defaultFilter`: TFilter | Register entity model with hidden properties and default filters |
2307
+ | `@datasource({ driver, autoDiscovery? })` | Class | `driver`: e.g. `'node-postgres'`, `'typesense'` (required); `autoDiscovery`: boolean (default true) | Register datasource with driver configuration |
2453
2308
  | `@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 |
2309
+ | `@controller({ path })` | Class | `path`: base path string | Register controller with base path. Default authentication/authorization is set at the `ControllerFactory` level, not on this decorator |
2455
2310
  | `@get({ configs })` | Method | Full route config (path, request, responses, authenticate, authorize, middleware) | Define GET route |
2456
2311
  | `@post({ configs })` | Method | Same as `@get` | Define POST route |
2457
2312
  | `@put({ configs })` | Method | Same as `@get` | Define PUT route |
@@ -2469,6 +2324,7 @@ Utility functions for building OpenAPI-compliant response and request schemas:
2469
2324
 
2470
2325
  ```typescript
2471
2326
  import { jsonContent, jsonResponse, htmlResponse, idParamsSchema } from '@venizia/ignis';
2327
+ import { z } from '@hono/zod-openapi';
2472
2328
 
2473
2329
  // JSON request body
2474
2330
  jsonContent({
@@ -2505,255 +2361,84 @@ idParamsSchema({ idType: 'string' });
2505
2361
 
2506
2362
  ### Complete User CRUD with Auth, Validation, Soft Delete, Pagination
2507
2363
 
2364
+ A model with hidden fields + soft delete, a repository, a service enforcing a uniqueness rule, and a controller wiring auth/authorization onto routes -- the full layered pattern in one file group:
2365
+
2508
2366
  ```typescript
2509
2367
  // 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> {
2368
+ @model({ type: 'entity', settings: { hiddenProperties: ['password'], defaultFilter: { where: { isDeleted: false } } } })
2369
+ export class User extends BasePostgresEntity<typeof User.schema> {
2518
2370
  static override schema = pgTable('User', {
2519
2371
  ...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
- }),
2372
+ ...generateTzColumnDefs({ deleted: { enable: true, columnName: 'deleted_at', withTimezone: true } }),
2527
2373
  username: text('username').notNull().unique(),
2528
2374
  email: text('email').notNull().unique(),
2529
2375
  password: text('password'),
2530
- role: text('role').default('user').notNull(),
2531
2376
  isDeleted: boolean('is_deleted').default(false).notNull(),
2532
- metadata: jsonb('metadata').$type<Record<string, any>>(),
2533
2377
  });
2534
-
2535
2378
  static override relations = () => [];
2536
- static TABLE_NAME = 'User';
2379
+ static override TABLE_NAME = 'User';
2537
2380
  }
2538
2381
 
2539
2382
  // repositories/user.repository.ts
2540
2383
  @repository({ model: User, dataSource: PostgresDataSource })
2541
2384
  export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {}
2542
2385
 
2543
- // services/user.service.ts
2386
+ // services/user.service.ts -- enforces a uniqueness rule before delegating to the repository
2544
2387
  export class UserService extends BaseService {
2545
- constructor(
2546
- @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
2547
- ) {
2388
+ constructor(@inject({ key: 'repositories.UserRepository' }) private userRepository: UserRepository) {
2548
2389
  super({ scope: UserService.name });
2549
2390
  }
2550
2391
 
2551
2392
  async createUser(data: { username: string; email: string; password: string }) {
2552
- const exists = await this.userRepo.existsWith({ where: { email: data.email } });
2393
+ const exists = await this.userRepository.existsWith({ where: { email: data.email } });
2553
2394
  if (exists) {
2554
- throw getError({
2555
- statusCode: HTTP.ResultCodes.RS_4.Conflict,
2556
- message: 'Email already in use',
2557
- });
2395
+ throw getError({ statusCode: HTTP.ResultCodes.RS_4.Conflict, message: 'Email already in use' });
2558
2396
  }
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
- });
2397
+ return this.userRepository.create({ data: { ...data, password: await Bun.password.hash(data.password) } });
2571
2398
  }
2572
2399
  }
2573
2400
 
2574
- // controllers/user.controller.ts
2401
+ // controllers/user.controller.ts -- @get is public, @post requires jwt + authorization
2575
2402
  @controller({ path: '/users' })
2576
2403
  export class UserController extends BaseRestController {
2577
- constructor(
2578
- @inject({ key: 'services.UserService' }) private userService: UserService,
2579
- @inject({ key: 'repositories.UserRepository' }) private userRepo: UserRepository,
2580
- ) {
2404
+ constructor(@inject({ key: 'services.UserService' }) private userService: UserService) {
2581
2405
  super({ scope: UserController.name });
2582
2406
  }
2583
-
2584
2407
  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
2408
  }
2657
2409
  ```
2658
2410
 
2411
+ The full walkthrough (pagination, controller-level find/create/soft-delete routes, OpenAPI schemas) is the [Building a CRUD API tutorial](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/tutorials/building-a-crud-api.md).
2412
+
2659
2413
  ---
2660
2414
 
2661
2415
  ## Testing
2662
2416
 
2663
- ### Testing Repositories
2417
+ IGNIS uses the Bun test runner throughout. A repository test needs only a configured DataSource:
2664
2418
 
2665
2419
  ```typescript
2666
- import { describe, test, expect } from 'bun:test';
2420
+ import { describe, test, expect, beforeAll } from 'bun:test';
2667
2421
 
2668
2422
  describe('UserRepository', () => {
2669
- let repo: UserRepository;
2670
- let dataSource: PostgresDataSource;
2423
+ let repository: UserRepository;
2671
2424
 
2672
2425
  beforeAll(async () => {
2673
- dataSource = new PostgresDataSource();
2426
+ const dataSource = new PostgresDataSource();
2674
2427
  await dataSource.configure();
2675
- repo = new UserRepository(dataSource, { entityClass: User });
2428
+ repository = new UserRepository(dataSource, { entityClass: User });
2676
2429
  });
2677
2430
 
2678
2431
  test('create and find user', async () => {
2679
- const { data: user } = await repo.create({
2680
- data: { username: 'test', email: 'test@example.com' },
2681
- });
2682
-
2432
+ const { data: user } = await repository.create({ data: { username: 'test', email: 'test@example.com' } });
2683
2433
  expect(user.id).toBeDefined();
2684
- expect(user.username).toBe('test');
2685
2434
 
2686
- const found = await repo.findById({ id: user.id });
2687
- expect(found).not.toBeNull();
2435
+ const found = await repository.findById({ id: user.id });
2688
2436
  expect(found!.email).toBe('test@example.com');
2689
2437
  });
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
2438
  });
2717
2439
  ```
2718
2440
 
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
- ```
2441
+ Controller tests drive `app.getServer().fetch()` against a real running `Application` -- routes are only mounted inside `start()`. Full patterns (hidden-field/default-filter assertions, request validation, auth): [Testing tutorial](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/tutorials/testing.md).
2757
2442
 
2758
2443
  ---
2759
2444
 
@@ -2767,11 +2452,11 @@ describe('UserController', () => {
2767
2452
 
2768
2453
  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
2454
 
2770
- 5. **Visible Property Caching** -- The `FieldsVisibilityMixin` computes the visible property set once and caches it. Subsequent queries reuse the cached column selection.
2455
+ 5. **Visible Property Caching** -- `PostgresBaseRepository.getVisibleProperties()` computes the visible property set once and caches it. Subsequent queries reuse the cached column selection.
2771
2456
 
2772
2457
  6. **Parallel Count + Data** -- When `shouldQueryRange: true`, the data fetch and count query run in parallel via `Promise.all`, not sequentially.
2773
2458
 
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.
2459
+ 7. **Schema Factory Sharing** -- `BasePostgresEntity` uses a lazy singleton for the Drizzle-Zod schema factory, shared across all entity instances. Schema generation does not create redundant factory objects.
2775
2460
 
2776
2461
  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
2462
 
@@ -2784,8 +2469,8 @@ describe('UserController', () => {
2784
2469
  ## Documentation
2785
2470
 
2786
2471
  - [IGNIS Repository](https://github.com/VENIZIA-AI/ignis)
2787
- - [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/get-started/index.md)
2788
- - [Core Concepts](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/get-started/core-concepts/application.md)
2472
+ - [Getting Started](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/index.md)
2473
+ - [Core Concepts](https://github.com/VENIZIA-AI/ignis/blob/main/docs/wiki/content/guides/core-concepts/application/index.md)
2789
2474
  - [Examples](https://github.com/VENIZIA-AI/ignis/tree/main/examples/vert)
2790
2475
 
2791
2476
  ---