odgn-rights 0.1.0 → 0.5.0
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/README.md +489 -0
- package/dist/adapters/base-adapter.d.ts +83 -0
- package/dist/adapters/base-adapter.js +142 -0
- package/dist/adapters/factories.d.ts +31 -0
- package/dist/adapters/factories.js +48 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/postgres-adapter.d.ts +51 -0
- package/dist/adapters/postgres-adapter.js +469 -0
- package/dist/adapters/redis-adapter.d.ts +84 -0
- package/dist/adapters/redis-adapter.js +673 -0
- package/dist/adapters/schema.d.ts +25 -0
- package/dist/adapters/schema.js +186 -0
- package/dist/adapters/sqlite-adapter.d.ts +78 -0
- package/dist/adapters/sqlite-adapter.js +655 -0
- package/dist/adapters/types.d.ts +174 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/check.d.ts +2 -0
- package/dist/cli/commands/check.js +38 -0
- package/dist/cli/commands/explain.d.ts +2 -0
- package/dist/cli/commands/explain.js +93 -0
- package/dist/cli/commands/validate.d.ts +2 -0
- package/dist/cli/commands/validate.js +177 -0
- package/dist/cli/helpers/config-loader.d.ts +3 -0
- package/dist/cli/helpers/config-loader.js +13 -0
- package/dist/cli/helpers/flag-parser.d.ts +3 -0
- package/dist/cli/helpers/flag-parser.js +40 -0
- package/dist/cli/helpers/output.d.ts +10 -0
- package/dist/cli/helpers/output.js +29 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/types.d.ts +10 -0
- package/dist/cli/types.js +1 -0
- package/dist/helpers.d.ts +16 -0
- package/dist/{utils.js → helpers.js} +22 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/integrations/elysia.d.ts +235 -0
- package/dist/integrations/elysia.js +375 -0
- package/dist/right.d.ts +25 -1
- package/dist/right.js +183 -19
- package/dist/rights.d.ts +31 -0
- package/dist/rights.js +162 -31
- package/dist/role-registry.d.ts +9 -0
- package/dist/role-registry.js +15 -0
- package/dist/role.d.ts +3 -1
- package/dist/role.js +11 -0
- package/dist/subject-registry.d.ts +77 -0
- package/dist/subject-registry.js +123 -0
- package/dist/subject.d.ts +21 -2
- package/dist/subject.js +51 -8
- package/package.json +63 -7
- package/dist/utils.d.ts +0 -2
package/README.md
CHANGED
|
@@ -98,6 +98,100 @@ rights.write('/posts/1', { userId: 'abc', ownerId: 'xyz' }); // false
|
|
|
98
98
|
- `**` matches across segments (`/*/device/**`)
|
|
99
99
|
- `?` matches a single character (no slash)
|
|
100
100
|
|
|
101
|
+
## Rule Priority
|
|
102
|
+
|
|
103
|
+
By default, rules are matched by **specificity** — the most specific matching rule wins. However, you can override this with explicit **priority** values.
|
|
104
|
+
|
|
105
|
+
### How Priority Works
|
|
106
|
+
|
|
107
|
+
1. **Higher priority wins** regardless of specificity
|
|
108
|
+
2. **Equal priorities** fall back to specificity comparison
|
|
109
|
+
3. **Default priority is 0** when not specified
|
|
110
|
+
4. **Negative priorities** can deprioritize rules below the default
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const rights = new Rights();
|
|
114
|
+
|
|
115
|
+
// Specific path, default priority (0)
|
|
116
|
+
rights.add(
|
|
117
|
+
new Right('/posts/123', {
|
|
118
|
+
allow: [Flags.READ],
|
|
119
|
+
deny: [Flags.WRITE]
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Wildcard path, but high priority (100) — this rule wins!
|
|
124
|
+
rights.add(
|
|
125
|
+
new Right('/posts/*', {
|
|
126
|
+
allow: [Flags.READ, Flags.WRITE],
|
|
127
|
+
priority: 100
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
rights.write('/posts/123'); // true — high-priority wildcard rule wins
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Priority in Serialization
|
|
135
|
+
|
|
136
|
+
Priority is included in both text and JSON serialization formats.
|
|
137
|
+
|
|
138
|
+
**Text format** uses `^` after the path:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const right = new Right('/posts/*', {
|
|
142
|
+
allow: [Flags.WRITE],
|
|
143
|
+
priority: 100
|
|
144
|
+
});
|
|
145
|
+
right.toString(); // "+w:/posts/*^100"
|
|
146
|
+
|
|
147
|
+
// With tags and time ranges
|
|
148
|
+
// Format: [flags]:[path]^[priority]#[tags]@[validFrom]/[validUntil]
|
|
149
|
+
Right.parse('+rw:/admin/*^50#secure');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**JSON format** includes an optional `priority` field:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
[
|
|
156
|
+
{ "path": "/posts/*", "allow": "rw", "priority": 100 },
|
|
157
|
+
{ "path": "/posts/123", "allow": "r", "deny": "w" }
|
|
158
|
+
]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Priority is omitted from serialization when it equals 0 (the default).
|
|
162
|
+
|
|
163
|
+
### Use Cases
|
|
164
|
+
|
|
165
|
+
- **Emergency overrides**: Grant temporary high-priority access that bypasses normal rules
|
|
166
|
+
- **Default deny rules**: Use negative priority for fallback deny rules
|
|
167
|
+
- **Policy layers**: Implement organizational policies at different priority levels
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// Low-priority default: deny all writes
|
|
171
|
+
rights.add(
|
|
172
|
+
new Right('/**', {
|
|
173
|
+
deny: [Flags.WRITE],
|
|
174
|
+
priority: -100
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Normal priority: department-level permissions
|
|
179
|
+
rights.add(
|
|
180
|
+
new Right('/dept/engineering/**', {
|
|
181
|
+
allow: [Flags.READ, Flags.WRITE]
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// High priority: emergency maintenance access
|
|
186
|
+
rights.add(
|
|
187
|
+
new Right('/system/**', {
|
|
188
|
+
allow: [Flags.ALL],
|
|
189
|
+
priority: 1000,
|
|
190
|
+
tags: ['emergency']
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
101
195
|
## JSON Round‑Trip
|
|
102
196
|
|
|
103
197
|
```ts
|
|
@@ -105,3 +199,398 @@ const json = rights.toJSON();
|
|
|
105
199
|
// [ { path: '/', allow: 'r' }, { path: '/*/device/**', allow: 'c' }, ... ]
|
|
106
200
|
const loaded = Rights.fromJSON(json);
|
|
107
201
|
```
|
|
202
|
+
|
|
203
|
+
## Batch Permission Checks
|
|
204
|
+
|
|
205
|
+
Efficiently check multiple permissions at once with `checkMany()`.
|
|
206
|
+
|
|
207
|
+
### Basic Usage
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const rights = new Rights();
|
|
211
|
+
rights.allow('/users/*', Flags.READ);
|
|
212
|
+
rights.allow('/posts/*', Flags.WRITE);
|
|
213
|
+
rights.deny('/admin', Flags.ALL);
|
|
214
|
+
|
|
215
|
+
const results = rights.checkMany([
|
|
216
|
+
{ path: '/users/1', flags: Flags.READ },
|
|
217
|
+
{ path: '/posts/1', flags: Flags.WRITE },
|
|
218
|
+
{ path: '/admin', flags: Flags.ALL }
|
|
219
|
+
]);
|
|
220
|
+
// Returns: [true, true, false]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### With Context
|
|
224
|
+
|
|
225
|
+
The same context is shared across all checks:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
rights.add(
|
|
229
|
+
new Right('/posts/*', {
|
|
230
|
+
allow: [Flags.WRITE],
|
|
231
|
+
condition: ctx => ctx.userId === ctx.ownerId
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const results = rights.checkMany(
|
|
236
|
+
[
|
|
237
|
+
{ path: '/posts/1', flags: Flags.WRITE },
|
|
238
|
+
{ path: '/posts/2', flags: Flags.WRITE },
|
|
239
|
+
{ path: '/posts/3', flags: Flags.WRITE }
|
|
240
|
+
],
|
|
241
|
+
{ userId: 'user1', ownerId: 'user1' }
|
|
242
|
+
);
|
|
243
|
+
// Returns: [true, true, true]
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### With Subjects
|
|
247
|
+
|
|
248
|
+
Works with subjects that have multiple roles:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const viewer = new Role('viewer', new Rights().allow('/docs', Flags.READ));
|
|
252
|
+
const writer = new Role('writer', new Rights().allow('/docs', Flags.WRITE));
|
|
253
|
+
|
|
254
|
+
const subject = new Subject().memberOf(viewer).memberOf(writer);
|
|
255
|
+
|
|
256
|
+
const results = subject.checkMany([
|
|
257
|
+
{ path: '/docs', flags: Flags.READ },
|
|
258
|
+
{ path: '/docs', flags: Flags.WRITE },
|
|
259
|
+
{ path: '/docs', flags: Flags.DELETE }
|
|
260
|
+
]);
|
|
261
|
+
// Returns: [true, true, false]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Use Cases
|
|
265
|
+
|
|
266
|
+
- **Bulk authorization**: Check multiple resource permissions in a single call
|
|
267
|
+
- **Feature flags**: Enable/disable multiple features based on permissions
|
|
268
|
+
- **API responses**: Include permission information for multiple resources
|
|
269
|
+
- **UI rendering**: Determine visibility of multiple UI elements efficiently
|
|
270
|
+
|
|
271
|
+
## CLI Tool
|
|
272
|
+
|
|
273
|
+
The CLI tool helps test and debug permission configurations from the command line.
|
|
274
|
+
|
|
275
|
+
### Installation
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Install globally
|
|
279
|
+
npm install -g @odgn/rights
|
|
280
|
+
|
|
281
|
+
# Or use with npx
|
|
282
|
+
npx @odgn/rights --help
|
|
283
|
+
|
|
284
|
+
# Or run directly with bun
|
|
285
|
+
bun run src/cli/index.ts --help
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Commands
|
|
289
|
+
|
|
290
|
+
#### check
|
|
291
|
+
|
|
292
|
+
Test if a permission is allowed:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
# Basic usage
|
|
296
|
+
odgn-rights check -c config.json -p /users/123 -f READ
|
|
297
|
+
|
|
298
|
+
# With combined flags
|
|
299
|
+
odgn-rights check -c config.json -p /users/123 -f RW
|
|
300
|
+
|
|
301
|
+
# With comma-separated flags
|
|
302
|
+
odgn-rights check -c config.json -p /users/123 -f READ,WRITE
|
|
303
|
+
|
|
304
|
+
# Quiet mode for scripting (outputs 'true' or 'false')
|
|
305
|
+
odgn-rights check -c config.json -p /users/123 -f READ --quiet
|
|
306
|
+
|
|
307
|
+
# With context for conditional rights
|
|
308
|
+
odgn-rights check -c config.json -p /posts/1 -f WRITE --context '{"userId":"abc","ownerId":"abc"}'
|
|
309
|
+
|
|
310
|
+
# Override time for time-based rights
|
|
311
|
+
odgn-rights check -c config.json -p /scheduled -f READ --time 2025-06-15T12:00:00Z
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Exit codes: `0` = allowed, `1` = denied, `2` = error
|
|
315
|
+
|
|
316
|
+
#### explain
|
|
317
|
+
|
|
318
|
+
Understand why a permission is allowed or denied:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# Basic usage
|
|
322
|
+
odgn-rights explain -c config.json -p /users/123 -f WRITE
|
|
323
|
+
|
|
324
|
+
# JSON output
|
|
325
|
+
odgn-rights explain -c config.json -p /users/123 -f READ --json
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The explain command shows:
|
|
329
|
+
|
|
330
|
+
- Decision breakdown per flag
|
|
331
|
+
- Matching rules sorted by specificity
|
|
332
|
+
- Suggestions for granting denied permissions
|
|
333
|
+
|
|
334
|
+
#### validate
|
|
335
|
+
|
|
336
|
+
Validate a configuration file:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
# Validate JSON config
|
|
340
|
+
odgn-rights validate config.json
|
|
341
|
+
|
|
342
|
+
# Validate string format config
|
|
343
|
+
odgn-rights validate config.txt
|
|
344
|
+
|
|
345
|
+
# Strict mode (warns on broad patterns like /**)
|
|
346
|
+
odgn-rights validate --strict config.json
|
|
347
|
+
|
|
348
|
+
# JSON output
|
|
349
|
+
odgn-rights validate --json config.json
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Exit codes: `0` = valid, `1` = validation errors, `2` = file error
|
|
353
|
+
|
|
354
|
+
### Configuration Formats
|
|
355
|
+
|
|
356
|
+
The CLI supports two configuration formats:
|
|
357
|
+
|
|
358
|
+
**JSON format** (`config.json`):
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
[
|
|
362
|
+
{ "path": "/", "allow": "r" },
|
|
363
|
+
{ "path": "/users/*", "allow": "rw" },
|
|
364
|
+
{ "path": "/admin/**", "allow": "*", "tags": ["admin"] },
|
|
365
|
+
{ "path": "/scheduled", "allow": "r", "validFrom": "2025-01-01T00:00:00Z" }
|
|
366
|
+
]
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**String format** (`config.txt`):
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
# Comments start with #
|
|
373
|
+
+r:/
|
|
374
|
+
+rw:/users/*
|
|
375
|
+
+*:/admin/**
|
|
376
|
+
-d+rw:/public
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Flag Reference
|
|
380
|
+
|
|
381
|
+
| Flag | Letter | Description |
|
|
382
|
+
| ------- | ------ | ------------------ |
|
|
383
|
+
| READ | R | Read permission |
|
|
384
|
+
| WRITE | W | Write permission |
|
|
385
|
+
| CREATE | C | Create permission |
|
|
386
|
+
| DELETE | D | Delete permission |
|
|
387
|
+
| EXECUTE | X | Execute permission |
|
|
388
|
+
| ALL | \* | All permissions |
|
|
389
|
+
|
|
390
|
+
Flags can be combined: `RW`, `READ,WRITE`, `RWCDX`
|
|
391
|
+
|
|
392
|
+
## Database Adapters
|
|
393
|
+
|
|
394
|
+
Database adapters enable persistent storage of rights configurations in SQLite or PostgreSQL databases. This is useful for applications that need to load permissions from a database, share configurations across services, or audit permission changes.
|
|
395
|
+
|
|
396
|
+
### Installation
|
|
397
|
+
|
|
398
|
+
The adapters use Bun's built-in database drivers (`bun:sqlite` and `bun` SQL), so no additional dependencies are required.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { PostgresAdapter, SQLiteAdapter } from 'odgn-rights/adapters';
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Table Prefix
|
|
405
|
+
|
|
406
|
+
All adapters support a configurable table prefix. The default prefix is `tbl_`.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// Default prefix creates tables: tbl_rights, tbl_roles, etc.
|
|
410
|
+
const adapter = new SQLiteAdapter({ filename: './permissions.db' });
|
|
411
|
+
|
|
412
|
+
// Custom prefix creates tables: auth_rights, auth_roles, etc.
|
|
413
|
+
const adapter = new SQLiteAdapter({
|
|
414
|
+
filename: './permissions.db',
|
|
415
|
+
tablePrefix: 'auth_'
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// No prefix creates tables: rights, roles, etc.
|
|
419
|
+
const adapter = new SQLiteAdapter({
|
|
420
|
+
filename: './permissions.db',
|
|
421
|
+
tablePrefix: ''
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### SQLite Adapter
|
|
426
|
+
|
|
427
|
+
SQLite is ideal for single-process applications, embedded systems, or local development.
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
import { Flags, Right, Rights } from 'odgn-rights';
|
|
431
|
+
import { SQLiteAdapter } from 'odgn-rights/adapters';
|
|
432
|
+
|
|
433
|
+
// Create adapter and connect
|
|
434
|
+
const adapter = new SQLiteAdapter({
|
|
435
|
+
filename: './permissions.db', // Use ':memory:' for in-memory
|
|
436
|
+
enableWAL: true // Enable WAL mode for better concurrency
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
await adapter.connect();
|
|
440
|
+
await adapter.migrate();
|
|
441
|
+
|
|
442
|
+
// Save rights
|
|
443
|
+
const rights = new Rights();
|
|
444
|
+
rights.allow('/users/*', Flags.READ);
|
|
445
|
+
rights.allow('/admin/**', Flags.ALL);
|
|
446
|
+
await adapter.saveRights(rights);
|
|
447
|
+
|
|
448
|
+
// Load rights
|
|
449
|
+
const loaded = await adapter.loadRights();
|
|
450
|
+
loaded.has('/users/123', Flags.READ); // true
|
|
451
|
+
|
|
452
|
+
// Save and load roles
|
|
453
|
+
const { Role, RoleRegistry } = await import('odgn-rights');
|
|
454
|
+
const registry = new RoleRegistry();
|
|
455
|
+
const admin = registry.define('admin');
|
|
456
|
+
admin.rights.allow('/**', Flags.ALL);
|
|
457
|
+
await registry.saveTo(adapter);
|
|
458
|
+
|
|
459
|
+
// Load registry from database
|
|
460
|
+
const loadedRegistry = await RoleRegistry.loadFrom(adapter);
|
|
461
|
+
|
|
462
|
+
await adapter.disconnect();
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### PostgreSQL Adapter
|
|
466
|
+
|
|
467
|
+
PostgreSQL is ideal for multi-process applications, microservices, or when you need shared access to permissions.
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { Flags, RoleRegistry, Subject } from 'odgn-rights';
|
|
471
|
+
import { PostgresAdapter } from 'odgn-rights/adapters';
|
|
472
|
+
|
|
473
|
+
const adapter = new PostgresAdapter({
|
|
474
|
+
url: 'postgres://user:pass@localhost:5432/mydb',
|
|
475
|
+
// Or use individual options:
|
|
476
|
+
// hostname: 'localhost',
|
|
477
|
+
// port: 5432,
|
|
478
|
+
// database: 'mydb',
|
|
479
|
+
// username: 'user',
|
|
480
|
+
// password: 'pass',
|
|
481
|
+
tablePrefix: 'perms_' // Optional custom prefix
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await adapter.connect();
|
|
485
|
+
await adapter.migrate();
|
|
486
|
+
|
|
487
|
+
// Load registry and make changes
|
|
488
|
+
const registry = await adapter.loadRegistry();
|
|
489
|
+
const editor = registry.define('editor');
|
|
490
|
+
editor.rights.allow('/content/**', Flags.READ, Flags.WRITE);
|
|
491
|
+
|
|
492
|
+
// Save back
|
|
493
|
+
await adapter.saveRegistry(registry);
|
|
494
|
+
|
|
495
|
+
// Save subjects with roles
|
|
496
|
+
const user = new Subject();
|
|
497
|
+
user.memberOf(editor);
|
|
498
|
+
await adapter.saveSubject('user-123', user);
|
|
499
|
+
|
|
500
|
+
await adapter.disconnect();
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Factory Functions
|
|
504
|
+
|
|
505
|
+
Convenience functions for common patterns:
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import {
|
|
509
|
+
createPostgresRegistry,
|
|
510
|
+
createPostgresRights,
|
|
511
|
+
createSQLiteRegistry,
|
|
512
|
+
createSQLiteRights
|
|
513
|
+
} from 'odgn-rights/adapters';
|
|
514
|
+
|
|
515
|
+
// Create SQLite adapter with rights
|
|
516
|
+
const { adapter, rights } = await createSQLiteRights({
|
|
517
|
+
filename: './permissions.db'
|
|
518
|
+
});
|
|
519
|
+
rights.allow('/public/**', Flags.READ);
|
|
520
|
+
await adapter.saveRights(rights);
|
|
521
|
+
await adapter.disconnect();
|
|
522
|
+
|
|
523
|
+
// Create SQLite adapter with registry
|
|
524
|
+
const { adapter: regAdapter, registry } = await createSQLiteRegistry({
|
|
525
|
+
filename: ':memory:'
|
|
526
|
+
});
|
|
527
|
+
const viewer = registry.define('viewer');
|
|
528
|
+
viewer.rights.allow('/read/*', Flags.READ);
|
|
529
|
+
await registry.saveTo(regAdapter);
|
|
530
|
+
await regAdapter.disconnect();
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Transactions
|
|
534
|
+
|
|
535
|
+
Both adapters support transactions for atomic operations:
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
await adapter.transaction(async () => {
|
|
539
|
+
await adapter.saveRight(new Right('/a', { allow: [Flags.READ] }));
|
|
540
|
+
await adapter.saveRight(new Right('/b', { allow: [Flags.WRITE] }));
|
|
541
|
+
// If an error is thrown, all changes are rolled back
|
|
542
|
+
});
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Adapter Interface
|
|
546
|
+
|
|
547
|
+
All adapters implement the `DatabaseAdapter` interface:
|
|
548
|
+
|
|
549
|
+
| Method | Description |
|
|
550
|
+
| ---------------------------------- | --------------------------------- |
|
|
551
|
+
| `connect()` | Connect to the database |
|
|
552
|
+
| `disconnect()` | Disconnect from the database |
|
|
553
|
+
| `migrate()` | Create or update schema |
|
|
554
|
+
| `saveRight(right)` | Save a single right |
|
|
555
|
+
| `saveRights(rights)` | Save multiple rights |
|
|
556
|
+
| `loadRight(id)` | Load a right by ID |
|
|
557
|
+
| `loadRights()` | Load all rights |
|
|
558
|
+
| `loadRightsByPath(pattern)` | Load rights matching a pattern |
|
|
559
|
+
| `deleteRight(id)` | Delete a right |
|
|
560
|
+
| `saveRole(role)` | Save a role with its rights |
|
|
561
|
+
| `loadRole(name)` | Load a role by name |
|
|
562
|
+
| `loadRoles()` | Load all roles |
|
|
563
|
+
| `deleteRole(name)` | Delete a role |
|
|
564
|
+
| `saveRegistry(registry)` | Save entire RoleRegistry |
|
|
565
|
+
| `loadRegistry()` | Load RoleRegistry with all roles |
|
|
566
|
+
| `saveSubject(identifier, subject)` | Save a subject |
|
|
567
|
+
| `loadSubject(identifier)` | Load a subject |
|
|
568
|
+
| `deleteSubject(identifier)` | Delete a subject |
|
|
569
|
+
| `clear()` | Clear all data (for testing) |
|
|
570
|
+
| `transaction(fn)` | Execute operations in transaction |
|
|
571
|
+
|
|
572
|
+
### Database Schema
|
|
573
|
+
|
|
574
|
+
The adapters create the following tables (with the configured prefix):
|
|
575
|
+
|
|
576
|
+
| Table | Purpose |
|
|
577
|
+
| -------------------------- | ------------------------------------ |
|
|
578
|
+
| `{prefix}rights` | Individual rights with paths & flags |
|
|
579
|
+
| `{prefix}roles` | Role definitions |
|
|
580
|
+
| `{prefix}role_rights` | Role-to-rights mapping |
|
|
581
|
+
| `{prefix}role_inheritance` | Role inheritance relationships |
|
|
582
|
+
| `{prefix}subjects` | Subject records |
|
|
583
|
+
| `{prefix}subject_roles` | Subject-to-roles mapping |
|
|
584
|
+
| `{prefix}subject_rights` | Direct subject rights |
|
|
585
|
+
|
|
586
|
+
### Persistence Metadata
|
|
587
|
+
|
|
588
|
+
Rights saved to the database receive a `dbId` property:
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
const right = new Right('/test', { allow: [Flags.READ] });
|
|
592
|
+
console.log(right.dbId); // undefined
|
|
593
|
+
|
|
594
|
+
await adapter.saveRight(right);
|
|
595
|
+
console.log(right.dbId); // 1 (database ID)
|
|
596
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Flags } from '../constants';
|
|
2
|
+
import { Right } from '../right';
|
|
3
|
+
import { Rights } from '../rights';
|
|
4
|
+
import { Role } from '../role';
|
|
5
|
+
import { RoleRegistry } from '../role-registry';
|
|
6
|
+
import { Subject } from '../subject';
|
|
7
|
+
import type { BaseAdapterOptions, DatabaseAdapter, RightsRow, TableNames } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for database adapters.
|
|
10
|
+
* Provides common utility methods for serialization/deserialization
|
|
11
|
+
* and table name management.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class BaseAdapter implements DatabaseAdapter {
|
|
14
|
+
protected readonly tablePrefix: string;
|
|
15
|
+
protected readonly _tables: TableNames;
|
|
16
|
+
constructor(options?: BaseAdapterOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Get the table names with the configured prefix
|
|
19
|
+
*/
|
|
20
|
+
protected get tables(): TableNames;
|
|
21
|
+
/**
|
|
22
|
+
* Serialize tags array to JSON string
|
|
23
|
+
*/
|
|
24
|
+
protected serializeTags: (tags: string[]) => string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Deserialize JSON string to tags array
|
|
27
|
+
*/
|
|
28
|
+
protected deserializeTags: (json: string | null) => string[];
|
|
29
|
+
/**
|
|
30
|
+
* Convert a Right instance to a database row (partial, without id and timestamps)
|
|
31
|
+
*/
|
|
32
|
+
protected rightToRow: (right: Right) => Omit<RightsRow, "id" | "created_at" | "updated_at">;
|
|
33
|
+
/**
|
|
34
|
+
* Convert a database row to a Right instance
|
|
35
|
+
*/
|
|
36
|
+
protected rowToRight: (row: RightsRow) => Right;
|
|
37
|
+
/**
|
|
38
|
+
* Convert a bitmask to an array of Flag values
|
|
39
|
+
*/
|
|
40
|
+
protected maskToFlags: (mask: number) => Flags[];
|
|
41
|
+
/**
|
|
42
|
+
* Convert an array of Flag values to a bitmask
|
|
43
|
+
*/
|
|
44
|
+
protected flagsToMask: (flags: Flags[]) => number;
|
|
45
|
+
/**
|
|
46
|
+
* Parse an ISO 8601 timestamp string to a Date, or undefined if null/invalid
|
|
47
|
+
*/
|
|
48
|
+
protected parseTimestamp: (value: string | null) => Date | undefined;
|
|
49
|
+
abstract connect(): Promise<void>;
|
|
50
|
+
abstract disconnect(): Promise<void>;
|
|
51
|
+
abstract migrate(): Promise<void>;
|
|
52
|
+
abstract saveRight(right: Right): Promise<number>;
|
|
53
|
+
abstract saveRights(rights: Rights): Promise<number[]>;
|
|
54
|
+
abstract loadRight(id: number): Promise<Right | null>;
|
|
55
|
+
abstract loadRights(): Promise<Rights>;
|
|
56
|
+
abstract loadRightsByPath(pathPattern: string): Promise<Rights>;
|
|
57
|
+
abstract deleteRight(id: number): Promise<boolean>;
|
|
58
|
+
abstract saveRole(role: Role): Promise<number>;
|
|
59
|
+
abstract loadRole(name: string): Promise<Role | null>;
|
|
60
|
+
abstract loadRoles(): Promise<Role[]>;
|
|
61
|
+
abstract deleteRole(name: string): Promise<boolean>;
|
|
62
|
+
abstract saveRegistry(registry: RoleRegistry): Promise<void>;
|
|
63
|
+
abstract loadRegistry(): Promise<RoleRegistry>;
|
|
64
|
+
abstract saveSubject(identifier: string, subject: Subject): Promise<number>;
|
|
65
|
+
abstract loadSubject(identifier: string): Promise<Subject | null>;
|
|
66
|
+
abstract deleteSubject(identifier: string): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Get all subject identifiers from the database.
|
|
69
|
+
* Used by findSubjectsWithAccess and can be overridden for optimization.
|
|
70
|
+
*/
|
|
71
|
+
protected abstract getAllSubjectIdentifiers(): Promise<string[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Find all subject identifiers that have access to a specific path with given flags.
|
|
74
|
+
* Default implementation uses getAllSubjectIdentifiers + loadSubject.
|
|
75
|
+
* Subclasses can override with optimized batch loading implementations.
|
|
76
|
+
* @param pathPattern The path pattern to check (supports wildcards)
|
|
77
|
+
* @param flags The flags to check for
|
|
78
|
+
* @returns Array of subject identifiers that have access
|
|
79
|
+
*/
|
|
80
|
+
findSubjectsWithAccess(pathPattern: string, flags: Flags): Promise<string[]>;
|
|
81
|
+
abstract clear(): Promise<void>;
|
|
82
|
+
abstract transaction<T>(fn: (adapter: DatabaseAdapter) => Promise<T>): Promise<T>;
|
|
83
|
+
}
|