@venizia/ignis-docs 0.0.8-0 → 0.0.8-2

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 (39) hide show
  1. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  2. package/dist/mcp-server/helpers/docs.helper.js +1 -1
  3. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  4. package/dist/mcp-server/tools/base.tool.d.ts +1 -1
  5. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
  6. package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -1
  7. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  8. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  9. package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -1
  10. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  11. package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
  12. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  13. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  14. package/package.json +14 -14
  15. package/wiki/extensions/components/authorization/api.md +239 -376
  16. package/wiki/extensions/components/authorization/errors.md +52 -43
  17. package/wiki/extensions/components/authorization/index.md +127 -65
  18. package/wiki/extensions/components/authorization/usage.md +198 -98
  19. package/wiki/extensions/helpers/kafka/consumer.md +6 -5
  20. package/wiki/extensions/helpers/kafka/examples.md +1 -1
  21. package/wiki/extensions/helpers/kafka/index.md +16 -12
  22. package/wiki/extensions/helpers/kafka/producer.md +4 -3
  23. package/wiki/guides/core-concepts/persistent/datasources.md +10 -11
  24. package/wiki/guides/core-concepts/persistent/index.md +6 -6
  25. package/wiki/guides/core-concepts/persistent/models.md +7 -5
  26. package/wiki/guides/core-concepts/persistent/repositories.md +11 -3
  27. package/wiki/guides/core-concepts/persistent/transactions.md +2 -1
  28. package/wiki/guides/core-concepts/rest-controllers.md +2 -2
  29. package/wiki/guides/core-concepts/services.md +0 -1
  30. package/wiki/guides/get-started/5-minute-quickstart.md +11 -10
  31. package/wiki/guides/migrations/scoped-rbac-migration.md +300 -0
  32. package/wiki/guides/tutorials/building-a-crud-api.md +43 -37
  33. package/wiki/guides/tutorials/complete-installation.md +64 -44
  34. package/wiki/guides/tutorials/ecommerce-api.md +21 -12
  35. package/wiki/guides/tutorials/realtime-chat.md +4 -5
  36. package/wiki/references/base/filter-system/default-filter.md +6 -3
  37. package/wiki/references/base/filter-system/fields-order-pagination.md +26 -0
  38. package/wiki/references/base/models.md +6 -3
  39. package/wiki/references/base/repositories/advanced.md +111 -0
@@ -11,8 +11,8 @@ Build your first Ignis API endpoint in 5 minutes. No database, no complex setup
11
11
  ```bash
12
12
  mkdir my-app && cd my-app
13
13
  bun init -y
14
- bun add hono @hono/zod-openapi @scalar/hono-api-reference @venizia/ignis
15
- bun add -d typescript @types/bun @venizia/dev-configs
14
+ bun add hono @hono/zod-openapi @scalar/hono-api-reference @venizia/ignis @venizia/ignis-helpers
15
+ bun add -d typescript @types/bun @venizia/dev-configs eslint prettier tsc-alias
16
16
  ```
17
17
 
18
18
  ## Step 2: Configure Development Tools (30 seconds)
@@ -99,11 +99,9 @@ class HelloController extends BaseRestController {
99
99
  super({ scope: "HelloController", path: "/hello" });
100
100
  }
101
101
 
102
- // NOTE: This is a function that must be overridden.
103
- override binding() {
104
- // Bind dependencies here (if needed)
105
- // Extra binding routes with functional way, use `bindRoute` or `defineRoute`
106
- }
102
+ // Override binding() to register custom routes via bindRoute() or defineRoute().
103
+ // For decorator-based routes (@get, @post), this can be empty.
104
+ override binding() {}
107
105
 
