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,673 @@
1
+ import Redis from 'ioredis';
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
+ /**
10
+ * Redis key structure:
11
+ *
12
+ * Rights:
13
+ * {prefix}rights:{id} → Hash { path, allow_mask, deny_mask, description, tags, valid_from, valid_until, created_at, updated_at }
14
+ * {prefix}rights:_seq → String (auto-increment counter)
15
+ * {prefix}rights:_all → Set of all right IDs
16
+ * {prefix}rights:_unique:{hash} → String (right ID for unique constraint lookup)
17
+ *
18
+ * Roles:
19
+ * {prefix}roles:{name} → Hash { name, created_at, updated_at }
20
+ * {prefix}roles:_all → Set of all role names
21
+ * {prefix}roles:{name}:rights → Set of right IDs
22
+ * {prefix}roles:{name}:parents → Set of parent role names
23
+ *
24
+ * Subjects:
25
+ * {prefix}subjects:{identifier} → Hash { identifier, id, created_at, updated_at }
26
+ * {prefix}subjects:_all → Set of all subject identifiers
27
+ * {prefix}subjects:_seq → String (auto-increment counter for subject IDs)
28
+ * {prefix}subjects:{identifier}:roles → Set of role names
29
+ * {prefix}subjects:{identifier}:rights → Set of right IDs
30
+ */
31
+ export class RedisAdapter extends BaseAdapter {
32
+ constructor(options = {}) {
33
+ super(options);
34
+ this.redis = null;
35
+ this.transactionDepth = 0;
36
+ this.options = options;
37
+ }
38
+ // ===========================================================================
39
+ // Key Helpers
40
+ // ===========================================================================
41
+ /**
42
+ * Generate a Redis key with the configured prefix
43
+ */
44
+ key(...parts) {
45
+ const prefix = this.options.keyPrefix ?? this.tablePrefix;
46
+ return `${prefix}${parts.join(':')}`;
47
+ }
48
+ /**
49
+ * Generate a unique hash for a right's unique constraint fields
50
+ */
51
+ rightUniqueHash(right) {
52
+ const parts = [
53
+ right.path,
54
+ right.allowMaskValue.toString(),
55
+ right.denyMaskValue.toString(),
56
+ right.priority.toString(),
57
+ right.validFrom?.toISOString() ?? 'null',
58
+ right.validUntil?.toISOString() ?? 'null'
59
+ ];
60
+ return parts.join('|');
61
+ }
62
+ // ===========================================================================
63
+ // Lifecycle
64
+ // ===========================================================================
65
+ async connect() {
66
+ this.redis = this.options.url
67
+ ? new Redis(this.options.url, {
68
+ lazyConnect: this.options.lazyConnect ?? true
69
+ })
70
+ : new Redis({
71
+ db: this.options.db ?? 0,
72
+ host: this.options.host ?? 'localhost',
73
+ lazyConnect: this.options.lazyConnect ?? true,
74
+ password: this.options.password,
75
+ port: this.options.port ?? 6379,
76
+ tls: this.options.tls
77
+ });
78
+ await this.redis.connect();
79
+ }
80
+ async disconnect() {
81
+ if (this.redis) {
82
+ await this.redis.quit();
83
+ this.redis = null;
84
+ }
85
+ }
86
+ async migrate() {
87
+ // Redis is schemaless, so migration is a no-op
88
+ // Just verify we're connected
89
+ if (!this.redis) {
90
+ throw new Error('Not connected');
91
+ }
92
+ await this.redis.ping();
93
+ }
94
+ // ===========================================================================
95
+ // Rights Operations
96
+ // ===========================================================================
97
+ async saveRight(right) {
98
+ if (!this.redis) {
99
+ throw new Error('Not connected');
100
+ }
101
+ const uniqueHash = this.rightUniqueHash(right);
102
+ const uniqueKey = this.key('rights', '_unique', uniqueHash);
103
+ // Check for existing right with same unique constraint
104
+ const existingId = await this.redis.get(uniqueKey);
105
+ if (existingId) {
106
+ const id = Number.parseInt(existingId, 10);
107
+ // Update the updated_at timestamp
108
+ await this.redis.hset(this.key('rights', existingId), 'updated_at', new Date().toISOString());
109
+ right._setDbId(id);
110
+ return id;
111
+ }
112
+ // Generate new ID
113
+ const id = await this.redis.incr(this.key('rights', '_seq'));
114
+ const now = new Date().toISOString();
115
+ const row = this.rightToRow(right);
116
+ const hashData = {
117
+ allow_mask: row.allow_mask.toString(),
118
+ created_at: now,
119
+ deny_mask: row.deny_mask.toString(),
120
+ description: row.description ?? '',
121
+ id: id.toString(),
122
+ path: row.path,
123
+ priority: row.priority.toString(),
124
+ tags: row.tags ?? '',
125
+ updated_at: now,
126
+ valid_from: row.valid_from ?? '',
127
+ valid_until: row.valid_until ?? ''
128
+ };
129
+ const rightKey = this.key('rights', id.toString());
130
+ // Use pipeline for atomic operation
131
+ const pipeline = this.redis.multi();
132
+ pipeline.hset(rightKey, hashData);
133
+ pipeline.sadd(this.key('rights', '_all'), id.toString());
134
+ pipeline.set(uniqueKey, id.toString());
135
+ await pipeline.exec();
136
+ right._setDbId(id);
137
+ return id;
138
+ }
139
+ async saveRights(rights) {
140
+ const ids = [];
141
+ await this.transaction(async () => {
142
+ for (const right of rights.allRights()) {
143
+ const id = await this.saveRight(right);
144
+ ids.push(id);
145
+ }
146
+ });
147
+ return ids;
148
+ }
149
+ async loadRight(id) {
150
+ if (!this.redis) {
151
+ throw new Error('Not connected');
152
+ }
153
+ const rightKey = this.key('rights', id.toString());
154
+ const data = await this.redis.hgetall(rightKey);
155
+ if (!data || Object.keys(data).length === 0) {
156
+ return null;
157
+ }
158
+ const row = this.hashToRightsRow(data);
159
+ return this.rowToRight(row);
160
+ }
161
+ async loadRights() {
162
+ if (!this.redis) {
163
+ throw new Error('Not connected');
164
+ }
165
+ const allIds = await this.redis.smembers(this.key('rights', '_all'));
166
+ const loadedRights = new Rights();
167
+ for (const idStr of allIds) {
168
+ const right = await this.loadRight(Number.parseInt(idStr, 10));
169
+ if (right) {
170
+ loadedRights.add(right);
171
+ }
172
+ }
173
+ return loadedRights;
174
+ }
175
+ async loadRightsByPath(pathPattern) {
176
+ if (!this.redis) {
177
+ throw new Error('Not connected');
178
+ }
179
+ // Convert glob pattern to regex for filtering
180
+ // * → .* and ? → .
181
+ const regexPattern = pathPattern
182
+ .replaceAll(/[$()+.[\\\]^{|}]/g, String.raw `\$&`) // Escape regex special chars except * and ?
183
+ .replaceAll('*', '.*')
184
+ .replaceAll('?', '.');
185
+ const regex = new RegExp(`^${regexPattern}$`);
186
+ const allRights = await this.loadRights();
187
+ const matchedRights = new Rights();
188
+ for (const right of allRights.allRights()) {
189
+ if (regex.test(right.path)) {
190
+ matchedRights.add(right);
191
+ }
192
+ }
193
+ return matchedRights;
194
+ }
195
+ async deleteRight(id) {
196
+ if (!this.redis) {
197
+ throw new Error('Not connected');
198
+ }
199
+ const rightKey = this.key('rights', id.toString());
200
+ const data = await this.redis.hgetall(rightKey);
201
+ if (!data || Object.keys(data).length === 0) {
202
+ return false;
203
+ }
204
+ // Reconstruct the unique hash to delete the unique constraint key
205
+ const row = this.hashToRightsRow(data);
206
+ const right = this.rowToRight(row);
207
+ const uniqueHash = this.rightUniqueHash(right);
208
+ const uniqueKey = this.key('rights', '_unique', uniqueHash);
209
+ const pipeline = this.redis.multi();
210
+ pipeline.del(rightKey);
211
+ pipeline.srem(this.key('rights', '_all'), id.toString());
212
+ pipeline.del(uniqueKey);
213
+ await pipeline.exec();
214
+ return true;
215
+ }
216
+ // ===========================================================================
217
+ // Role Operations
218
+ // ===========================================================================
219
+ async saveRole(role) {
220
+ if (!this.redis) {
221
+ throw new Error('Not connected');
222
+ }
223
+ return this.transaction(async () => {
224
+ const roleKey = this.key('roles', role.name);
225
+ const existingData = await this.redis.hgetall(roleKey);
226
+ const now = new Date().toISOString();
227
+ const createdAt = existingData && existingData.created_at ? existingData.created_at : now;
228
+ // Save/update role hash
229
+ await this.redis.hset(roleKey, {
230
+ created_at: createdAt,
231
+ name: role.name,
232
+ updated_at: now
233
+ });
234
+ // Add to roles set
235
+ await this.redis.sadd(this.key('roles', '_all'), role.name);
236
+ // Clear and rebuild role rights
237
+ const roleRightsKey = this.key('roles', role.name, 'rights');
238
+ await this.redis.del(roleRightsKey);
239
+ for (const right of role.rights.allRights()) {
240
+ const rightId = await this.saveRight(right);
241
+ await this.redis.sadd(roleRightsKey, rightId.toString());
242
+ }
243
+ // Clear and rebuild parent roles
244
+ const roleParentsKey = this.key('roles', role.name, 'parents');
245
+ await this.redis.del(roleParentsKey);
246
+ for (const parent of role.parents) {
247
+ await this.saveRole(parent);
248
+ await this.redis.sadd(roleParentsKey, parent.name);
249
+ }
250
+ // Return a synthetic ID (use hash of role name since Redis doesn't have auto-increment for this)
251
+ // For consistency, we'll return a positive integer based on the role's position or hash
252
+ const allRoles = await this.redis.smembers(this.key('roles', '_all'));
253
+ return allRoles.indexOf(role.name) + 1;
254
+ });
255
+ }
256
+ async loadRole(name) {
257
+ if (!this.redis) {
258
+ throw new Error('Not connected');
259
+ }
260
+ const roleKey = this.key('roles', name);
261
+ const data = await this.redis.hgetall(roleKey);
262
+ if (!data || Object.keys(data).length === 0) {
263
+ return null;
264
+ }
265
+ // Load role's rights
266
+ const rights = new Rights();
267
+ const roleRightsKey = this.key('roles', name, 'rights');
268
+ const rightIds = await this.redis.smembers(roleRightsKey);
269
+ for (const rightIdStr of rightIds) {
270
+ const right = await this.loadRight(Number.parseInt(rightIdStr, 10));
271
+ if (right) {
272
+ rights.add(right);
273
+ }
274
+ }
275
+ return new Role(name, rights);
276
+ }
277
+ async loadRoles() {
278
+ if (!this.redis) {
279
+ throw new Error('Not connected');
280
+ }
281
+ const allRoleNames = await this.redis.smembers(this.key('roles', '_all'));
282
+ const loadedRoles = [];
283
+ for (const name of allRoleNames) {
284
+ const role = await this.loadRole(name);
285
+ if (role) {
286
+ loadedRoles.push(role);
287
+ }
288
+ }
289
+ return loadedRoles;
290
+ }
291
+ async deleteRole(name) {
292
+ if (!this.redis) {
293
+ throw new Error('Not connected');
294
+ }
295
+ const roleKey = this.key('roles', name);
296
+ const exists = await this.redis.exists(roleKey);
297
+ if (!exists) {
298
+ return false;
299
+ }
300
+ const pipeline = this.redis.multi();
301
+ pipeline.del(roleKey);
302
+ pipeline.del(this.key('roles', name, 'rights'));
303
+ pipeline.del(this.key('roles', name, 'parents'));
304
+ pipeline.srem(this.key('roles', '_all'), name);
305
+ await pipeline.exec();
306
+ return true;
307
+ }
308
+ // ===========================================================================
309
+ // RoleRegistry Operations
310
+ // ===========================================================================
311
+ async saveRegistry(registry) {
312
+ if (!this.redis) {
313
+ throw new Error('Not connected');
314
+ }
315
+ await this.transaction(async () => {
316
+ const rolesToSave = new Map();
317
+ const collectRoles = (role) => {
318
+ if (!rolesToSave.has(role.name)) {
319
+ rolesToSave.set(role.name, role);
320
+ for (const parent of role.parents) {
321
+ collectRoles(parent);
322
+ }
323
+ }
324
+ };
325
+ registry.toJSON().forEach(roleJson => {
326
+ const role = registry.get(roleJson.name);
327
+ if (role) {
328
+ collectRoles(role);
329
+ }
330
+ });
331
+ for (const role of rolesToSave.values()) {
332
+ await this.saveRole(role);
333
+ }
334
+ });
335
+ }
336
+ async loadRegistry() {
337
+ const registry = new RoleRegistry();
338
+ const roles = await this.loadRoles();
339
+ // First pass: define all roles with their rights
340
+ for (const role of roles) {
341
+ registry.define(role.name, role.rights);
342
+ }
343
+ // Second pass: set up inheritance
344
+ for (const role of roles) {
345
+ const roleParentsKey = this.key('roles', role.name, 'parents');
346
+ const parentNames = await this.redis.smembers(roleParentsKey);
347
+ const registryRole = registry.get(role.name);
348
+ if (registryRole) {
349
+ for (const parentName of parentNames) {
350
+ const parentRole = registry.get(parentName);
351
+ if (parentRole) {
352
+ registryRole.inheritsFrom(parentRole);
353
+ }
354
+ }
355
+ }
356
+ }
357
+ return registry;
358
+ }
359
+ // ===========================================================================
360
+ // Subject Operations
361
+ // ===========================================================================
362
+ async saveSubject(identifier, subject) {
363
+ if (!this.redis) {
364
+ throw new Error('Not connected');
365
+ }
366
+ return this.transaction(async () => {
367
+ const subjectKey = this.key('subjects', identifier);
368
+ const existingData = await this.redis.hgetall(subjectKey);
369
+ const now = new Date().toISOString();
370
+ let id;
371
+ if (existingData && existingData.id) {
372
+ id = Number.parseInt(existingData.id, 10);
373
+ // Update existing subject
374
+ await this.redis.hset(subjectKey, {
375
+ identifier,
376
+ updated_at: now
377
+ });
378
+ }
379
+ else {
380
+ // Create new subject with auto-increment ID
381
+ id = await this.redis.incr(this.key('subjects', '_seq'));
382
+ await this.redis.hset(subjectKey, {
383
+ created_at: now,
384
+ id: id.toString(),
385
+ identifier,
386
+ updated_at: now
387
+ });
388
+ await this.redis.sadd(this.key('subjects', '_all'), identifier);
389
+ }
390
+ // Clear and rebuild subject roles
391
+ const subjectRolesKey = this.key('subjects', identifier, 'roles');
392
+ await this.redis.del(subjectRolesKey);
393
+ for (const role of subject.roles) {
394
+ await this.saveRole(role);
395
+ await this.redis.sadd(subjectRolesKey, role.name);
396
+ }
397
+ // Clear and rebuild subject direct rights
398
+ const subjectRightsKey = this.key('subjects', identifier, 'rights');
399
+ await this.redis.del(subjectRightsKey);
400
+ for (const right of subject.rights.allRights()) {
401
+ const rightId = await this.saveRight(right);
402
+ await this.redis.sadd(subjectRightsKey, rightId.toString());
403
+ }
404
+ return id;
405
+ });
406
+ }
407
+ async loadSubject(identifier) {
408
+ if (!this.redis) {
409
+ throw new Error('Not connected');
410
+ }
411
+ const subjectKey = this.key('subjects', identifier);
412
+ const data = await this.redis.hgetall(subjectKey);
413
+ if (!data || Object.keys(data).length === 0) {
414
+ return null;
415
+ }
416
+ const subject = new Subject();
417
+ // Load subject's roles
418
+ const subjectRolesKey = this.key('subjects', identifier, 'roles');
419
+ const roleNames = await this.redis.smembers(subjectRolesKey);
420
+ for (const roleName of roleNames) {
421
+ const role = await this.loadRole(roleName);
422
+ if (role) {
423
+ subject.memberOf(role);
424
+ }
425
+ }
426
+ // Load subject's direct rights
427
+ const subjectRightsKey = this.key('subjects', identifier, 'rights');
428
+ const rightIds = await this.redis.smembers(subjectRightsKey);
429
+ for (const rightIdStr of rightIds) {
430
+ const right = await this.loadRight(Number.parseInt(rightIdStr, 10));
431
+ if (right) {
432
+ subject.rights.add(right);
433
+ }
434
+ }
435
+ return subject;
436
+ }
437
+ async deleteSubject(identifier) {
438
+ if (!this.redis) {
439
+ throw new Error('Not connected');
440
+ }
441
+ const subjectKey = this.key('subjects', identifier);
442
+ const exists = await this.redis.exists(subjectKey);
443
+ if (!exists) {
444
+ return false;
445
+ }
446
+ const pipeline = this.redis.multi();
447
+ pipeline.del(subjectKey);
448
+ pipeline.del(this.key('subjects', identifier, 'roles'));
449
+ pipeline.del(this.key('subjects', identifier, 'rights'));
450
+ pipeline.srem(this.key('subjects', '_all'), identifier);
451
+ await pipeline.exec();
452
+ return true;
453
+ }
454
+ async getAllSubjectIdentifiers() {
455
+ if (!this.redis) {
456
+ throw new Error('Not connected');
457
+ }
458
+ const allSubjectsKey = this.key('subjects', '_all');
459
+ return this.redis.smembers(allSubjectsKey);
460
+ }
461
+ /**
462
+ * Optimized findSubjectsWithAccess using batch loading with Redis pipeline.
463
+ * Reduces N+1 queries to a constant number of Redis operations regardless of subject count.
464
+ */
465
+ async findSubjectsWithAccess(pathPattern, flags) {
466
+ if (!this.redis) {
467
+ throw new Error('Not connected');
468
+ }
469
+ // Get all subject identifiers
470
+ const allSubjectsKey = this.key('subjects', '_all');
471
+ const subjectIdentifiers = await this.redis.smembers(allSubjectsKey);
472
+ if (subjectIdentifiers.length === 0) {
473
+ return [];
474
+ }
475
+ // Batch load all subject roles using pipeline
476
+ const rolesPipeline = this.redis.pipeline();
477
+ for (const identifier of subjectIdentifiers) {
478
+ rolesPipeline.smembers(this.key('subjects', identifier, 'roles'));
479
+ }
480
+ const rolesResults = await rolesPipeline.exec();
481
+ // Build subject -> roles mapping
482
+ const subjectRolesMap = new Map();
483
+ for (let i = 0; i < subjectIdentifiers.length; i++) {
484
+ const identifier = subjectIdentifiers[i];
485
+ const result = rolesResults?.[i];
486
+ if (result && !result[0]) {
487
+ subjectRolesMap.set(identifier, result[1]);
488
+ }
489
+ else {
490
+ subjectRolesMap.set(identifier, []);
491
+ }
492
+ }
493
+ // Collect all unique role names
494
+ const allRoleNames = new Set();
495
+ for (const roles of subjectRolesMap.values()) {
496
+ for (const roleName of roles) {
497
+ allRoleNames.add(roleName);
498
+ }
499
+ }
500
+ // Batch load all role rights using pipeline
501
+ const roleRightIdsPipeline = this.redis.pipeline();
502
+ const roleNamesArray = Array.from(allRoleNames);
503
+ for (const roleName of roleNamesArray) {
504
+ roleRightIdsPipeline.smembers(this.key('roles', roleName, 'rights'));
505
+ }
506
+ const roleRightIdsResults = await roleRightIdsPipeline.exec();
507
+ // Build role -> right IDs mapping
508
+ const roleRightIdsMap = new Map();
509
+ for (let i = 0; i < roleNamesArray.length; i++) {
510
+ const roleName = roleNamesArray[i];
511
+ const result = roleRightIdsResults?.[i];
512
+ if (result && !result[0]) {
513
+ roleRightIdsMap.set(roleName, result[1]);
514
+ }
515
+ else {
516
+ roleRightIdsMap.set(roleName, []);
517
+ }
518
+ }
519
+ // Batch load all subject direct rights using pipeline
520
+ const directRightsPipeline = this.redis.pipeline();
521
+ for (const identifier of subjectIdentifiers) {
522
+ directRightsPipeline.smembers(this.key('subjects', identifier, 'rights'));
523
+ }
524
+ const directRightsResults = await directRightsPipeline.exec();
525
+ // Build subject -> direct right IDs mapping
526
+ const subjectDirectRightIdsMap = new Map();
527
+ for (let i = 0; i < subjectIdentifiers.length; i++) {
528
+ const identifier = subjectIdentifiers[i];
529
+ const result = directRightsResults?.[i];
530
+ if (result && !result[0]) {
531
+ subjectDirectRightIdsMap.set(identifier, result[1]);
532
+ }
533
+ else {
534
+ subjectDirectRightIdsMap.set(identifier, []);
535
+ }
536
+ }
537
+ // Collect all unique right IDs
538
+ const allRightIds = new Set();
539
+ for (const rightIds of roleRightIdsMap.values()) {
540
+ for (const id of rightIds) {
541
+ allRightIds.add(id);
542
+ }
543
+ }
544
+ for (const rightIds of subjectDirectRightIdsMap.values()) {
545
+ for (const id of rightIds) {
546
+ allRightIds.add(id);
547
+ }
548
+ }
549
+ // Batch load all rights using pipeline
550
+ const rightsPipeline = this.redis.pipeline();
551
+ const rightIdsArray = Array.from(allRightIds);
552
+ for (const rightId of rightIdsArray) {
553
+ rightsPipeline.hgetall(this.key('rights', rightId));
554
+ }
555
+ const rightsResults = await rightsPipeline.exec();
556
+ // Build right ID -> Right mapping
557
+ const rightsMap = new Map();
558
+ for (let i = 0; i < rightIdsArray.length; i++) {
559
+ const rightId = rightIdsArray[i];
560
+ const result = rightsResults?.[i];
561
+ if (result && !result[0] && result[1]) {
562
+ const data = result[1];
563
+ if (Object.keys(data).length > 0) {
564
+ const row = this.hashToRightsRow(data);
565
+ rightsMap.set(rightId, this.rowToRight(row));
566
+ }
567
+ }
568
+ }
569
+ // Build role -> Rights mapping
570
+ const roleRightsMap = new Map();
571
+ for (const [roleName, rightIds] of roleRightIdsMap) {
572
+ const rights = new Rights();
573
+ for (const rightId of rightIds) {
574
+ const right = rightsMap.get(rightId);
575
+ if (right) {
576
+ rights.add(right);
577
+ }
578
+ }
579
+ roleRightsMap.set(roleName, rights);
580
+ }
581
+ // Now construct Subject objects and check access
582
+ const matchingSubjects = [];
583
+ for (const identifier of subjectIdentifiers) {
584
+ const subject = new Subject();
585
+ // Add roles with their rights
586
+ const roleNames = subjectRolesMap.get(identifier) ?? [];
587
+ for (const roleName of roleNames) {
588
+ const roleRights = roleRightsMap.get(roleName) ?? new Rights();
589
+ const role = new Role(roleName, roleRights);
590
+ subject.memberOf(role);
591
+ }
592
+ // Add direct rights
593
+ const directRightIds = subjectDirectRightIdsMap.get(identifier) ?? [];
594
+ for (const rightId of directRightIds) {
595
+ const right = rightsMap.get(rightId);
596
+ if (right) {
597
+ subject.rights.add(right);
598
+ }
599
+ }
600
+ // Check if subject has the requested access
601
+ if (subject.has(pathPattern, flags)) {
602
+ matchingSubjects.push(identifier);
603
+ }
604
+ }
605
+ return matchingSubjects;
606
+ }
607
+ // ===========================================================================
608
+ // Utility
609
+ // ===========================================================================
610
+ async clear() {
611
+ if (!this.redis) {
612
+ throw new Error('Not connected');
613
+ }
614
+ const prefix = this.options.keyPrefix ?? this.tablePrefix;
615
+ const pattern = `${prefix}*`;
616
+ // Use SCAN to find all keys matching our prefix (safer than KEYS for production)
617
+ let cursor = '0';
618
+ const keysToDelete = [];
619
+ do {
620
+ const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
621
+ cursor = nextCursor;
622
+ keysToDelete.push(...keys);
623
+ } while (cursor !== '0');
624
+ // Delete keys in batches
625
+ if (keysToDelete.length > 0) {
626
+ const batchSize = 100;
627
+ for (let i = 0; i < keysToDelete.length; i += batchSize) {
628
+ const batch = keysToDelete.slice(i, i + batchSize);
629
+ await this.redis.del(...batch);
630
+ }
631
+ }
632
+ }
633
+ async transaction(fn) {
634
+ if (!this.redis) {
635
+ throw new Error('Not connected');
636
+ }
637
+ // Note: Redis MULTI/EXEC doesn't support true rollback like SQL databases.
638
+ // If an error occurs, we can't undo operations that were already executed.
639
+ // For nested transactions, we just track depth and only wrap at the outermost level.
640
+ const isNested = this.transactionDepth > 0;
641
+ this.transactionDepth++;
642
+ try {
643
+ const result = await fn(this);
644
+ this.transactionDepth--;
645
+ return result;
646
+ }
647
+ catch (error) {
648
+ this.transactionDepth--;
649
+ throw error;
650
+ }
651
+ }
652
+ // ===========================================================================
653
+ // Private Helpers
654
+ // ===========================================================================
655
+ /**
656
+ * Convert Redis hash data to a RightsRow
657
+ */
658
+ hashToRightsRow(data) {
659
+ return {
660
+ allow_mask: Number.parseInt(data.allow_mask || '0', 10),
661
+ created_at: data.created_at || new Date().toISOString(),
662
+ deny_mask: Number.parseInt(data.deny_mask || '0', 10),
663
+ description: data.description || null,
664
+ id: Number.parseInt(data.id || '0', 10),
665
+ path: data.path || '',
666
+ priority: Number.parseInt(data.priority || '0', 10),
667
+ tags: data.tags || null,
668
+ updated_at: data.updated_at || new Date().toISOString(),
669
+ valid_from: data.valid_from || null,
670
+ valid_until: data.valid_until || null
671
+ };
672
+ }
673
+ }
@@ -0,0 +1,25 @@
1
+ import type { TableNames } from './types';
2
+ /**
3
+ * Default table prefix for all adapter tables
4
+ */
5
+ export declare const DEFAULT_TABLE_PREFIX = "tbl_";
6
+ /**
7
+ * Generate table names with the given prefix
8
+ */
9
+ export declare const createTableNames: (prefix?: string) => TableNames;
10
+ /**
11
+ * Generate SQLite schema with the given table names
12
+ */
13
+ export declare const generateSQLiteSchema: (tables: TableNames) => string;
14
+ /**
15
+ * Generate PostgreSQL schema with the given table names
16
+ */
17
+ export declare const generatePostgresSchema: (tables: TableNames) => string;
18
+ /**
19
+ * Generate SQLite statements to drop all tables (useful for testing)
20
+ */
21
+ export declare const generateSQLiteDropSchema: (tables: TableNames) => string;
22
+ /**
23
+ * Generate PostgreSQL statements to drop all tables (useful for testing)
24
+ */
25
+ export declare const generatePostgresDropSchema: (tables: TableNames) => string;