odgn-rights 0.2.0 → 0.5.1

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.
@@ -0,0 +1,11 @@
1
+ export type { BaseAdapterOptions, DatabaseAdapter, RightsRow, RoleInheritanceRow, RoleRightRow, RoleRow, SubjectRightRow, SubjectRoleRow, SubjectRow, TableNames } from './types';
2
+ export { createTableNames, DEFAULT_TABLE_PREFIX, generatePostgresDropSchema, generatePostgresSchema, generateSQLiteDropSchema, generateSQLiteSchema } from './schema';
3
+ export { BaseAdapter } from './base-adapter';
4
+ export { SQLiteAdapter } from './sqlite-adapter';
5
+ export type { SQLiteAdapterOptions } from './sqlite-adapter';
6
+ export { PostgresAdapter } from './postgres-adapter';
7
+ export type { PostgresAdapterOptions } from './postgres-adapter';
8
+ export { RedisAdapter } from './redis-adapter';
9
+ export type { RedisAdapterOptions } from './redis-adapter';
10
+ export { createPostgresRegistry, createPostgresRights, createRedisRegistry, createRedisRights, createSQLiteRegistry, createSQLiteRights } from './factories';
11
+ export type { CreatePostgresRegistryOptions, CreateRedisRightsOptions, CreateSQLiteRightsOptions } from './factories';
@@ -0,0 +1,12 @@
1
+ // Schema utilities
2
+ export { createTableNames, DEFAULT_TABLE_PREFIX, generatePostgresDropSchema, generatePostgresSchema, generateSQLiteDropSchema, generateSQLiteSchema } from './schema';
3
+ // Base adapter
4
+ export { BaseAdapter } from './base-adapter';
5
+ // SQLite adapter
6
+ export { SQLiteAdapter } from './sqlite-adapter';
7
+ // PostgreSQL adapter
8
+ export { PostgresAdapter } from './postgres-adapter';
9
+ // Redis adapter
10
+ export { RedisAdapter } from './redis-adapter';
11
+ // Factory functions
12
+ export { createPostgresRegistry, createPostgresRights, createRedisRegistry, createRedisRights, createSQLiteRegistry, createSQLiteRights } from './factories';
@@ -0,0 +1,51 @@
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 { BaseAdapter } from './base-adapter';
8
+ import type { BaseAdapterOptions, DatabaseAdapter } from './types';
9
+ export type PostgresAdapterOptions = BaseAdapterOptions & {
10
+ database?: string;
11
+ hostname?: string;
12
+ idleTimeout?: number;
13
+ max?: number;
14
+ password?: string;
15
+ port?: number;
16
+ ssl?: boolean | object;
17
+ url?: string;
18
+ username?: string;
19
+ };
20
+ export declare class PostgresAdapter extends BaseAdapter {
21
+ private sql;
22
+ private readonly options;
23
+ private transactionDepth;
24
+ constructor(options?: PostgresAdapterOptions);
25
+ connect(): Promise<void>;
26
+ disconnect(): Promise<void>;
27
+ migrate(): Promise<void>;
28
+ saveRight(right: Right): Promise<number>;
29
+ saveRights(rights: Rights): Promise<number[]>;
30
+ loadRight(id: number): Promise<Right | null>;
31
+ loadRights(): Promise<Rights>;
32
+ loadRightsByPath(pathPattern: string): Promise<Rights>;
33
+ deleteRight(id: number): Promise<boolean>;
34
+ saveRole(role: Role): Promise<number>;
35
+ loadRole(name: string): Promise<Role | null>;
36
+ loadRoles(): Promise<Role[]>;
37
+ deleteRole(name: string): Promise<boolean>;
38
+ saveRegistry(registry: RoleRegistry): Promise<void>;
39
+ loadRegistry(): Promise<RoleRegistry>;
40
+ saveSubject(identifier: string, subject: Subject): Promise<number>;
41
+ loadSubject(identifier: string): Promise<Subject | null>;
42
+ deleteSubject(identifier: string): Promise<boolean>;
43
+ protected getAllSubjectIdentifiers(): Promise<string[]>;
44
+ /**
45
+ * Optimized findSubjectsWithAccess using batch loading with JOINs.
46
+ * Reduces N+1 queries to a constant number of queries regardless of subject count.
47
+ */
48
+ findSubjectsWithAccess(pathPattern: string, flags: Flags): Promise<string[]>;
49
+ clear(): Promise<void>;
50
+ transaction<T>(fn: (adapter: DatabaseAdapter) => Promise<T>): Promise<T>;
51
+ }
@@ -0,0 +1,471 @@
1
+ import { SQL } from 'bun';
2
+ import { Flags } from '../constants';
3
+ import { Right } from '../right';
4
+ import { Rights } from '../rights';
5
+ import { Role } from '../role';
6
+ import { RoleRegistry } from '../role-registry';
7
+ import { Subject } from '../subject';
8
+ import { BaseAdapter } from './base-adapter';
9
+ import { generatePostgresSchema } from './schema';
10
+ export class PostgresAdapter extends BaseAdapter {
11
+ constructor(options = {}) {
12
+ super(options);
13
+ this.sql = null;
14
+ this.transactionDepth = 0;
15
+ this.options = options;
16
+ }
17
+ async connect() {
18
+ this.sql = this.options.url
19
+ ? new SQL({
20
+ idleTimeout: this.options.idleTimeout ?? 30,
21
+ max: this.options.max ?? 1,
22
+ ssl: this.options.ssl,
23
+ url: this.options.url
24
+ })
25
+ : new SQL({
26
+ database: this.options.database,
27
+ hostname: this.options.hostname ?? 'localhost',
28
+ idleTimeout: this.options.idleTimeout ?? 30,
29
+ max: this.options.max ?? 1,
30
+ password: this.options.password,
31
+ port: this.options.port ?? 5432,
32
+ ssl: this.options.ssl,
33
+ username: this.options.username
34
+ });
35
+ }
36
+ async disconnect() {
37
+ if (this.sql) {
38
+ await this.sql.end();
39
+ this.sql = null;
40
+ }
41
+ }
42
+ async migrate() {
43
+ if (!this.sql) {
44
+ throw new Error('Not connected');
45
+ }
46
+ await this.sql.unsafe(generatePostgresSchema(this.tables));
47
+ }
48
+ // ===========================================================================
49
+ // Rights Operations
50
+ // ===========================================================================
51
+ async saveRight(right) {
52
+ if (!this.sql) {
53
+ throw new Error('Not connected');
54
+ }
55
+ const row = this.rightToRow(right);
56
+ const { rights: rightsTable } = this.tables;
57
+ const [result] = await this.sql.unsafe(`
58
+ INSERT INTO ${rightsTable} (path, allow_mask, deny_mask, priority, description, tags, valid_from, valid_until)
59
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
60
+ ON CONFLICT (path, allow_mask, deny_mask, priority, valid_from, valid_until)
61
+ DO UPDATE SET updated_at = NOW()
62
+ RETURNING id
63
+ `, [
64
+ row.path,
65
+ row.allow_mask,
66
+ row.deny_mask,
67
+ row.priority,
68
+ row.description,
69
+ row.tags,
70
+ row.valid_from,
71
+ row.valid_until
72
+ ]);
73
+ right._setDbId(result.id);
74
+ return result.id;
75
+ }
76
+ async saveRights(rights) {
77
+ const ids = [];
78
+ await this.transaction(async () => {
79
+ for (const right of rights.allRights()) {
80
+ const id = await this.saveRight(right);
81
+ ids.push(id);
82
+ }
83
+ });
84
+ return ids;
85
+ }
86
+ async loadRight(id) {
87
+ if (!this.sql) {
88
+ throw new Error('Not connected');
89
+ }
90
+ const { rights: rightsTable } = this.tables;
91
+ const [row] = await this.sql.unsafe(`SELECT * FROM ${rightsTable} WHERE id = $1`, [id]);
92
+ if (!row) {
93
+ return null;
94
+ }
95
+ return this.rowToRight(row);
96
+ }
97
+ async loadRights() {
98
+ if (!this.sql) {
99
+ throw new Error('Not connected');
100
+ }
101
+ const { rights: rightsTable } = this.tables;
102
+ const rows = await this.sql.unsafe(`SELECT * FROM ${rightsTable} ORDER BY id`);
103
+ const loadedRights = new Rights();
104
+ for (const row of rows) {
105
+ loadedRights.add(this.rowToRight(row));
106
+ }
107
+ return loadedRights;
108
+ }
109
+ async loadRightsByPath(pathPattern) {
110
+ if (!this.sql) {
111
+ throw new Error('Not connected');
112
+ }
113
+ const { rights: rightsTable } = this.tables;
114
+ const pattern = pathPattern.replaceAll('*', '%').replaceAll('?', '_');
115
+ const rows = await this.sql.unsafe(`SELECT * FROM ${rightsTable} WHERE path LIKE $1 ORDER BY id`, [pattern]);
116
+ const loadedRights = new Rights();
117
+ for (const row of rows) {
118
+ loadedRights.add(this.rowToRight(row));
119
+ }
120
+ return loadedRights;
121
+ }
122
+ async deleteRight(id) {
123
+ if (!this.sql) {
124
+ throw new Error('Not connected');
125
+ }
126
+ const { rights: rightsTable } = this.tables;
127
+ const result = await this.sql.unsafe(`DELETE FROM ${rightsTable} WHERE id = $1`, [id]);
128
+ return result.count > 0;
129
+ }
130
+ // ===========================================================================
131
+ // Role Operations
132
+ // ===========================================================================
133
+ async saveRole(role) {
134
+ if (!this.sql) {
135
+ throw new Error('Not connected');
136
+ }
137
+ const { roleInheritance, roleRights, roles } = this.tables;
138
+ return this.transaction(async () => {
139
+ const [roleResult] = await this.sql.unsafe(`
140
+ INSERT INTO ${roles} (name)
141
+ VALUES ($1)
142
+ ON CONFLICT (name) DO UPDATE SET updated_at = NOW()
143
+ RETURNING id
144
+ `, [role.name]);
145
+ const roleId = roleResult.id;
146
+ await this.sql.unsafe(`DELETE FROM ${roleRights} WHERE role_id = $1`, [
147
+ roleId
148
+ ]);
149
+ await this.sql.unsafe(`DELETE FROM ${roleInheritance} WHERE child_role_id = $1`, [roleId]);
150
+ for (const right of role.rights.allRights()) {
151
+ const rightId = await this.saveRight(right);
152
+ await this.sql.unsafe(`INSERT INTO ${roleRights} (role_id, right_id) VALUES ($1, $2)`, [roleId, rightId]);
153
+ }
154
+ for (const parent of role.parents) {
155
+ const parentId = await this.saveRole(parent);
156
+ await this.sql.unsafe(`INSERT INTO ${roleInheritance} (child_role_id, parent_role_id) VALUES ($1, $2)`, [roleId, parentId]);
157
+ }
158
+ return roleId;
159
+ });
160
+ }
161
+ async loadRole(name) {
162
+ if (!this.sql) {
163
+ throw new Error('Not connected');
164
+ }
165
+ const { roleRights, roles } = this.tables;
166
+ const [roleRow] = await this.sql.unsafe(`SELECT * FROM ${roles} WHERE name = $1`, [name]);
167
+ if (!roleRow) {
168
+ return null;
169
+ }
170
+ const rights = new Rights();
171
+ const roleRightRows = await this.sql.unsafe(`SELECT right_id FROM ${roleRights} WHERE role_id = $1`, [roleRow.id]);
172
+ for (const rr of roleRightRows) {
173
+ const right = await this.loadRight(rr.right_id);
174
+ if (right) {
175
+ rights.add(right);
176
+ }
177
+ }
178
+ return new Role(name, rights);
179
+ }
180
+ async loadRoles() {
181
+ if (!this.sql) {
182
+ throw new Error('Not connected');
183
+ }
184
+ const { roles: rolesTable } = this.tables;
185
+ const roleRows = await this.sql.unsafe(`SELECT * FROM ${rolesTable} ORDER BY id`);
186
+ const loadedRoles = [];
187
+ for (const row of roleRows) {
188
+ const role = await this.loadRole(row.name);
189
+ if (role) {
190
+ loadedRoles.push(role);
191
+ }
192
+ }
193
+ return loadedRoles;
194
+ }
195
+ async deleteRole(name) {
196
+ if (!this.sql) {
197
+ throw new Error('Not connected');
198
+ }
199
+ const { roles } = this.tables;
200
+ const result = await this.sql.unsafe(`DELETE FROM ${roles} WHERE name = $1`, [name]);
201
+ return result.count > 0;
202
+ }
203
+ // ===========================================================================
204
+ // RoleRegistry Operations
205
+ // ===========================================================================
206
+ async saveRegistry(registry) {
207
+ if (!this.sql) {
208
+ throw new Error('Not connected');
209
+ }
210
+ await this.transaction(async () => {
211
+ const rolesToSave = new Map();
212
+ const collectRoles = (role) => {
213
+ if (!rolesToSave.has(role.name)) {
214
+ rolesToSave.set(role.name, role);
215
+ for (const parent of role.parents) {
216
+ collectRoles(parent);
217
+ }
218
+ }
219
+ };
220
+ registry.toJSON().forEach(roleJson => {
221
+ const role = registry.get(roleJson.name);
222
+ if (role) {
223
+ collectRoles(role);
224
+ }
225
+ });
226
+ for (const role of rolesToSave.values()) {
227
+ await this.saveRole(role);
228
+ }
229
+ });
230
+ }
231
+ async loadRegistry() {
232
+ const registry = new RoleRegistry();
233
+ const roles = await this.loadRoles();
234
+ const roleMap = new Map();
235
+ for (const role of roles) {
236
+ // Define the role in the registry and get the registered role back
237
+ const registeredRole = registry.define(role.name, role.rights);
238
+ const { roles: rolesTable } = this.tables;
239
+ const [roleRow] = await this.sql.unsafe(`SELECT id FROM ${rolesTable} WHERE name = $1`, [role.name]);
240
+ if (roleRow) {
241
+ // Store the registered role, not the original loaded role
242
+ roleMap.set(role.name, { id: roleRow.id, role: registeredRole });
243
+ }
244
+ }
245
+ const { roleInheritance } = this.tables;
246
+ const inheritRows = await this.sql.unsafe(`SELECT child_role_id, parent_role_id FROM ${roleInheritance}`);
247
+ for (const ir of inheritRows) {
248
+ const inheritanceRow = ir;
249
+ let childRole;
250
+ let parentRole;
251
+ for (const [, data] of roleMap.entries()) {
252
+ if (data.id === inheritanceRow.child_role_id) {
253
+ childRole = data.role;
254
+ }
255
+ if (data.id === inheritanceRow.parent_role_id) {
256
+ parentRole = data.role;
257
+ }
258
+ }
259
+ if (childRole && parentRole) {
260
+ childRole.inheritsFrom(parentRole);
261
+ }
262
+ }
263
+ return registry;
264
+ }
265
+ // ===========================================================================
266
+ // Subject Operations
267
+ // ===========================================================================
268
+ async saveSubject(identifier, subject) {
269
+ if (!this.sql) {
270
+ throw new Error('Not connected');
271
+ }
272
+ const { subjectRights, subjectRoles, subjects } = this.tables;
273
+ return this.transaction(async () => {
274
+ const [subjectResult] = await this.sql.unsafe(`
275
+ INSERT INTO ${subjects} (identifier)
276
+ VALUES ($1)
277
+ ON CONFLICT (identifier) DO UPDATE SET updated_at = NOW()
278
+ RETURNING id
279
+ `, [identifier]);
280
+ const subjectId = subjectResult.id;
281
+ await this.sql.unsafe(`DELETE FROM ${subjectRoles} WHERE subject_id = $1`, [subjectId]);
282
+ await this.sql.unsafe(`DELETE FROM ${subjectRights} WHERE subject_id = $1`, [subjectId]);
283
+ for (const role of subject.roles) {
284
+ const roleId = await this.saveRole(role);
285
+ await this.sql.unsafe(`INSERT INTO ${subjectRoles} (subject_id, role_id) VALUES ($1, $2)`, [subjectId, roleId]);
286
+ }
287
+ for (const right of subject.rights.allRights()) {
288
+ const rightId = await this.saveRight(right);
289
+ await this.sql.unsafe(`INSERT INTO ${subjectRights} (subject_id, right_id) VALUES ($1, $2)`, [subjectId, rightId]);
290
+ }
291
+ return subjectId;
292
+ });
293
+ }
294
+ async loadSubject(identifier) {
295
+ if (!this.sql) {
296
+ throw new Error('Not connected');
297
+ }
298
+ const { roles, subjectRights, subjectRoles, subjects } = this.tables;
299
+ const [subjectRow] = await this.sql.unsafe(`SELECT * FROM ${subjects} WHERE identifier = $1`, [identifier]);
300
+ if (!subjectRow) {
301
+ return null;
302
+ }
303
+ const subject = new Subject();
304
+ const subjectRoleRows = await this.sql.unsafe(`SELECT role_id FROM ${subjectRoles} WHERE subject_id = $1`, [subjectRow.id]);
305
+ for (const sr of subjectRoleRows) {
306
+ const subjectRoleRow = sr;
307
+ const [roleName] = await this.sql.unsafe(`SELECT name FROM ${roles} WHERE id = $1`, [subjectRoleRow.role_id]);
308
+ if (roleName) {
309
+ const role = await this.loadRole(roleName.name);
310
+ if (role) {
311
+ subject.memberOf(role);
312
+ }
313
+ }
314
+ }
315
+ const subjectRightRows = await this.sql.unsafe(`SELECT right_id FROM ${subjectRights} WHERE subject_id = $1`, [subjectRow.id]);
316
+ for (const sr of subjectRightRows) {
317
+ const subjectRightRow = sr;
318
+ const right = await this.loadRight(subjectRightRow.right_id);
319
+ if (right) {
320
+ subject.rights.add(right);
321
+ }
322
+ }
323
+ return subject;
324
+ }
325
+ async deleteSubject(identifier) {
326
+ if (!this.sql) {
327
+ throw new Error('Not connected');
328
+ }
329
+ const { subjects } = this.tables;
330
+ const result = await this.sql.unsafe(`DELETE FROM ${subjects} WHERE identifier = $1`, [identifier]);
331
+ return result.count > 0;
332
+ }
333
+ async getAllSubjectIdentifiers() {
334
+ if (!this.sql) {
335
+ throw new Error('Not connected');
336
+ }
337
+ const { subjects } = this.tables;
338
+ const rows = (await this.sql.unsafe(`SELECT identifier FROM ${subjects}`));
339
+ return rows.map(row => row.identifier);
340
+ }
341
+ /**
342
+ * Optimized findSubjectsWithAccess using batch loading with JOINs.
343
+ * Reduces N+1 queries to a constant number of queries regardless of subject count.
344
+ */
345
+ async findSubjectsWithAccess(pathPattern, flags) {
346
+ if (!this.sql) {
347
+ throw new Error('Not connected');
348
+ }
349
+ const { rights, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
350
+ // Load all subjects with their data in batch queries
351
+ const subjectRows = await this.sql.unsafe(`SELECT id, identifier FROM ${subjects}`);
352
+ if (subjectRows.length === 0) {
353
+ return [];
354
+ }
355
+ // Create a map of subject id -> identifier for quick lookup
356
+ const subjectIdToIdentifier = new Map();
357
+ for (const row of subjectRows) {
358
+ const { id, identifier } = row;
359
+ subjectIdToIdentifier.set(id, identifier);
360
+ }
361
+ // Batch load all subject-role mappings with role names
362
+ const subjectRoleRows = await this.sql.unsafe(`SELECT sr.subject_id, r.name as role_name
363
+ FROM ${subjectRoles} sr
364
+ JOIN ${roles} r ON sr.role_id = r.id`);
365
+ // Batch load all subject direct rights
366
+ const subjectRightRows = await this.sql.unsafe(`SELECT sr.subject_id, rt.*
367
+ FROM ${subjectRights} sr
368
+ JOIN ${rights} rt ON sr.right_id = rt.id`);
369
+ // Batch load all role rights
370
+ const roleRightRows = await this.sql.unsafe(`SELECT r.name as role_name, rt.*
371
+ FROM ${roleRights} rr
372
+ JOIN ${roles} r ON rr.role_id = r.id
373
+ JOIN ${rights} rt ON rr.right_id = rt.id`);
374
+ // Build role -> rights mapping
375
+ const roleRightsMap = new Map();
376
+ for (const row of roleRightRows) {
377
+ const { role_name, ...rightData } = row;
378
+ if (!roleRightsMap.has(role_name)) {
379
+ roleRightsMap.set(role_name, new Rights());
380
+ }
381
+ roleRightsMap
382
+ .get(role_name)
383
+ .add(this.rowToRight(rightData));
384
+ }
385
+ // Build subject -> roles mapping
386
+ const subjectRolesMap = new Map();
387
+ for (const row of subjectRoleRows) {
388
+ const { role_name, subject_id } = row;
389
+ if (!subjectRolesMap.has(subject_id)) {
390
+ subjectRolesMap.set(subject_id, []);
391
+ }
392
+ subjectRolesMap.get(subject_id).push(role_name);
393
+ }
394
+ // Build subject -> direct rights mapping
395
+ const subjectDirectRightsMap = new Map();
396
+ for (const row of subjectRightRows) {
397
+ const { subject_id, ...rightData } = row;
398
+ if (!subjectDirectRightsMap.has(subject_id)) {
399
+ subjectDirectRightsMap.set(subject_id, new Rights());
400
+ }
401
+ subjectDirectRightsMap
402
+ .get(subject_id)
403
+ .add(this.rowToRight(rightData));
404
+ }
405
+ // Now construct Subject objects and check access
406
+ const matchingSubjects = [];
407
+ for (const [subjectId, identifier] of subjectIdToIdentifier) {
408
+ const subject = new Subject();
409
+ // Add roles with their rights
410
+ const roleNames = subjectRolesMap.get(subjectId) ?? [];
411
+ for (const roleName of roleNames) {
412
+ const roleRights = roleRightsMap.get(roleName) ?? new Rights();
413
+ const role = new Role(roleName, roleRights);
414
+ subject.memberOf(role);
415
+ }
416
+ // Add direct rights
417
+ const directRights = subjectDirectRightsMap.get(subjectId);
418
+ if (directRights) {
419
+ for (const right of directRights.allRights()) {
420
+ subject.rights.add(right);
421
+ }
422
+ }
423
+ // Check if subject has the requested access
424
+ if (subject.has(pathPattern, flags)) {
425
+ matchingSubjects.push(identifier);
426
+ }
427
+ }
428
+ return matchingSubjects;
429
+ }
430
+ // ===========================================================================
431
+ // Utility
432
+ // ===========================================================================
433
+ async clear() {
434
+ if (!this.sql) {
435
+ throw new Error('Not connected');
436
+ }
437
+ const { rights, roleInheritance, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
438
+ await this.sql.unsafe(`DELETE FROM ${subjectRights}`);
439
+ await this.sql.unsafe(`DELETE FROM ${subjectRoles}`);
440
+ await this.sql.unsafe(`DELETE FROM ${subjects}`);
441
+ await this.sql.unsafe(`DELETE FROM ${roleInheritance}`);
442
+ await this.sql.unsafe(`DELETE FROM ${roleRights}`);
443
+ await this.sql.unsafe(`DELETE FROM ${roles}`);
444
+ await this.sql.unsafe(`DELETE FROM ${rights}`);
445
+ }
446
+ async transaction(fn) {
447
+ if (!this.sql) {
448
+ throw new Error('Not connected');
449
+ }
450
+ const isNested = this.transactionDepth > 0;
451
+ if (!isNested) {
452
+ await this.sql.unsafe('BEGIN');
453
+ }
454
+ this.transactionDepth++;
455
+ try {
456
+ const result = await fn(this);
457
+ this.transactionDepth--;
458
+ if (!isNested) {
459
+ await this.sql.unsafe('COMMIT');
460
+ }
461
+ return result;
462
+ }
463
+ catch (error) {
464
+ this.transactionDepth--;
465
+ if (!isNested) {
466
+ await this.sql.unsafe('ROLLBACK');
467
+ }
468
+ throw error;
469
+ }
470
+ }
471
+ }
@@ -0,0 +1,84 @@
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 { BaseAdapter } from './base-adapter';
8
+ import type { BaseAdapterOptions, DatabaseAdapter } from './types';
9
+ export type RedisAdapterOptions = BaseAdapterOptions & {
10
+ db?: number;
11
+ host?: string;
12
+ keyPrefix?: string;
13
+ lazyConnect?: boolean;
14
+ password?: string;
15
+ port?: number;
16
+ tls?: object;
17
+ url?: string;
18
+ };
19
+ /**
20
+ * Redis key structure:
21
+ *
22
+ * Rights:
23
+ * {prefix}rights:{id} → Hash { path, allow_mask, deny_mask, description, tags, valid_from, valid_until, created_at, updated_at }
24
+ * {prefix}rights:_seq → String (auto-increment counter)
25
+ * {prefix}rights:_all → Set of all right IDs
26
+ * {prefix}rights:_unique:{hash} → String (right ID for unique constraint lookup)
27
+ *
28
+ * Roles:
29
+ * {prefix}roles:{name} → Hash { name, created_at, updated_at }
30
+ * {prefix}roles:_all → Set of all role names
31
+ * {prefix}roles:{name}:rights → Set of right IDs
32
+ * {prefix}roles:{name}:parents → Set of parent role names
33
+ *
34
+ * Subjects:
35
+ * {prefix}subjects:{identifier} → Hash { identifier, id, created_at, updated_at }
36
+ * {prefix}subjects:_all → Set of all subject identifiers
37
+ * {prefix}subjects:_seq → String (auto-increment counter for subject IDs)
38
+ * {prefix}subjects:{identifier}:roles → Set of role names
39
+ * {prefix}subjects:{identifier}:rights → Set of right IDs
40
+ */
41
+ export declare class RedisAdapter extends BaseAdapter {
42
+ private redis;
43
+ private readonly options;
44
+ private transactionDepth;
45
+ constructor(options?: RedisAdapterOptions);
46
+ /**
47
+ * Generate a Redis key with the configured prefix
48
+ */
49
+ private key;
50
+ /**
51
+ * Generate a unique hash for a right's unique constraint fields
52
+ */
53
+ private rightUniqueHash;
54
+ connect(): Promise<void>;
55
+ disconnect(): Promise<void>;
56
+ migrate(): Promise<void>;
57
+ saveRight(right: Right): Promise<number>;
58
+ saveRights(rights: Rights): Promise<number[]>;
59
+ loadRight(id: number): Promise<Right | null>;
60
+ loadRights(): Promise<Rights>;
61
+ loadRightsByPath(pathPattern: string): Promise<Rights>;
62
+ deleteRight(id: number): Promise<boolean>;
63
+ saveRole(role: Role): Promise<number>;
64
+ loadRole(name: string): Promise<Role | null>;
65
+ loadRoles(): Promise<Role[]>;
66
+ deleteRole(name: string): Promise<boolean>;
67
+ saveRegistry(registry: RoleRegistry): Promise<void>;
68
+ loadRegistry(): Promise<RoleRegistry>;
69
+ saveSubject(identifier: string, subject: Subject): Promise<number>;
70
+ loadSubject(identifier: string): Promise<Subject | null>;
71
+ deleteSubject(identifier: string): Promise<boolean>;
72
+ protected getAllSubjectIdentifiers(): Promise<string[]>;
73
+ /**
74
+ * Optimized findSubjectsWithAccess using batch loading with Redis pipeline.
75
+ * Reduces N+1 queries to a constant number of Redis operations regardless of subject count.
76
+ */
77
+ findSubjectsWithAccess(pathPattern: string, flags: Flags): Promise<string[]>;
78
+ clear(): Promise<void>;
79
+ transaction<T>(fn: (adapter: DatabaseAdapter) => Promise<T>): Promise<T>;
80
+ /**
81
+ * Convert Redis hash data to a RightsRow
82
+ */
83
+ private hashToRightsRow;
84
+ }