odgn-rights 0.2.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.
@@ -0,0 +1,655 @@
1
+ import { Database } from 'bun:sqlite';
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 { generateSQLiteSchema } from './schema';
10
+ export class SQLiteAdapter extends BaseAdapter {
11
+ constructor(options = {}) {
12
+ super(options);
13
+ this.db = null;
14
+ this.roleIdCache = new Map();
15
+ this.transactionDepth = 0;
16
+ this.stmtInsertRight = null;
17
+ this.stmtSelectRightById = null;
18
+ this.stmtSelectAllRights = null;
19
+ this.stmtDeleteRight = null;
20
+ this.stmtInsertRole = null;
21
+ this.stmtSelectRoleByName = null;
22
+ this.stmtSelectAllRoles = null;
23
+ this.stmtSelectRoleById = null;
24
+ this.stmtDeleteRole = null;
25
+ this.stmtInsertRoleRight = null;
26
+ this.stmtDeleteRoleRights = null;
27
+ this.stmtSelectRoleRights = null;
28
+ this.stmtInsertRoleInheritance = null;
29
+ this.stmtDeleteRoleInheritance = null;
30
+ this.stmtDeleteChildInheritance = null;
31
+ this.stmtSelectRoleInheritance = null;
32
+ this.stmtInsertSubject = null;
33
+ this.stmtSelectSubjectByIdentifier = null;
34
+ this.stmtUpdateSubject = null;
35
+ this.stmtDeleteSubject = null;
36
+ this.stmtInsertSubjectRole = null;
37
+ this.stmtDeleteSubjectRoles = null;
38
+ this.stmtSelectSubjectRoles = null;
39
+ this.stmtInsertSubjectRight = null;
40
+ this.stmtDeleteSubjectRights = null;
41
+ this.stmtSelectSubjectRights = null;
42
+ // ===========================================================================
43
+ // Prepared Statements
44
+ // ===========================================================================
45
+ this.prepareStatements = () => {
46
+ if (!this.db) {
47
+ return;
48
+ }
49
+ const { rights, roleInheritance, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
50
+ this.stmtInsertRight = this.db.prepare(`
51
+ INSERT INTO ${rights} (path, allow_mask, deny_mask, priority, description, tags, valid_from, valid_until)
52
+ VALUES ($path, $allow_mask, $deny_mask, $priority, $description, $tags, $valid_from, $valid_until)
53
+ `);
54
+ this.stmtSelectRightById = this.db.prepare(`
55
+ SELECT * FROM ${rights} WHERE id = $id
56
+ `);
57
+ this.stmtSelectAllRights = this.db.prepare(`
58
+ SELECT * FROM ${rights} ORDER BY id
59
+ `);
60
+ this.stmtDeleteRight = this.db.prepare(`
61
+ DELETE FROM ${rights} WHERE id = $id
62
+ `);
63
+ this.stmtInsertRole = this.db.prepare(`
64
+ INSERT INTO ${roles} (name) VALUES ($name)
65
+ `);
66
+ this.stmtSelectRoleByName = this.db.prepare(`
67
+ SELECT * FROM ${roles} WHERE name = $name
68
+ `);
69
+ this.stmtSelectAllRoles = this.db.prepare(`
70
+ SELECT * FROM ${roles} ORDER BY id
71
+ `);
72
+ this.stmtSelectRoleById = this.db.prepare(`
73
+ SELECT * FROM ${roles} WHERE id = $id
74
+ `);
75
+ this.stmtDeleteRole = this.db.prepare(`
76
+ DELETE FROM ${roles} WHERE name = $name
77
+ `);
78
+ this.stmtInsertRoleRight = this.db.prepare(`
79
+ INSERT INTO ${roleRights} (role_id, right_id) VALUES ($role_id, $right_id)
80
+ `);
81
+ this.stmtDeleteRoleRights = this.db.prepare(`
82
+ DELETE FROM ${roleRights} WHERE role_id = $role_id
83
+ `);
84
+ this.stmtSelectRoleRights = this.db.prepare(`
85
+ SELECT right_id FROM ${roleRights} WHERE role_id = $role_id
86
+ `);
87
+ this.stmtInsertRoleInheritance = this.db.prepare(`
88
+ INSERT INTO ${roleInheritance} (child_role_id, parent_role_id) VALUES ($child_role_id, $parent_role_id)
89
+ `);
90
+ this.stmtDeleteRoleInheritance = this.db.prepare(`
91
+ DELETE FROM ${roleInheritance} WHERE child_role_id = $child_role_id AND parent_role_id = $parent_role_id
92
+ `);
93
+ this.stmtDeleteChildInheritance = this.db.prepare(`
94
+ DELETE FROM ${roleInheritance} WHERE child_role_id = $child_role_id
95
+ `);
96
+ this.stmtSelectRoleInheritance = this.db.prepare(`
97
+ SELECT child_role_id, parent_role_id FROM ${roleInheritance}
98
+ `);
99
+ this.stmtInsertSubject = this.db.prepare(`
100
+ INSERT INTO ${subjects} (identifier) VALUES ($identifier)
101
+ `);
102
+ this.stmtSelectSubjectByIdentifier = this.db.prepare(`
103
+ SELECT * FROM ${subjects} WHERE identifier = $identifier
104
+ `);
105
+ this.stmtUpdateSubject = this.db.prepare(`
106
+ UPDATE ${subjects} SET updated_at = datetime('now') WHERE id = $id
107
+ `);
108
+ this.stmtDeleteSubject = this.db.prepare(`
109
+ DELETE FROM ${subjects} WHERE identifier = $identifier
110
+ `);
111
+ this.stmtInsertSubjectRole = this.db.prepare(`
112
+ INSERT INTO ${subjectRoles} (subject_id, role_id) VALUES ($subject_id, $role_id)
113
+ `);
114
+ this.stmtDeleteSubjectRoles = this.db.prepare(`
115
+ DELETE FROM ${subjectRoles} WHERE subject_id = $subject_id
116
+ `);
117
+ this.stmtSelectSubjectRoles = this.db.prepare(`
118
+ SELECT role_id FROM ${subjectRoles} WHERE subject_id = $subject_id
119
+ `);
120
+ this.stmtInsertSubjectRight = this.db.prepare(`
121
+ INSERT INTO ${subjectRights} (subject_id, right_id) VALUES ($subject_id, $right_id)
122
+ `);
123
+ this.stmtDeleteSubjectRights = this.db.prepare(`
124
+ DELETE FROM ${subjectRights} WHERE subject_id = $subject_id
125
+ `);
126
+ this.stmtSelectSubjectRights = this.db.prepare(`
127
+ SELECT right_id FROM ${subjectRights} WHERE subject_id = $subject_id
128
+ `);
129
+ };
130
+ this.finalizeStatements = () => {
131
+ this.stmtInsertRight?.finalize();
132
+ this.stmtSelectRightById?.finalize();
133
+ this.stmtSelectAllRights?.finalize();
134
+ this.stmtDeleteRight?.finalize();
135
+ this.stmtInsertRole?.finalize();
136
+ this.stmtSelectRoleByName?.finalize();
137
+ this.stmtSelectAllRoles?.finalize();
138
+ this.stmtSelectRoleById?.finalize();
139
+ this.stmtDeleteRole?.finalize();
140
+ this.stmtInsertRoleRight?.finalize();
141
+ this.stmtDeleteRoleRights?.finalize();
142
+ this.stmtSelectRoleRights?.finalize();
143
+ this.stmtInsertRoleInheritance?.finalize();
144
+ this.stmtDeleteRoleInheritance?.finalize();
145
+ this.stmtDeleteChildInheritance?.finalize();
146
+ this.stmtSelectRoleInheritance?.finalize();
147
+ this.stmtInsertSubject?.finalize();
148
+ this.stmtSelectSubjectByIdentifier?.finalize();
149
+ this.stmtUpdateSubject?.finalize();
150
+ this.stmtDeleteSubject?.finalize();
151
+ this.stmtInsertSubjectRole?.finalize();
152
+ this.stmtDeleteSubjectRoles?.finalize();
153
+ this.stmtSelectSubjectRoles?.finalize();
154
+ this.stmtInsertSubjectRight?.finalize();
155
+ this.stmtDeleteSubjectRights?.finalize();
156
+ this.stmtSelectSubjectRights?.finalize();
157
+ };
158
+ this.options = {
159
+ create: true,
160
+ enableWAL: false,
161
+ filename: ':memory:',
162
+ ...options
163
+ };
164
+ }
165
+ async connect() {
166
+ this.db = new Database(this.options.filename, {
167
+ create: this.options.create,
168
+ readonly: this.options.readonly,
169
+ strict: this.options.strict
170
+ });
171
+ if (this.options.enableWAL) {
172
+ this.db.run('PRAGMA journal_mode = WAL');
173
+ }
174
+ }
175
+ prepareStatementsAfterMigration() {
176
+ this.prepareStatements();
177
+ }
178
+ async disconnect() {
179
+ this.finalizeStatements();
180
+ this.db?.close();
181
+ this.db = null;
182
+ this.roleIdCache.clear();
183
+ }
184
+ async migrate() {
185
+ if (!this.db) {
186
+ throw new Error('Not connected');
187
+ }
188
+ this.db.run(generateSQLiteSchema(this.tables));
189
+ }
190
+ async enableWAL() {
191
+ if (!this.db) {
192
+ throw new Error('Not connected');
193
+ }
194
+ this.db.run('PRAGMA journal_mode = WAL');
195
+ }
196
+ // ===========================================================================
197
+ // Rights Operations
198
+ // ===========================================================================
199
+ async saveRight(right) {
200
+ if (!this.db || !this.stmtInsertRight) {
201
+ throw new Error('Not connected');
202
+ }
203
+ const row = this.rightToRow(right);
204
+ this.stmtInsertRight.run({
205
+ $allow_mask: row.allow_mask,
206
+ $deny_mask: row.deny_mask,
207
+ $description: row.description,
208
+ $path: row.path,
209
+ $priority: row.priority,
210
+ $tags: row.tags,
211
+ $valid_from: row.valid_from,
212
+ $valid_until: row.valid_until
213
+ });
214
+ const result = this.db.query('SELECT last_insert_rowid() as id').get();
215
+ right._setDbId(result.id);
216
+ return result.id;
217
+ }
218
+ async saveRights(rights) {
219
+ const ids = [];
220
+ await this.transaction(async () => {
221
+ for (const right of rights.allRights()) {
222
+ const id = await this.saveRight(right);
223
+ ids.push(id);
224
+ }
225
+ });
226
+ return ids;
227
+ }
228
+ async loadRight(id) {
229
+ if (!this.db || !this.stmtSelectRightById) {
230
+ throw new Error('Not connected');
231
+ }
232
+ const row = this.stmtSelectRightById.get({ $id: id });
233
+ if (!row) {
234
+ return null;
235
+ }
236
+ return this.rowToRight(row);
237
+ }
238
+ async loadRights() {
239
+ if (!this.db || !this.stmtSelectAllRights) {
240
+ throw new Error('Not connected');
241
+ }
242
+ const rows = this.stmtSelectAllRights.all();
243
+ const loadedRights = new Rights();
244
+ for (const row of rows) {
245
+ loadedRights.add(this.rowToRight(row));
246
+ }
247
+ return loadedRights;
248
+ }
249
+ async loadRightsByPath(pathPattern) {
250
+ if (!this.db) {
251
+ throw new Error('Not connected');
252
+ }
253
+ const { rights: rightsTable } = this.tables;
254
+ const pattern = pathPattern
255
+ .replaceAll('%', String.raw `\%`)
256
+ .replaceAll('_', String.raw `\_`);
257
+ const sqlPattern = pattern.replaceAll('*', '%');
258
+ const stmt = this.db.prepare(String.raw `SELECT * FROM ${rightsTable} WHERE path LIKE $pattern ESCAPE '\' ORDER BY id`);
259
+ const rows = stmt.all({ $pattern: sqlPattern });
260
+ const loadedRights = new Rights();
261
+ for (const row of rows) {
262
+ loadedRights.add(this.rowToRight(row));
263
+ }
264
+ return loadedRights;
265
+ }
266
+ async deleteRight(id) {
267
+ if (!this.db || !this.stmtDeleteRight) {
268
+ throw new Error('Not connected');
269
+ }
270
+ const result = this.stmtDeleteRight.run({ $id: id });
271
+ return result.changes > 0;
272
+ }
273
+ // ===========================================================================
274
+ // Role Operations
275
+ // ===========================================================================
276
+ async saveRole(role) {
277
+ if (!this.db) {
278
+ throw new Error('Not connected');
279
+ }
280
+ const { roles } = this.tables;
281
+ return this.transaction(async () => {
282
+ let roleId;
283
+ const existing = this.db.prepare(`SELECT id FROM ${roles} WHERE name = $name`).get({ $name: role.name });
284
+ if (existing) {
285
+ roleId = existing.id;
286
+ this.db.prepare(`UPDATE ${roles} SET updated_at = datetime('now') WHERE id = $id`).run({ $id: roleId });
287
+ }
288
+ else {
289
+ this.db.prepare(`INSERT INTO ${roles} (name) VALUES ($name)`).run({
290
+ $name: role.name
291
+ });
292
+ const result = this.db.query('SELECT last_insert_rowid() as id').get();
293
+ roleId = result.id;
294
+ }
295
+ this.roleIdCache.set(role.name, roleId);
296
+ this.stmtDeleteRoleRights?.run({ $role_id: roleId });
297
+ this.stmtDeleteChildInheritance?.run({ $child_role_id: roleId });
298
+ for (const right of role.rights.allRights()) {
299
+ const rightId = await this.saveRight(right);
300
+ this.stmtInsertRoleRight?.run({
301
+ $right_id: rightId,
302
+ $role_id: roleId
303
+ });
304
+ }
305
+ for (const parent of role.parents) {
306
+ const parentId = await this.saveRole(parent);
307
+ this.stmtInsertRoleInheritance?.run({
308
+ $child_role_id: roleId,
309
+ $parent_role_id: parentId
310
+ });
311
+ }
312
+ return roleId;
313
+ });
314
+ }
315
+ async loadRole(name) {
316
+ if (!this.db || !this.stmtSelectRoleByName) {
317
+ throw new Error('Not connected');
318
+ }
319
+ const roleRow = this.stmtSelectRoleByName.get({
320
+ $name: name
321
+ });
322
+ if (!roleRow) {
323
+ return null;
324
+ }
325
+ const rights = new Rights();
326
+ const rightRows = this.stmtSelectRoleRights?.all({
327
+ $role_id: roleRow.id
328
+ });
329
+ if (rightRows) {
330
+ for (const rr of rightRows) {
331
+ const right = await this.loadRight(rr.right_id);
332
+ if (right) {
333
+ rights.add(right);
334
+ }
335
+ }
336
+ }
337
+ return new Role(name, rights);
338
+ }
339
+ async loadRoles() {
340
+ if (!this.db || !this.stmtSelectAllRoles) {
341
+ throw new Error('Not connected');
342
+ }
343
+ const roleRows = this.stmtSelectAllRoles.all();
344
+ const roles = [];
345
+ for (const roleRow of roleRows) {
346
+ const role = await this.loadRole(roleRow.name);
347
+ if (role) {
348
+ roles.push(role);
349
+ }
350
+ }
351
+ return roles;
352
+ }
353
+ async deleteRole(name) {
354
+ if (!this.db || !this.stmtDeleteRole) {
355
+ throw new Error('Not connected');
356
+ }
357
+ const roleRow = this.stmtSelectRoleByName?.get({ $name: name });
358
+ if (roleRow) {
359
+ this.roleIdCache.delete(name);
360
+ }
361
+ const result = this.stmtDeleteRole.run({ $name: name });
362
+ return result.changes > 0;
363
+ }
364
+ // ===========================================================================
365
+ // RoleRegistry Operations
366
+ // ===========================================================================
367
+ async saveRegistry(registry) {
368
+ if (!this.db) {
369
+ throw new Error('Not connected');
370
+ }
371
+ await this.transaction(async () => {
372
+ const rolesToSave = new Map();
373
+ const collectRoles = (role) => {
374
+ if (!rolesToSave.has(role.name)) {
375
+ rolesToSave.set(role.name, role);
376
+ for (const parent of role.parents) {
377
+ collectRoles(parent);
378
+ }
379
+ }
380
+ };
381
+ registry.toJSON().forEach(roleJson => {
382
+ const role = registry.get(roleJson.name);
383
+ if (role) {
384
+ collectRoles(role);
385
+ }
386
+ });
387
+ for (const role of rolesToSave.values()) {
388
+ await this.saveRole(role);
389
+ }
390
+ });
391
+ }
392
+ async loadRegistry() {
393
+ const registry = new RoleRegistry();
394
+ const roles = await this.loadRoles();
395
+ const roleMap = new Map();
396
+ for (const role of roles) {
397
+ const registryRole = registry.define(role.name, role.rights);
398
+ const roleRow = this.stmtSelectRoleByName?.get({
399
+ $name: role.name
400
+ });
401
+ if (roleRow) {
402
+ roleMap.set(role.name, { id: roleRow.id, role: registryRole });
403
+ this.roleIdCache.set(role.name, roleRow.id);
404
+ }
405
+ }
406
+ const { roleInheritance } = this.tables;
407
+ const inheritRows = this.db
408
+ ?.query(`SELECT child_role_id, parent_role_id FROM ${roleInheritance}`)
409
+ .all();
410
+ if (inheritRows) {
411
+ for (const ir of inheritRows) {
412
+ let childRole;
413
+ let parentRole;
414
+ for (const [, data] of roleMap.entries()) {
415
+ if (data.id === ir.child_role_id) {
416
+ childRole = data.role;
417
+ }
418
+ if (data.id === ir.parent_role_id) {
419
+ parentRole = data.role;
420
+ }
421
+ }
422
+ if (childRole && parentRole) {
423
+ childRole.inheritsFrom(parentRole);
424
+ }
425
+ }
426
+ }
427
+ return registry;
428
+ }
429
+ // ===========================================================================
430
+ // Subject Operations
431
+ // ===========================================================================
432
+ async saveSubject(identifier, subject) {
433
+ if (!this.db) {
434
+ throw new Error('Not connected');
435
+ }
436
+ const { subjects } = this.tables;
437
+ return this.transaction(async () => {
438
+ let subjectId;
439
+ const existing = this.db.prepare(`SELECT id FROM ${subjects} WHERE identifier = $identifier`).get({ $identifier: identifier });
440
+ if (existing) {
441
+ subjectId = existing.id;
442
+ this.db.prepare(`UPDATE ${subjects} SET updated_at = datetime('now') WHERE id = $id`).run({ $id: subjectId });
443
+ }
444
+ else {
445
+ this.db.prepare(`INSERT INTO ${subjects} (identifier) VALUES ($identifier)`).run({ $identifier: identifier });
446
+ const result = this.db.query('SELECT last_insert_rowid() as id').get();
447
+ subjectId = result.id;
448
+ }
449
+ this.stmtDeleteSubjectRoles?.run({ $subject_id: subjectId });
450
+ this.stmtDeleteSubjectRights?.run({ $subject_id: subjectId });
451
+ for (const role of subject.roles) {
452
+ const roleId = await this.saveRole(role);
453
+ this.stmtInsertSubjectRole?.run({
454
+ $role_id: roleId,
455
+ $subject_id: subjectId
456
+ });
457
+ }
458
+ for (const right of subject.rights.allRights()) {
459
+ const rightId = await this.saveRight(right);
460
+ this.stmtInsertSubjectRight?.run({
461
+ $right_id: rightId,
462
+ $subject_id: subjectId
463
+ });
464
+ }
465
+ return subjectId;
466
+ });
467
+ }
468
+ async loadSubject(identifier) {
469
+ if (!this.db || !this.stmtSelectSubjectByIdentifier) {
470
+ throw new Error('Not connected');
471
+ }
472
+ const subjectRow = this.stmtSelectSubjectByIdentifier.get({
473
+ $identifier: identifier
474
+ });
475
+ if (!subjectRow) {
476
+ return null;
477
+ }
478
+ const subject = new Subject();
479
+ const roleRows = this.stmtSelectSubjectRoles?.all({
480
+ $subject_id: subjectRow.id
481
+ });
482
+ if (roleRows) {
483
+ for (const sr of roleRows) {
484
+ const roleName = this.db.prepare(`SELECT name FROM ${this.tables.roles} WHERE id = $id`).get({ $id: sr.role_id });
485
+ if (roleName) {
486
+ const role = await this.loadRole(roleName.name);
487
+ if (role) {
488
+ subject.memberOf(role);
489
+ }
490
+ }
491
+ }
492
+ }
493
+ const rightRows = this.stmtSelectSubjectRights?.all({
494
+ $subject_id: subjectRow.id
495
+ });
496
+ if (rightRows) {
497
+ for (const sr of rightRows) {
498
+ const right = await this.loadRight(sr.right_id);
499
+ if (right) {
500
+ subject.rights.add(right);
501
+ }
502
+ }
503
+ }
504
+ return subject;
505
+ }
506
+ async deleteSubject(identifier) {
507
+ if (!this.db || !this.stmtDeleteSubject) {
508
+ throw new Error('Not connected');
509
+ }
510
+ const result = this.stmtDeleteSubject.run({ $identifier: identifier });
511
+ return result.changes > 0;
512
+ }
513
+ async getAllSubjectIdentifiers() {
514
+ if (!this.db) {
515
+ throw new Error('Not connected');
516
+ }
517
+ const rows = this.db
518
+ .prepare(`SELECT identifier FROM ${this.tables.subjects}`)
519
+ .all();
520
+ return rows.map(row => row.identifier);
521
+ }
522
+ /**
523
+ * Optimized findSubjectsWithAccess using batch loading with JOINs.
524
+ * Reduces N+1 queries to a constant number of queries regardless of subject count.
525
+ */
526
+ async findSubjectsWithAccess(pathPattern, flags) {
527
+ if (!this.db) {
528
+ throw new Error('Not connected');
529
+ }
530
+ const { rights, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
531
+ // Load all subjects with their data in batch queries
532
+ const subjectRows = this.db
533
+ .prepare(`SELECT id, identifier FROM ${subjects}`)
534
+ .all();
535
+ if (subjectRows.length === 0) {
536
+ return [];
537
+ }
538
+ // Create a map of subject id -> identifier for quick lookup
539
+ const subjectIdToIdentifier = new Map();
540
+ const subjectIdentifierToId = new Map();
541
+ for (const row of subjectRows) {
542
+ subjectIdToIdentifier.set(row.id, row.identifier);
543
+ subjectIdentifierToId.set(row.identifier, row.id);
544
+ }
545
+ // Batch load all subject-role mappings with role names
546
+ const subjectRoleRows = this.db
547
+ .prepare(`SELECT sr.subject_id, r.name as role_name
548
+ FROM ${subjectRoles} sr
549
+ JOIN ${roles} r ON sr.role_id = r.id`)
550
+ .all();
551
+ // Batch load all subject direct rights
552
+ const subjectRightRows = this.db
553
+ .prepare(`SELECT sr.subject_id, rt.*
554
+ FROM ${subjectRights} sr
555
+ JOIN ${rights} rt ON sr.right_id = rt.id`)
556
+ .all();
557
+ // Batch load all role rights
558
+ const roleRightRows = this.db
559
+ .prepare(`SELECT r.name as role_name, rt.*
560
+ FROM ${roleRights} rr
561
+ JOIN ${roles} r ON rr.role_id = r.id
562
+ JOIN ${rights} rt ON rr.right_id = rt.id`)
563
+ .all();
564
+ // Build role -> rights mapping
565
+ const roleRightsMap = new Map();
566
+ for (const row of roleRightRows) {
567
+ if (!roleRightsMap.has(row.role_name)) {
568
+ roleRightsMap.set(row.role_name, new Rights());
569
+ }
570
+ roleRightsMap.get(row.role_name).add(this.rowToRight(row));
571
+ }
572
+ // Build subject -> roles mapping
573
+ const subjectRolesMap = new Map();
574
+ for (const row of subjectRoleRows) {
575
+ if (!subjectRolesMap.has(row.subject_id)) {
576
+ subjectRolesMap.set(row.subject_id, []);
577
+ }
578
+ subjectRolesMap.get(row.subject_id).push(row.role_name);
579
+ }
580
+ // Build subject -> direct rights mapping
581
+ const subjectDirectRightsMap = new Map();
582
+ for (const row of subjectRightRows) {
583
+ if (!subjectDirectRightsMap.has(row.subject_id)) {
584
+ subjectDirectRightsMap.set(row.subject_id, new Rights());
585
+ }
586
+ subjectDirectRightsMap.get(row.subject_id).add(this.rowToRight(row));
587
+ }
588
+ // Now construct Subject objects and check access
589
+ const matchingSubjects = [];
590
+ for (const [subjectId, identifier] of subjectIdToIdentifier) {
591
+ const subject = new Subject();
592
+ // Add roles with their rights
593
+ const roleNames = subjectRolesMap.get(subjectId) ?? [];
594
+ for (const roleName of roleNames) {
595
+ const roleRights = roleRightsMap.get(roleName) ?? new Rights();
596
+ const role = new Role(roleName, roleRights);
597
+ subject.memberOf(role);
598
+ }
599
+ // Add direct rights
600
+ const directRights = subjectDirectRightsMap.get(subjectId);
601
+ if (directRights) {
602
+ for (const right of directRights.allRights()) {
603
+ subject.rights.add(right);
604
+ }
605
+ }
606
+ // Check if subject has the requested access
607
+ if (subject.has(pathPattern, flags)) {
608
+ matchingSubjects.push(identifier);
609
+ }
610
+ }
611
+ return matchingSubjects;
612
+ }
613
+ // ===========================================================================
614
+ // Utility
615
+ // ===========================================================================
616
+ async clear() {
617
+ if (!this.db) {
618
+ throw new Error('Not connected');
619
+ }
620
+ const { rights, roleInheritance, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
621
+ this.db.run(`DELETE FROM ${subjectRights}`);
622
+ this.db.run(`DELETE FROM ${subjectRoles}`);
623
+ this.db.run(`DELETE FROM ${subjects}`);
624
+ this.db.run(`DELETE FROM ${roleInheritance}`);
625
+ this.db.run(`DELETE FROM ${roleRights}`);
626
+ this.db.run(`DELETE FROM ${roles}`);
627
+ this.db.run(`DELETE FROM ${rights}`);
628
+ this.roleIdCache.clear();
629
+ }
630
+ async transaction(fn) {
631
+ if (!this.db) {
632
+ throw new Error('Not connected');
633
+ }
634
+ const isNested = this.transactionDepth > 0;
635
+ if (!isNested) {
636
+ this.db.run('BEGIN IMMEDIATE TRANSACTION');
637
+ }
638
+ this.transactionDepth++;
639
+ try {
640
+ const result = await fn(this);
641
+ this.transactionDepth--;
642
+ if (!isNested) {
643
+ this.db.run('COMMIT');
644
+ }
645
+ return result;
646
+ }
647
+ catch (error) {
648
+ this.transactionDepth--;
649
+ if (!isNested) {
650
+ this.db.run('ROLLBACK');
651
+ }
652
+ throw error;
653
+ }
654
+ }
655
+ }