108
106
  @get({
109
107
  configs: {
@@ -152,9 +150,11 @@ const app = new App({
152
150
  host: "0.0.0.0",
153
151
  port: 3000,
154
152
  path: { base: "/api", isStrict: false },
153
+ debug: { shouldShowRoutes: true }, // Prints all registered routes on startup
155
154
  },
156
155
  });
157
156
 
157
+ // start() runs the full lifecycle: preConfigure → register resources → setupMiddlewares → HTTP server
158
158
  app.start();
159
159
  ```
160
160
 
@@ -177,10 +177,11 @@ Update `package.json` to add build scripts:
177
177
  "server:prod": "NODE_ENV=production bun run dist/index.js"
178
178
  },
179
179
  "dependencies": {
180
- "hono": "^4.4.12",
180
+ "hono": "^4.12.1",
181
181
  "@hono/zod-openapi": "latest",
182
182
  "@scalar/hono-api-reference": "latest",
183
- "@venizia/ignis": "latest"
183
+ "@venizia/ignis": "latest",
184
+ "@venizia/ignis-helpers": "latest"
184
185
  },
185
186
  "devDependencies": {
186
187
  "typescript": "^5.5.3",
@@ -240,7 +241,7 @@ Open `http://localhost:3000/doc/explorer` to see interactive Swagger UI document
240
241
  | `BaseRestController` | Provides lifecycle hooks, route binding, and OpenAPI integration for REST controllers |
241
242
  | `BaseApplication` | Manages dependency injection, middleware, and server startup |
242
243
  | `SwaggerComponent` | Generates interactive API docs at `/doc/explorer` |
243
- | `app.start()` | Boots the DI container and starts HTTP server on port 3000 |
244
+ | `app.start()` | Runs the full lifecycle (preConfigure → register resources → middlewares) then starts HTTP server on port 3000 |
244
245
 
245
246
  ### Why Development Configs?
246
247
 
@@ -0,0 +1,300 @@
1
+ # Migrating to the Scoped RBAC Authorization (ignis ≥ scoped-rbac release)
2
+
3
+ > **Audience:** the team/agent maintaining **nx-seller** (and any app that wrote a custom
4
+ > `DrizzleCasbinAdapter` subclass). This is a hand-off spec describing exactly what breaks when you
5
+ > upgrade `@venizia/ignis` to the scoped-RBAC release and the two supported migration paths.
6
+
7
+ ---
8
+
9
+ ## 0. TL;DR
10
+
11
+ The ignis `authorize` module was reworked around a single **edge table** + a **scoped Casbin model**.
12
+ As part of that, several symbols nx-seller depends on were **removed or changed**. Upgrading ignis
13
+ **will not compile** until you act on the breakages in §2.
14
+
15
+ You have two paths:
16
+
17
+ | Path | Effort | When |
18
+ |------|--------|------|
19
+ | **B — Bridge** (re-base your custom adapter on `BaseFilteredAdapter`, keep your flat model) | Low — code only, **no data migration** | Do this first to unblock the upgrade |
20
+ | **A — Adopt scoped** (delete your custom adapter, use `ScopedCasbinAdapter` + scoped model) | High — needs a **data migration** | The intended long-term target |
21
+
22
+ Both are described below with exact before/after.
23
+
24
+ ---
25
+
26
+ ## 1. What changed in ignis
27
+
28
+ | Area | Before | After |
29
+ |------|--------|-------|
30
+ | Adapter base | `DrizzleCasbinAdapter` (concrete, app subclassed it) | **removed.** New: thin `BaseFilteredAdapter<TFilter>` + a ready-made generic `ScopedCasbinAdapter` |
31
+ | Adapter options | `IDrizzleCasbinAdapterOptions` | **removed.** `BaseFilteredAdapter` takes `{ scope, dataSource }`; `ScopedCasbinAdapter` takes `{ dataSource, entities }` |
32
+ | Filter shape | `{ principalType, principalValue }` (flat) | `{ principal: { type, id } }` (`ICasbinPolicyFilter`) |
33
+ | `CasbinRuleVariants` | `P, G, GROUP('group'), POLICY('policy')` + `SCHEME_SET`/`isValid` | trimmed to **casbin prefixes only**: `P, G, G2, G3, G4, G5`. `GROUP`/`POLICY` **removed** |
34
+ | DB `variant` discriminator | lived on `CasbinRuleVariants.GROUP/.POLICY` | now `AuthorizationPolicyVariants.*.action` (`grant`, `assign_role`, `join_domain`, `role_inherits`, `resource_inherits`, `action_inherits`, `domain_inherits`) |
35
+ | Cache drivers | `redis` **and** `in-memory` | **redis only.** `CasbinEnforcerCachedDrivers.IN_MEMORY` removed; `cached` is now `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` |
36
+ | Cache invalidation interface | `IAuthorizationCacheInvalidator` / `TAuthorizationCacheInvalidator` | **removed.** `invalidateUserCache?`/`rebuildUserCache?` are now **optional** members of `IAuthorizationEnforcer` (feature-detected by the registry) |
37
+ | Enforcer model | shared single enforcer | **pool** of per-request enforcers (`BasePoolHelper`); policy loaded per request, fail-closed |
38
+ | Scoped option | n/a | new `isScoped?: boolean` on `ICasbinEnforcerOptions` |
39
+
40
+ > **Note:** the old `CasbinRuleVariants.GROUP = 'group'` and `.POLICY = 'policy'`. Your
41
+ > `PolicyDefinition.variant` column currently stores the strings **`'group'`** and **`'policy'`**.
42
+ > Confirm with: `SELECT DISTINCT variant FROM identity."PolicyDefinition";`
43
+
44
+ ---
45
+
46
+ ## 2. What breaks in nx-seller (exact references)
47
+
48
+ When you bump ignis, these stop compiling/working:
49
+
50
+ 1. **`packages/core/src/security/application-casbin-adapter.ts`**
51
+ - `extends DrizzleCasbinAdapter` → class removed.
52
+ - `import { DrizzleCasbinAdapter, IDrizzleCasbinAdapterOptions, ICasbinPolicyFilter } from '@venizia/ignis'` → first two removed; `ICasbinPolicyFilter` still exists but its **shape changed**.
53
+ - `filter.principalValue` / `filter.principalType` → now `filter.principal.id` / `filter.principal.type`.
54
+ - `CasbinRuleVariants.GROUP` / `CasbinRuleVariants.POLICY` → removed.
55
+ - `this.entities.role.principalType` / `this.entities.permission.principalType` → `entities` no longer provided by the base.
56
+
57
+ 2. **`packages/core/src/repositories/public/policy-definition.repository.ts`**
58
+ - Many `eq(pd.variant, CasbinRuleVariants.GROUP)` / `.POLICY` → constant removed. This file is the
59
+ biggest single breakage surface outside the adapter.
60
+
61
+ 3. **`packages/core/src/application/verifier.ts`** (enforcer registration)
62
+ - The Redis-absent fallback uses `CasbinEnforcerCachedDrivers.IN_MEMORY` → removed. You must pick
63
+ Redis or `{ use: false }`.
64
+
65
+ 4. Any seed/migration writing `variant = 'group' | 'policy'` keeps working at the DB level (those are
66
+ plain strings), but the **constants** that produced them are gone.
67
+
68
+ ---
69
+
70
+ ## 3. Path B — Bridge (recommended first step, no data migration)
71
+
72
+ Goal: compile against new ignis with **identical runtime behavior** — keep your flat `CASBIN_RBAC_MODEL`,
73
+ your `group`/`policy` variant values, and all bespoke logic (global roles, HQ-owner expansion).
74
+
75
+ ### 3.1 Define your own variant constants
76
+
77
+ `CasbinRuleVariants.GROUP/.POLICY` are gone from ignis, but your DB still stores `'group'`/`'policy'`.
78
+ Own these strings locally so you are decoupled from ignis's casbin-prefix enum:
79
+
80
+ ```ts
81
+ // packages/core/src/security/policy-variant.ts
82
+ export class PolicyDefinitionVariant {
83
+ /** user→role assignment + user→merchant membership rows. */
84
+ static readonly GROUP = 'group';
85
+ /** permission grant rows (role→perm, user→perm). */
86
+ static readonly POLICY = 'policy';
87
+ }
88
+ ```
89
+
90
+ Then replace every `CasbinRuleVariants.GROUP` → `PolicyDefinitionVariant.GROUP` and
91
+ `CasbinRuleVariants.POLICY` → `PolicyDefinitionVariant.POLICY` in
92
+ `application-casbin-adapter.ts` **and** `policy-definition.repository.ts`.
93
+ Keep using `CasbinRuleVariants.G` / `.P` for the **emitted casbin lines** (those still exist).
94
+
95
+ ### 3.2 Re-base `ApplicationCasbinAdapter` on `BaseFilteredAdapter`
96
+
97
+ ```ts
98
+ // BEFORE
99
+ import {
100
+ DrizzleCasbinAdapter,
101
+ type IDrizzleCasbinAdapterOptions,
102
+ type ICasbinPolicyFilter,
103
+ } from '@venizia/ignis';
104
+
105
+ export class ApplicationCasbinAdapter extends DrizzleCasbinAdapter {
106
+ private readonly appDataSource: IDrizzleCasbinAdapterOptions['dataSource'];
107
+ constructor(opts: IDrizzleCasbinAdapterOptions) {
108
+ super(opts);
109
+ this.appDataSource = opts.dataSource;
110
+ }
111
+ // ... used this.entities.role.principalType, filter.principalValue, etc.
112
+ }
113
+ ```
114
+
115
+ ```ts
116
+ // AFTER
117
+ import { BaseFilteredAdapter, type ICasbinPolicyFilter } from '@venizia/ignis';
118
+ import type { IDataSource } from '@venizia/ignis';
119
+
120
+ interface IAppCasbinEntities {
121
+ role: { principalType: string };
122
+ permission: { principalType: string };
123
+ }
124
+
125
+ export class ApplicationCasbinAdapter extends BaseFilteredAdapter {
126
+ private readonly entities: IAppCasbinEntities;
127
+
128
+ constructor(opts: { dataSource: IDataSource; entities: IAppCasbinEntities }) {
129
+ super({ scope: ApplicationCasbinAdapter.name, dataSource: opts.dataSource });
130
+ this.entities = opts.entities;
131
+ }
132
+
133
+ // `this.connector` is provided by BaseFilteredAdapter (replaces this.appConnector).
134
+
135
+ override async loadFilteredPolicy(model: Model, filter: ICasbinPolicyFilter): Promise<void> {
136
+ const userId = filter.principal.id; // was filter.principalValue
137
+ const principalType = filter.principal.type; // was filter.principalType
138
+ // ... unchanged bespoke logic ...
139
+ // Instead of `Helper.loadPolicyLine(line, model)` per line you may use:
140
+ // await this.loadLines({ model, lines });
141
+ }
142
+ }
143
+ ```
144
+
145
+ Key swaps inside the class:
146
+ - `this.appConnector` → `this.connector` (from `BaseFilteredAdapter`).
147
+ - `filter.principalValue` → `filter.principal.id`; `filter.principalType` → `filter.principal.type`.
148
+ - `this.entities.role.principalType` / `this.entities.permission.principalType` → from your own
149
+ `entities` (pass the same values you pass today; drop the `tableName`/`policyDefinition` parts the
150
+ base used to require — you import the Drizzle tables directly already).
151
+ - `CasbinRuleVariants.GROUP/.POLICY` → `PolicyDefinitionVariant.GROUP/.POLICY` (§3.1).
152
+
153
+ ### 3.3 Fix the cache fallback in `verifier.ts`
154
+
155
+ ```ts
156
+ // BEFORE — in-memory fallback (driver removed)
157
+ const cached: ICasbinEnforcerOptions['cached'] = redis
158
+ ? { use: true, driver: CasbinEnforcerCachedDrivers.REDIS, options: { ... } }
159
+ : { use: true, driver: CasbinEnforcerCachedDrivers.IN_MEMORY, options: { expiresIn: 5*60*1000 } };
160
+
161
+ // AFTER — Redis or no cache
162
+ const cached: ICasbinEnforcerOptions['cached'] = redis
163
+ ? { use: true, driver: CasbinEnforcerCachedDrivers.REDIS, options: { connection: redis, expiresIn: 5*60*1000, keyFn: ({ user }) => `casbin:${user.principalType}:${user.userId}` } }
164
+ : { use: false };
165
+ ```
166
+
167
+ > **Decide:** in prod, **always provide Redis** — without it every request rebuilds the policy from the
168
+ > DB (no per-user cache). The pool still protects you from the concurrency race, but you lose the line cache.
169
+
170
+ ### 3.4 Adapter construction (`verifier.ts`)
171
+
172
+ Drop the `policyDefinition`/`tableName` entries the old base required; pass only what your re-based
173
+ class declares:
174
+
175
+ ```ts
176
+ const adapter = new ApplicationCasbinAdapter({
177
+ dataSource: this.get<PostgresCoreDataSource>({ /* unchanged */ }),
178
+ entities: {
179
+ role: { principalType: Role.AUTHORIZATION_SUBJECT! },
180
+ permission: { principalType: Permission.AUTHORIZATION_SUBJECT! },
181
+ },
182
+ });
183
+ ```
184
+
185
+ Everything else in the registration (`CASBIN_RBAC_MODEL`, `domainMatching`, `normalizePayloadFn`)
186
+ stays. **Result: behavior identical, compiles on new ignis, zero data migration.**
187
+
188
+ ---
189
+
190
+ ## 4. Path A — Adopt the scoped model (target state)
191
+
192
+ This deletes `ApplicationCasbinAdapter` entirely and uses the generic `ScopedCasbinAdapter`. The
193
+ bespoke logic moves from **code** into **data (edges)**. Do this once Path B has unblocked you.
194
+
195
+ ### 4.1 Register the generic adapter + scoped model
196
+
197
+ ```ts
198
+ import {
199
+ ScopedCasbinAdapter,
200
+ CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
201
+ CasbinEnforcerModelDrivers,
202
+ } from '@venizia/ignis';
203
+
204
+ const adapter = new ScopedCasbinAdapter({
205
+ dataSource: this.get<PostgresCoreDataSource>({ /* ... */ }),
206
+ entities: {
207
+ policyDefinition: { tableName: PolicyDefinition.TABLE_NAME, schemaName: 'identity' },
208
+ permission: { tableName: Permission.TABLE_NAME, schemaName: 'identity' },
209
+ principals: { user: 'User', role: 'Role' }, // casbin name prefixes
210
+ domainTypes: ['Merchant', 'Organizer'], // domain types you scope on
211
+ softDelete: { use: true, columnName: 'deleted_at' },
212
+ },
213
+ });
214
+
215
+ // In the enforcer options:
216
+ // model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL }
217
+ // isScoped: true
218
+ // adapter, cached
219
+ // ❌ remove domainMatching (isScoped auto-registers keyMatch on g + objectMatch on g4)
220
+ // ❌ remove normalizePayloadFn (scoped mode uses the default (sub,dom,obj,act) payload;
221
+ // pass the request domain via the provider's domain resolver instead)
222
+ ```
223
+
224
+ ### 4.2 Data migration — the `variant` column
225
+
226
+ The scoped adapter filters on `AuthorizationPolicyVariants.*.action`, not `group`/`policy`. You must
227
+ re-classify rows:
228
+
229
+ | Current row (`variant`) | Becomes | Rule |
230
+ |-------------------------|---------|------|
231
+ | `group`, subject=User, target=Role | `assign_role` | user→role |
232
+ | `group`, subject=Role, target=Role | `role_inherits` | role→role (if you have role hierarchy) |
233
+ | `group`, subject=User, target=Merchant | `join_domain` | user→domain membership |
234
+ | `policy` (role→perm or user→perm) | `grant` | permission grant |
235
+
236
+ New edge types you may need to **add** (no equivalent today):
237
+ - `domain_inherits` (Merchant ⊂ Organizer / HQ) — **this replaces the bespoke `queryHqOwnerOrgMerchants`
238
+ JOIN**. Materialize one row per Merchant→Organizer (or →HQ-merchant) relationship; the scoped model's
239
+ `g3` then cascades a grant on the parent domain to all child merchants automatically. Maintain these
240
+ rows when merchants/organizers are created or moved.
241
+ - `resource_inherits` (`g4`) / `action_inherits` (`g5`) — only if you want resource/action hierarchies.
242
+
243
+ ### 4.3 Re-express bespoke behavior as data
244
+
245
+ | Today (code in `ApplicationCasbinAdapter`) | Scoped equivalent (data) |
246
+ |--------------------------------------------|--------------------------|
247
+ | Global role → wildcard `*` domain (`AppFixedRoles.isGlobalRole`) | Store the `assign_role` row with **NULL domain** → adapter emits `g, User_x, Role_y, *` |
248
+ | NULL-domain role → expand to every member merchant | Grant rows with domain **`ANY_MEMBER`** + `join_domain` (`g2`) membership rows; the matcher resolves "any domain I'm a member of" |
249
+ | HQ-owner expansion (live JOIN) | `domain_inherits` (`g3`) edges (see §4.2) |
250
+ | Domain-agnostic role permissions (`p, Role, *, ...`) | Grant rows with domain `ANY_MEMBER` (default when `domain` is NULL) |
251
+
252
+ ### 4.4 Behavioral caveat — resource matching changes
253
+
254
+ Your flat model uses exact `r.obj == p.obj`. The scoped model uses **`objectMatch`** (dotted-prefix +
255
+ wildcard): a grant on `Order` will now **also** match `Order.findById`, and `p.obj = '*'` matches any
256
+ resource. Audit your permission `code`s before switching, or you may unintentionally widen access.
257
+
258
+ ### 4.5 Update `policy-definition.repository.ts`
259
+
260
+ This file queries by `variant`. After the data migration, replace `CasbinRuleVariants.GROUP/.POLICY`
261
+ with the relevant `AuthorizationPolicyVariants.*.action` values (e.g. `ASSIGN_ROLE.action`,
262
+ `JOIN_DOMAIN.action`, `GRANT.action`) matching the row kind each query targets.
263
+
264
+ ---
265
+
266
+ ## 5. Decision guide
267
+
268
+ ```
269
+ Need to ship the ignis bump now, behavior unchanged? → Path B (bridge).
270
+ Ready to model org hierarchy as data + run a migration? → Path A (scoped).
271
+ ```
272
+
273
+ Path B and Path A are not mutually exclusive: do **B** to upgrade safely, then schedule **A** to delete
274
+ the bespoke adapter and gain resource/action/domain hierarchies for free.
275
+
276
+ ---
277
+
278
+ ## 6. Verification checklist (either path)
279
+
280
+ - [ ] `bun run build` (or `tsc -p .`) is clean — no references to `DrizzleCasbinAdapter`,
281
+ `IDrizzleCasbinAdapterOptions`, `CasbinRuleVariants.GROUP/.POLICY`, `CasbinEnforcerCachedDrivers.IN_MEMORY`,
282
+ `IAuthorizationCacheInvalidator`.
283
+ - [ ] `SELECT DISTINCT variant FROM identity."PolicyDefinition"` matches what your adapter filters on
284
+ (`group`/`policy` for Path B; the new `*.action` set for Path A).
285
+ - [ ] A request for a user with a role-inherited / per-merchant / global permission resolves the same
286
+ ALLOW/DENY as before the upgrade (pick 3–4 representative users and diff).
287
+ - [ ] If `cached.use: true`, Redis is reachable; if a permission changes, call
288
+ `enforcer.invalidateUserCache({ user })` (or rely on TTL) — see the ignis authorization docs.
289
+ - [ ] Super-admin / always-allow-roles still short-circuit (these run in the provider before the enforcer).
290
+
291
+ ---
292
+
293
+ ## 7. Reference — current nx-seller wiring (before)
294
+
295
+ For context, the current registration (`packages/core/src/application/verifier.ts`) uses:
296
+ `ApplicationCasbinAdapter` (subclass of removed `DrizzleCasbinAdapter`), `CASBIN_RBAC_MODEL` (flat
297
+ `g + p`, exact `r.obj == p.obj`), a Redis-or-in-memory `cached`, `domainMatching { roleDefinition: 'g',
298
+ fn: keyMatch }`, and a `normalizePayloadFn` mapping subject/domain via
299
+ `ApplicationCasbinAdapter.toUserVerb/toMerchantVerb`. Path B keeps all of this except the adapter base
300
+ and the in-memory cache; Path A replaces the adapter, model, `domainMatching`, and `normalizePayloadFn`.
@@ -74,10 +74,10 @@ HTTP Request (GET /api/todos/:id)
74
74
 
75
75
  ```bash
76
76
  # Add database packages
77
- bun add drizzle-orm drizzle-zod pg lodash
77
+ bun add drizzle-orm drizzle-zod pg
78
78
 
79
79
  # Add dev dependencies for migrations
80
- bun add -d drizzle-kit @types/pg @types/lodash
80
+ bun add -d drizzle-kit @types/pg
81
81
  ```
82
82
 
83
83
  ## Step 2: Define the Model
@@ -182,7 +182,6 @@ Create `src/datasources/postgres.datasource.ts`:
182
182
  import {
183
183
  BaseDataSource,
184
184
  datasource,
185
- TNodePostgresConnector,
186
185
  ValueOrPromise,
187
186
  } from '@venizia/ignis';
188
187
  import { drizzle } from 'drizzle-orm/node-postgres';
@@ -205,7 +204,7 @@ interface IDSConfigs {
205
204
  * 3. Drizzle is initialized with the auto-discovered schema
206
205
  */
207
206
  @datasource({ driver: 'node-postgres' })
208
- export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, IDSConfigs> {
207
+ export class PostgresDataSource extends BaseDataSource<IDSConfigs> {
209
208
  constructor() {
210
209
  super({
211
210
  name: PostgresDataSource.name,
@@ -243,7 +242,7 @@ export class PostgresDataSource extends BaseDataSource<TNodePostgresConnector, I
243
242
  - Schema is auto-discovered from `@repository` decorators - no manual registration needed
244
243
  - Uses `getSchema()` for lazy schema resolution (resolves when all models are loaded)
245
244
  - Uses environment variables for connection config
246
- - Implements connection lifecycle methods (`connect()`, `disconnect()`)
245
+ - Implements `configure()` for connection setup and `getConnectionString()` for URL generation
247
246
 
248
247
  > **Deep Dive:** See [DataSources Reference](/references/base/datasources) for advanced configuration and multiple database support.
249
248
 
@@ -306,24 +305,18 @@ Dependency Injection (DI) is a design pattern where objects receive their depend
306
305
 
307
306
  `ControllerFactory` generates a full CRUD controller with automatic validation and OpenAPI docs.
308
307
 
309
- Create `src/controllers/todo.controller.ts`:
308
+ Create `src/controllers/todo/definitions.ts`:
310
309
 
311
310
  ```typescript
312
- // src/controllers/todo.controller.ts
311
+ // src/controllers/todo/definitions.ts
313
312
  import { Todo } from '@/models/todo.model';
314
313
  import { TodoRepository } from '@/repositories/todo.repository';
315
- import {
316
- BindingKeys,
317
- BindingNamespaces,
318
- controller,
319
- ControllerFactory,
320
- inject,
321
- } from '@venizia/ignis';
314
+ import { ControllerFactory } from '@venizia/ignis';
322
315
 
323
- const BASE_PATH = '/todos';
316
+ export const BASE_PATH = '/todos';
324
317
 
325
- // 1. The factory generates a controller class with all CRUD routes
326
- const _Controller = ControllerFactory.defineCrudController({
318
+ // The factory generates a controller class with all CRUD routes
319
+ export const _Controller = ControllerFactory.defineCrudController({
327
320
  repository: { name: TodoRepository.name },
328
321
  controller: {
329
322
  name: 'TodoController',
@@ -331,8 +324,17 @@ const _Controller = ControllerFactory.defineCrudController({
331
324
  },
332
325
  entity: () => Todo, // The entity is used to generate OpenAPI schemas
333
326
  });
327
+ ```
328
+
329
+ Create `src/controllers/todo/todo.controller.ts`:
334
330
 
335
- // 2. Extend the generated controller to inject the repository
331
+ ```typescript
332
+ // src/controllers/todo/todo.controller.ts
333
+ import { TodoRepository } from '@/repositories/todo.repository';
334
+ import { BindingKeys, BindingNamespaces, controller, inject } from '@venizia/ignis';
335
+ import { BASE_PATH, _Controller } from './definitions';
336
+
337
+ // Extend the generated controller to inject the repository
336
338
  @controller({ path: BASE_PATH })
337
339
  export class TodoController extends _Controller {
338
340
  constructor(
@@ -349,6 +351,12 @@ export class TodoController extends _Controller {
349
351
  }
350
352
  ```
351
353
 
354
+ Create `src/controllers/todo/index.ts`:
355
+
356
+ ```typescript
357
+ export * from './todo.controller';
358
+ ```
359
+
352
360
  **Auto-generated Endpoints:**
353
361
  | Method | Path | Description |
354
362
  |--------|------|-------------|
@@ -371,13 +379,13 @@ Update `src/application.ts` to register all components:
371
379
  ```typescript
372
380
  // src/application.ts
373
381
  import { BaseApplication, IApplicationConfigs, IApplicationInfo, SwaggerComponent, ValueOrPromise } from '@venizia/ignis';
374
- import { HelloController } from './controllers/hello.controller';
382
+ import { HelloController } from './controllers/hello';
375
383
  import packageJson from '../package.json';
376
384
 
377
385
  // Import our new components
378
386
  import { PostgresDataSource } from './datasources/postgres.datasource';
379
387
  import { TodoRepository } from './repositories/todo.repository';
380
- import { TodoController } from './controllers/todo.controller';
388
+ import { TodoController } from './controllers/todo';
381
389
 
382
390
  export const appConfigs: IApplicationConfigs = {
383
391
  host: process.env.HOST ?? '0.0.0.0',
@@ -413,6 +421,9 @@ export class Application extends BaseApplication {
413
421
  }
414
422
  ```
415
423
 
424
+ > [!IMPORTANT] Registration Order
425
+ > Register in this order: **DataSources → Repositories → Services → Controllers**. DataSources must exist before Repositories that reference them. The framework resolves dependencies during initialization, so registering out of order will cause "Binding not found" errors.
426
+
416
427
  ## Step 7: Run Database Migration
417
428
 
418
429
  ### Understanding Database Migrations
@@ -462,32 +473,27 @@ Add these scripts to your `package.json`:
462
473
 
463
474
  ```json
464
475
  "scripts": {
465
- "migrate:dev": "NODE_ENV=development drizzle-kit migrate --config=src/migration.ts",
466
- "generate-migration:dev": "NODE_ENV=development drizzle-kit generate --config=src/migration.ts"
476
+ "db:push": "NODE_ENV=development drizzle-kit push --config=src/migration.ts",
477
+ "db:generate": "NODE_ENV=development drizzle-kit generate --config=src/migration.ts",
478
+ "db:migrate": "NODE_ENV=development drizzle-kit migrate --config=src/migration.ts"
467
479
  }
468
480
  ```
469
481
 
470
482
  ### Run the Migration
471
483
 
484
+ For development, use `push` — it reads your schema and applies changes directly to the database:
485
+
472
486
  ```bash
473
- bun run migrate:dev
487
+ bun run db:push
474
488
  ```
475
489
 
476
490
  **What happens when you run this:**
477
491
 
478
492
  1. **Reads** `src/models/todo.model.ts` to see what your schema looks like
479
- 2. **Generates SQL** to create the `Todo` table
480
- 3. **Connects** to your PostgreSQL database
481
- 4. **Executes** the SQL to create the table
482
- 5. **Saves** migration files to `./migration/` folder (for version control)
493
+ 2. **Compares** it against the current database state
494
+ 3. **Generates and executes** the SQL to create/update tables
483
495
 
484
- **Expected output:**
485
- ```
486
- Reading schema...
487
- Generating migration...
488
- Executing migration...
489
- ✓ Done!
490
- ```
496
+ > **Production workflow:** Use `db:generate` to create versioned migration files, then `db:migrate` to apply them. This gives you version control and rollback capability. `push` is simpler but skips the migration file step.
491
497
 
492
498
  **Verify it worked:**
493
499
  ```bash
@@ -574,7 +580,7 @@ sudo service postgresql start # Linux
574
580
 
575
581
  **Fix:**
576
582
  ```bash
577
- bun run migrate:dev
583
+ bun run db:push
578
584
  ```
579
585
 
580
586
  **Verify the table exists:**
@@ -629,9 +635,9 @@ Now that you've built the Todo API, try building a **User** feature on your own!
629
635
  |:----:|------|
630
636
  | 1 | Create `src/models/user.model.ts` |
631
637
  | 2 | Create `src/repositories/user.repository.ts` (auto-registers User with PostgresDataSource) |
632
- | 3 | Create `src/controllers/user.controller.ts` |
638
+ | 3 | Create `src/controllers/user/` (definitions.ts, user.controller.ts, index.ts) |
633
639
  | 4 | Register repository and controller in `application.ts` |
634
- | 5 | Run migration: `bun run migrate:dev` |
640
+ | 5 | Push schema: `bun run db:push` |
635
641
  | 6 | Test with curl |
636
642
 
637
643
  **Hint:** Follow the exact same pattern as `Todo`. The only changes are the model name and fields!