@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.
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +1 -1
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/package.json +14 -14
- package/wiki/extensions/components/authorization/api.md +239 -376
- package/wiki/extensions/components/authorization/errors.md +52 -43
- package/wiki/extensions/components/authorization/index.md +127 -65
- package/wiki/extensions/components/authorization/usage.md +198 -98
- package/wiki/extensions/helpers/kafka/consumer.md +6 -5
- package/wiki/extensions/helpers/kafka/examples.md +1 -1
- package/wiki/extensions/helpers/kafka/index.md +16 -12
- package/wiki/extensions/helpers/kafka/producer.md +4 -3
- package/wiki/guides/core-concepts/persistent/datasources.md +10 -11
- package/wiki/guides/core-concepts/persistent/index.md +6 -6
- package/wiki/guides/core-concepts/persistent/models.md +7 -5
- package/wiki/guides/core-concepts/persistent/repositories.md +11 -3
- package/wiki/guides/core-concepts/persistent/transactions.md +2 -1
- package/wiki/guides/core-concepts/rest-controllers.md +2 -2
- package/wiki/guides/core-concepts/services.md +0 -1
- package/wiki/guides/get-started/5-minute-quickstart.md +11 -10
- package/wiki/guides/migrations/scoped-rbac-migration.md +300 -0
- package/wiki/guides/tutorials/building-a-crud-api.md +43 -37
- package/wiki/guides/tutorials/complete-installation.md +64 -44
- package/wiki/guides/tutorials/ecommerce-api.md +21 -12
- package/wiki/guides/tutorials/realtime-chat.md +4 -5
- package/wiki/references/base/filter-system/default-filter.md +6 -3
- package/wiki/references/base/filter-system/fields-order-pagination.md +26 -0
- package/wiki/references/base/models.md +6 -3
- 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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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()` |
|
|
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
|
|
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
|
|
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<
|
|
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
|
|
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.
|
|
308
|
+
Create `src/controllers/todo/definitions.ts`:
|
|
310
309
|
|
|
311
310
|
```typescript
|
|
312
|
-
// src/controllers/todo.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"
|
|
466
|
-
"generate
|
|
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
|
|
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. **
|
|
480
|
-
3. **
|
|
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
|
-
**
|
|
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
|
|
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 |
|
|
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!
|