From cd00146e7562e1e3dc7c78b800943c2f2205f724 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 08:13:02 +0300 Subject: [PATCH 01/25] feat: add initial playground package with permission management examples --- playground/package.json | 14 ++ playground/run.ts | 87 +++++++++++ pnpm-lock.yaml | 6 + pnpm-workspace.yaml | 2 + src/permask.ts | 215 +++++++++++++++++++++++++- test/permark.test.ts | 331 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 playground/package.json create mode 100644 playground/run.ts create mode 100644 pnpm-workspace.yaml create mode 100644 test/permark.test.ts diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 0000000..24a3eba --- /dev/null +++ b/playground/package.json @@ -0,0 +1,14 @@ +{ + "name": "playground", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx run.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "permask": "workspace:*" + } +} diff --git a/playground/run.ts b/playground/run.ts new file mode 100644 index 0000000..6795d79 --- /dev/null +++ b/playground/run.ts @@ -0,0 +1,87 @@ +import { Permark } from 'permask'; + +// Example 1: Using default permissions (READ, WRITE, DELETE) +const defaultPermissions = new Permark(); + +// Create a permission bitmask for group 1 with read and write access +const userPermission = defaultPermissions.createStandardBitmask({ + group: 1, + read: true, + write: true +}); + +console.log('User Permission:', defaultPermissions.parse(userPermission)); +console.log('Can Read:', defaultPermissions.canRead(userPermission)); // true +console.log('Can Write:', defaultPermissions.canWrite(userPermission)); // true +console.log('Can Delete:', defaultPermissions.canDelete(userPermission)); // false + +// Example 2: Custom permissions with named groups +const customPermissions = new Permark({ + permissions: { + VIEW: 1, // 0b001 + EDIT: 2, // 0b010 + DELETE: 4, // 0b100 + SHARE: 8, // 0b1000 + PRINT: 16, // 0b10000 + DOWNLOAD: 32, // 0b100000 + ADMIN: 64, // 0b1000000 + ENCRYPT: 128 // 0b10000000 + }, + accessBits: 8, // We need 7 bits for our permissions + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + VIDEOS: 3, + FILES: 4, + ADMIN: 100 + } +}); + +// Create permissions for a document editor +const editorPermission = customPermissions.create('DOCUMENTS', ['VIEW', 'EDIT', 'SHARE']); +console.log('Editor Permission:', customPermissions.parse(editorPermission)); + +// Check specific permissions +console.log('Can View:', customPermissions.hasPermission(editorPermission, 'VIEW')); // true +console.log('Can Delete:', customPermissions.hasPermission(editorPermission, 'DELETE')); // false + +// Add a permission +const enhancedPermission = customPermissions.addPermission(editorPermission, 'PRINT'); +console.log('Enhanced Permission:', customPermissions.parse(enhancedPermission)); + +// Remove a permission +const reducedPermission = customPermissions.removePermission(enhancedPermission, 'SHARE'); +console.log('Reduced Permission:', customPermissions.parse(reducedPermission)); + +// Check group +console.log('Is Document Group:', customPermissions.hasGroup(editorPermission, 'DOCUMENTS')); // true +console.log('Is Photo Group:', customPermissions.hasGroup(editorPermission, 'PHOTOS')); // false +console.log('Group Name:', customPermissions.getGroupName(editorPermission)); // "DOCUMENTS" + +// Register a new permission at runtime +customPermissions.registerPermission('ENCRYPT', 128); +const securePermission = customPermissions.addPermission(editorPermission, 'ENCRYPT'); +console.log('Secure Permission:', customPermissions.parse(securePermission)); + +// Register a new group at runtime +customPermissions.registerGroup('SECURE_DOCS', 101); +const secureDocPermission = customPermissions.create('SECURE_DOCS', ['VIEW', 'EDIT', 'ENCRYPT']); +console.log('Secure Doc Permission:', customPermissions.parse(secureDocPermission)); + +// Example 3: Using the Permark class with different bits configuration +const tinyPermissions = new Permark({ + permissions: { + READ_ONLY: 1, // 0b01 + FULL: 3 // 0b11 + }, + accessBits: 2, // Only using 2 bits for permissions + groups: { + PUBLIC: 1, + PRIVATE: 2 + } +}); + +const publicReadOnly = tinyPermissions.create('PUBLIC', ['READ_ONLY']); +console.log('Public Read Only:', tinyPermissions.parse(publicReadOnly)); +console.log('Group:', tinyPermissions.getGroup(publicReadOnly)); + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c2ed5..d4e7f89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,12 @@ importers: specifier: ^3.0.1 version: 3.0.1(@types/node@22.10.6)(jiti@2.4.2)(yaml@2.7.0) + playground: + dependencies: + permask: + specifier: workspace:* + version: link:.. + packages: '@ampproject/remapping@2.3.0': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..425e4d9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - playground \ No newline at end of file diff --git a/src/permask.ts b/src/permask.ts index 84e442c..5abeb21 100644 --- a/src/permask.ts +++ b/src/permask.ts @@ -1,4 +1,6 @@ import type { EnumOrObjectType, StringKeysType } from "./types/utils"; +import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; +import { PermissionAccess } from "./constants/permission"; import { canDelete, canRead, @@ -6,7 +8,11 @@ import { createBitmask, getPermissionGroup, hasPermissionGroup, - parseBitmask + parseBitmask, + addPermissionAccess, + setPermissionGroup, + hasPermissionAccess, + getPermissionAccess } from "./utils/bitmask"; /** @@ -40,3 +46,210 @@ export function createPermask< canDelete }; } + +export class Permark = Record> { + private permissions: T; + private accessBits: number; + private accessMask: number; + private groups: Record; + + /** + * Create a custom permission system + * @param options Configuration options for the permission system + */ + constructor(options: { + permissions?: T; // Custom permission types with bit values + accessBits?: number; // Number of bits allocated for permissions + accessMask?: number; // Mask for access bits + groups?: Record; // Custom groups + } = {}) { + // Use the provided values or fall back to constants + this.accessBits = options.accessBits || ACCESS_BITS; + + // For accessMask, use provided value, or calculate based on custom bits, or use the constant + if (options.accessMask !== undefined) { + this.accessMask = options.accessMask; + } else if (options.accessBits !== undefined) { + // Only recalculate if custom bits provided + this.accessMask = (1 << this.accessBits) - 1; + } else { + // Otherwise use the predefined constant + this.accessMask = ACCESS_MASK; + } + + this.permissions = (options.permissions || PermissionAccess as unknown as T); + this.groups = options.groups || {}; + + // Validate that permissions fit within specified bits + const maxValue = this.accessMask; + for (const [key, value] of Object.entries(this.permissions)) { + if (value > maxValue) { + throw new Error(`Permission '${key}' value ${value} exceeds the maximum value ${maxValue} for ${this.accessBits} bits`); + } + } + } + + /** + * Create a bitmask with the specified permissions and group + */ + create(group: number | string, permissionList: (keyof T)[]): number { + let access = 0; + for (const permission of permissionList) { + access |= this.permissions[permission as string] || 0; + } + + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return (groupValue << this.accessBits) | access; + } + + /** + * Check if a bitmask has a specific permission + */ + hasPermission(bitmask: number, permission: keyof T): boolean { + const access = bitmask & this.accessMask; + const permValue = (this.permissions as Record)[permission as string] || 0; + return (access & permValue) !== 0; + } + + /** + * Get permission group from bitmask + */ + getGroup(bitmask: number): number { + return bitmask >> this.accessBits; + } + + /** + * Get group name by group value + */ + getGroupName(bitmask: number): string | undefined { + const groupValue = this.getGroup(bitmask); + const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); + return entry?.[0]; + } + + /** + * Check if bitmask has the specified group + */ + hasGroup(bitmask: number, group: number | string): boolean { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return this.getGroup(bitmask) === groupValue; + } + + /** + * Add permission to existing bitmask without changing the group + */ + addPermission(bitmask: number, permission: keyof T): number { + const group = this.getGroup(bitmask); + const permValue = (this.permissions as Record)[permission as string] || 0; + const access = (bitmask & this.accessMask) | permValue; + return (group << this.accessBits) | access; + } + + /** + * Remove permission from existing bitmask + */ + removePermission(bitmask: number, permission: keyof T): number { + const group = this.getGroup(bitmask); + const permValue = (this.permissions as Record)[permission as string] || 0; + const access = (bitmask & this.accessMask) & ~permValue; + return (group << this.accessBits) | access; + } + + /** + * Parse bitmask into an object with group and permissions + */ + parse(bitmask: number): { + group: number; + groupName?: string; + permissions: Partial>; + } { + const group = this.getGroup(bitmask); + const permissions = {} as Partial>; + + for (const [key, value] of Object.entries(this.permissions)) { + permissions[key as keyof T] = this.hasPermission(bitmask, key as keyof T); + } + + return { + group, + groupName: this.getGroupName(bitmask), + permissions + }; + } + + /** + * Register a new permission type (use with caution) + */ + registerPermission(name: string, bitValue: number): void { + if (bitValue > this.accessMask) { + throw new Error(`Permission value ${bitValue} exceeds the maximum value ${this.accessMask} for ${this.accessBits} bits`); + } + + (this.permissions as Record)[name] = bitValue; + } + + /** + * Register a new group + */ + registerGroup(name: string, value: number): void { + this.groups[name] = value; + } + + /** + * Create compatible bitmasks that can be used with the standard utils functions + */ + createStandardBitmask({ + group, + read = false, + write = false, + delete: del = false, + customPermissions = [] + }: { + group: number | string; + read?: boolean; + write?: boolean; + delete?: boolean; + customPermissions?: (keyof T)[]; + }): number { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + + // Create our own bitmask instead of using the utility function that uses ACCESS_BITS + let bitmask = 0; + if (read) bitmask |= PermissionAccess.READ; + if (write) bitmask |= PermissionAccess.WRITE; + if (del) bitmask |= PermissionAccess.DELETE; + + // Set group using our accessBits + bitmask |= (groupValue << this.accessBits); + + // Add any custom permissions + for (const permission of customPermissions) { + const permValue = (this.permissions as Record)[permission as string]; + if (permValue !== undefined) { + // Get the current access part without changing the group + const currentAccess = bitmask & this.accessMask; + // Add the new permission + const newAccess = currentAccess | permValue; + // Replace the access part in the result + bitmask = (bitmask & ~this.accessMask) | newAccess; + } + } + + return bitmask; + } + + /** + * Compatibility methods with the standard utils + */ + canRead(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.READ); + } + + canWrite(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.WRITE); + } + + canDelete(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.DELETE); + } +} \ No newline at end of file diff --git a/test/permark.test.ts b/test/permark.test.ts new file mode 100644 index 0000000..c31f6a3 --- /dev/null +++ b/test/permark.test.ts @@ -0,0 +1,331 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Permark } from '../src/permask'; +import { PermissionAccess } from '../src/constants/permission'; +import { ACCESS_BITS, ACCESS_MASK } from '../src/constants/bitmask'; + +describe('Permark', () => { + // Default permissions instance for basic tests + let defaultPermark: Permark; + + // Custom permissions for advanced tests + let customPermark: Permark<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + [key: string]: number; + }>; + + beforeEach(() => { + // Create a default instance + defaultPermark = new Permark(); + + // Create a custom instance with more bits + customPermark = new Permark({ + permissions: { + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16 // 0b10000 + }, + accessBits: 6, // Changed from 5 to 6 to accommodate values up to 63 + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + VIDEOS: 3 + } + }); + }); + + describe('Initialization', () => { + it('should initialize with default values', () => { + expect(defaultPermark).toBeDefined(); + + // Create a permission to verify defaults are working + const permission = defaultPermark.createStandardBitmask({ + group: 1, + read: true + }); + + expect(defaultPermark.canRead(permission)).toBe(true); + expect(defaultPermark.canWrite(permission)).toBe(false); + expect(defaultPermark.getGroup(permission)).toBe(1); + }); + + it('should initialize with custom permissions', () => { + expect(customPermark).toBeDefined(); + + // Create a custom permission + const permission = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT']); + + // Verify custom permissions work + expect(customPermark.hasPermission(permission, 'VIEW')).toBe(true); + expect(customPermark.hasPermission(permission, 'EDIT')).toBe(true); + expect(customPermark.hasPermission(permission, 'DELETE')).toBe(false); + }); + + it('should throw error for permission values exceeding bit capacity', () => { + expect(() => new Permark({ + permissions: { INVALID: 16 }, + accessBits: 3 // Only allows values up to 7 + })).toThrow(/exceeds the maximum value/); + }); + }); + + describe('Permission Operations', () => { + it('should create permissions with the correct bitmask', () => { + // Create a permission for documents that allows view and edit + const permission = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT']); + + // VIEW (1) + EDIT (2) = 3 + // GROUP (1) << 6 = 64 (updated for 6 bits) + // Expected: 64 | 3 = 67 + expect(permission).toBe(67); + + // Parse and verify components + const parsed = customPermark.parse(permission); + expect(parsed.group).toBe(1); + expect(parsed.groupName).toBe('DOCUMENTS'); + expect(parsed.permissions.VIEW).toBe(true); + expect(parsed.permissions.EDIT).toBe(true); + expect(parsed.permissions.DELETE).toBe(false); + }); + + it('should add permissions correctly', () => { + // Create a base permission + const base = customPermark.create('DOCUMENTS', ['VIEW']); + + // Add EDIT permission + const withEdit = customPermark.addPermission(base, 'EDIT'); + + expect(customPermark.hasPermission(withEdit, 'VIEW')).toBe(true); + expect(customPermark.hasPermission(withEdit, 'EDIT')).toBe(true); + expect(customPermark.getGroup(withEdit)).toBe(1); // Group should be unchanged + }); + + it('should remove permissions correctly', () => { + // Create a permission with multiple accesses + const fullAccess = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT', 'DELETE']); + + // Remove the EDIT permission + const reducedAccess = customPermark.removePermission(fullAccess, 'EDIT'); + + expect(customPermark.hasPermission(reducedAccess, 'VIEW')).toBe(true); + expect(customPermark.hasPermission(reducedAccess, 'EDIT')).toBe(false); + expect(customPermark.hasPermission(reducedAccess, 'DELETE')).toBe(true); + }); + + it('should check permissions correctly', () => { + const permission = customPermark.create('PHOTOS', ['VIEW', 'SHARE']); + + expect(customPermark.hasPermission(permission, 'VIEW')).toBe(true); + expect(customPermark.hasPermission(permission, 'SHARE')).toBe(true); + expect(customPermark.hasPermission(permission, 'EDIT')).toBe(false); + expect(customPermark.hasPermission(permission, 'DELETE')).toBe(false); + expect(customPermark.hasPermission(permission, 'PRINT')).toBe(false); + }); + }); + + describe('Group Operations', () => { + it('should get group value correctly', () => { + const permission = customPermark.create('VIDEOS', ['VIEW']); + expect(customPermark.getGroup(permission)).toBe(3); + }); + + it('should get group name correctly', () => { + const permission = customPermark.create('PHOTOS', ['VIEW']); + expect(customPermark.getGroupName(permission)).toBe('PHOTOS'); + }); + + it('should check group membership correctly', () => { + const permission = customPermark.create('DOCUMENTS', ['VIEW']); + + expect(customPermark.hasGroup(permission, 'DOCUMENTS')).toBe(true); + expect(customPermark.hasGroup(permission, 'PHOTOS')).toBe(false); + expect(customPermark.hasGroup(permission, 1)).toBe(true); + expect(customPermark.hasGroup(permission, 2)).toBe(false); + }); + + it('should handle unknown group names gracefully', () => { + const permission = customPermark.create(999, ['VIEW']); + expect(customPermark.getGroupName(permission)).toBeUndefined(); + + // Creating with non-existent string group should use group 0 + const noGroup = customPermark.create('NON_EXISTENT', ['VIEW']); + expect(customPermark.getGroup(noGroup)).toBe(0); + }); + }); + + describe('Standard Bitmask Compatibility', () => { + it('should create standard bitmasks correctly', () => { + const permission = defaultPermark.createStandardBitmask({ + group: 5, + read: true, + write: true, + delete: false + }); + + expect(defaultPermark.canRead(permission)).toBe(true); + expect(defaultPermark.canWrite(permission)).toBe(true); + expect(defaultPermark.canDelete(permission)).toBe(false); + expect(defaultPermark.getGroup(permission)).toBe(5); + }); + + it('should work with named groups', () => { + customPermark.registerGroup('SPREADSHEETS', 10); + + const permission = customPermark.createStandardBitmask({ + group: 'SPREADSHEETS', + read: true, + write: true, + delete: false, + customPermissions: ['SHARE'] + }); + + expect(customPermark.getGroup(permission)).toBe(10); + expect(customPermark.canRead(permission)).toBe(true); + expect(customPermark.canWrite(permission)).toBe(true); + expect(customPermark.hasPermission(permission, 'SHARE')).toBe(true); + }); + }); + + describe('Dynamic Registration', () => { + it('should register new permissions at runtime', () => { + // Register a new permission - changed from 32 to 32 which fits in 6 bits + customPermark.registerPermission('ARCHIVE', 32); + + // Create permission using the new type + const permission = customPermark.create('DOCUMENTS', ['VIEW', 'ARCHIVE']); + + expect(customPermark.hasPermission(permission, 'ARCHIVE')).toBe(true); + expect(customPermark.parse(permission).permissions['ARCHIVE']).toBe(true); + }); + + it('should throw error when registering permissions that exceed bit capacity', () => { + // Should test with a value that definitely exceeds the capacity (now 6 bits = max 63) + expect(() => customPermark.registerPermission('OVERFLOW', 64)).toThrow(/exceeds the maximum value/); + }); + + it('should register new groups at runtime', () => { + // Register a new group + customPermark.registerGroup('ARCHIVES', 20); + + // Create permission for the new group + const permission = customPermark.create('ARCHIVES', ['VIEW']); + + expect(customPermark.getGroup(permission)).toBe(20); + expect(customPermark.getGroupName(permission)).toBe('ARCHIVES'); + }); + }); + + describe('Parse Functionality', () => { + it('should parse bitmasks to detailed objects', () => { + const permission = customPermark.create('VIDEOS', ['VIEW', 'EDIT', 'SHARE']); + + const parsed = customPermark.parse(permission); + + expect(parsed).toEqual({ + group: 3, + groupName: 'VIDEOS', + permissions: { + VIEW: true, + EDIT: true, + DELETE: false, + SHARE: true, + PRINT: false + } + }); + }); + + it('should handle parsing bitmasks with unknown groups', () => { + // Create a permission with a group that doesn't have a name + const permission = customPermark.create(42, ['VIEW']); + + const parsed = customPermark.parse(permission); + + expect(parsed.group).toBe(42); + expect(parsed.groupName).toBeUndefined(); + expect(parsed.permissions.VIEW).toBe(true); + }); + }); + + describe('Advanced Features', () => { + it('should handle custom access mask', () => { + // Create a completely separate instance with its own configuration + // to avoid interference with other tests + const customMaskPermark = new Permark({ + permissions: { TEST: 1, TEST2: 2 }, // Start with minimal permissions + accessBits: 4, + accessMask: 0b1111 // Explicitly set mask + }); + + // Register a permission that's at the limit + expect(() => customMaskPermark.registerPermission('MAX', 15)).not.toThrow(); + // Register a permission that exceeds the limit + expect(() => customMaskPermark.registerPermission('OVER', 16)).toThrow(/exceeds the maximum value/); + }); + + it('should handle the default values correctly', () => { + const permission = defaultPermark.createStandardBitmask({ + group: 1, + read: true + }); + + // Instead of assuming specific values, check the functional behavior + expect(defaultPermark.canRead(permission)).toBe(true); + expect(defaultPermark.canWrite(permission)).toBe(false); + expect(defaultPermark.getGroup(permission)).toBe(1); + }); + }); + + describe('Edge Cases', () => { + // Create a separate instance for edge case tests to avoid interference + let edgeCasePermark: Permark<{ + VIEW: number; + [key: string]: number; + }>; + + beforeEach(() => { + edgeCasePermark = new Permark({ + permissions: { + VIEW: 1 + }, + accessBits: 4, // Use a higher bit count for these tests + groups: { + DOCUMENTS: 1 + } + }); + }); + + it('should handle group 0 correctly', () => { + const permission = edgeCasePermark.create(0, ['VIEW']); + expect(edgeCasePermark.getGroup(permission)).toBe(0); + }); + + it('should handle permission value 0 correctly', () => { + edgeCasePermark.registerPermission('NO_ACCESS', 0); + + const permission = edgeCasePermark.create('DOCUMENTS', ['NO_ACCESS']); + + // Creating with no actual permission bits results in 0 access + expect(edgeCasePermark.hasPermission(permission, 'VIEW')).toBe(false); + expect(edgeCasePermark.hasPermission(permission, 'NO_ACCESS')).toBe(false); // 0 & anything = 0 + }); + + it('should handle attempting to use undefined permissions', () => { + const permission = edgeCasePermark.create('DOCUMENTS', ['VIEW']); + + // @ts-ignore - Intentionally testing with a non-existent permission + expect(edgeCasePermark.hasPermission(permission, 'NON_EXISTENT')).toBe(false); + + // @ts-ignore - Intentionally testing with a non-existent permission + const updatedPermission = edgeCasePermark.addPermission(permission, 'NON_EXISTENT'); + + // The permission should be unchanged since NON_EXISTENT maps to value 0 + expect(edgeCasePermark.hasPermission(updatedPermission, 'VIEW')).toBe(true); + expect(edgeCasePermark.getGroup(updatedPermission)).toBe(1); + }); + }); +}); From a4083f1980b1deac058170f1081eba87023f7313 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 08:17:27 +0300 Subject: [PATCH 02/25] feat: implement Permask class for flexible permission management --- playground/run.ts | 10 +- src/index.ts | 1 + .../permask-class.test.ts | 186 ++++++++------- src/permask-class.ts | 219 ++++++++++++++++++ src/permask.ts | 215 +---------------- 5 files changed, 318 insertions(+), 313 deletions(-) rename test/permark.test.ts => src/permask-class.test.ts (55%) create mode 100644 src/permask-class.ts diff --git a/playground/run.ts b/playground/run.ts index 6795d79..966805d 100644 --- a/playground/run.ts +++ b/playground/run.ts @@ -1,7 +1,7 @@ -import { Permark } from 'permask'; +import { Permask } from 'permask'; // Example 1: Using default permissions (READ, WRITE, DELETE) -const defaultPermissions = new Permark(); +const defaultPermissions = new Permask(); // Create a permission bitmask for group 1 with read and write access const userPermission = defaultPermissions.createStandardBitmask({ @@ -16,7 +16,7 @@ console.log('Can Write:', defaultPermissions.canWrite(userPermission)); // true console.log('Can Delete:', defaultPermissions.canDelete(userPermission)); // false // Example 2: Custom permissions with named groups -const customPermissions = new Permark({ +const customPermissions = new Permask({ permissions: { VIEW: 1, // 0b001 EDIT: 2, // 0b010 @@ -68,8 +68,8 @@ customPermissions.registerGroup('SECURE_DOCS', 101); const secureDocPermission = customPermissions.create('SECURE_DOCS', ['VIEW', 'EDIT', 'ENCRYPT']); console.log('Secure Doc Permission:', customPermissions.parse(secureDocPermission)); -// Example 3: Using the Permark class with different bits configuration -const tinyPermissions = new Permark({ +// Example 3: Using the Permask class with different bits configuration +const tinyPermissions = new Permask({ permissions: { READ_ONLY: 1, // 0b01 FULL: 3 // 0b11 diff --git a/src/index.ts b/src/index.ts index 2d866b7..1472224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from "./permask"; export * from "./utils/bitmask"; export * from "./constants/permission"; export * from "./integrations/express"; +export * from "./permask-class"; \ No newline at end of file diff --git a/test/permark.test.ts b/src/permask-class.test.ts similarity index 55% rename from test/permark.test.ts rename to src/permask-class.test.ts index c31f6a3..e1a9ce7 100644 --- a/test/permark.test.ts +++ b/src/permask-class.test.ts @@ -1,14 +1,12 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Permark } from '../src/permask'; -import { PermissionAccess } from '../src/constants/permission'; -import { ACCESS_BITS, ACCESS_MASK } from '../src/constants/bitmask'; +import { Permask } from '../src/permask-class'; -describe('Permark', () => { +describe('Permask', () => { // Default permissions instance for basic tests - let defaultPermark: Permark; + let defaultPermask: Permask; // Custom permissions for advanced tests - let customPermark: Permark<{ + let customPermask: Permask<{ VIEW: number; EDIT: number; DELETE: number; @@ -19,10 +17,10 @@ describe('Permark', () => { beforeEach(() => { // Create a default instance - defaultPermark = new Permark(); + defaultPermask = new Permask(); // Create a custom instance with more bits - customPermark = new Permark({ + customPermask = new Permask({ permissions: { VIEW: 1, // 0b00001 EDIT: 2, // 0b00010 @@ -41,33 +39,33 @@ describe('Permark', () => { describe('Initialization', () => { it('should initialize with default values', () => { - expect(defaultPermark).toBeDefined(); + expect(defaultPermask).toBeDefined(); // Create a permission to verify defaults are working - const permission = defaultPermark.createStandardBitmask({ + const permission = defaultPermask.createStandardBitmask({ group: 1, read: true }); - expect(defaultPermark.canRead(permission)).toBe(true); - expect(defaultPermark.canWrite(permission)).toBe(false); - expect(defaultPermark.getGroup(permission)).toBe(1); + expect(defaultPermask.canRead(permission)).toBe(true); + expect(defaultPermask.canWrite(permission)).toBe(false); + expect(defaultPermask.getGroup(permission)).toBe(1); }); it('should initialize with custom permissions', () => { - expect(customPermark).toBeDefined(); + expect(customPermask).toBeDefined(); // Create a custom permission - const permission = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT']); + const permission = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); // Verify custom permissions work - expect(customPermark.hasPermission(permission, 'VIEW')).toBe(true); - expect(customPermark.hasPermission(permission, 'EDIT')).toBe(true); - expect(customPermark.hasPermission(permission, 'DELETE')).toBe(false); + expect(customPermask.hasPermission(permission, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(permission, 'EDIT')).toBe(true); + expect(customPermask.hasPermission(permission, 'DELETE')).toBe(false); }); it('should throw error for permission values exceeding bit capacity', () => { - expect(() => new Permark({ + expect(() => new Permask({ permissions: { INVALID: 16 }, accessBits: 3 // Only allows values up to 7 })).toThrow(/exceeds the maximum value/); @@ -77,7 +75,7 @@ describe('Permark', () => { describe('Permission Operations', () => { it('should create permissions with the correct bitmask', () => { // Create a permission for documents that allows view and edit - const permission = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT']); + const permission = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); // VIEW (1) + EDIT (2) = 3 // GROUP (1) << 6 = 64 (updated for 6 bits) @@ -85,7 +83,7 @@ describe('Permark', () => { expect(permission).toBe(67); // Parse and verify components - const parsed = customPermark.parse(permission); + const parsed = customPermask.parse(permission); expect(parsed.group).toBe(1); expect(parsed.groupName).toBe('DOCUMENTS'); expect(parsed.permissions.VIEW).toBe(true); @@ -95,88 +93,88 @@ describe('Permark', () => { it('should add permissions correctly', () => { // Create a base permission - const base = customPermark.create('DOCUMENTS', ['VIEW']); + const base = customPermask.create('DOCUMENTS', ['VIEW']); // Add EDIT permission - const withEdit = customPermark.addPermission(base, 'EDIT'); + const withEdit = customPermask.addPermission(base, 'EDIT'); - expect(customPermark.hasPermission(withEdit, 'VIEW')).toBe(true); - expect(customPermark.hasPermission(withEdit, 'EDIT')).toBe(true); - expect(customPermark.getGroup(withEdit)).toBe(1); // Group should be unchanged + expect(customPermask.hasPermission(withEdit, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(withEdit, 'EDIT')).toBe(true); + expect(customPermask.getGroup(withEdit)).toBe(1); // Group should be unchanged }); it('should remove permissions correctly', () => { // Create a permission with multiple accesses - const fullAccess = customPermark.create('DOCUMENTS', ['VIEW', 'EDIT', 'DELETE']); + const fullAccess = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT', 'DELETE']); // Remove the EDIT permission - const reducedAccess = customPermark.removePermission(fullAccess, 'EDIT'); + const reducedAccess = customPermask.removePermission(fullAccess, 'EDIT'); - expect(customPermark.hasPermission(reducedAccess, 'VIEW')).toBe(true); - expect(customPermark.hasPermission(reducedAccess, 'EDIT')).toBe(false); - expect(customPermark.hasPermission(reducedAccess, 'DELETE')).toBe(true); + expect(customPermask.hasPermission(reducedAccess, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(reducedAccess, 'EDIT')).toBe(false); + expect(customPermask.hasPermission(reducedAccess, 'DELETE')).toBe(true); }); it('should check permissions correctly', () => { - const permission = customPermark.create('PHOTOS', ['VIEW', 'SHARE']); + const permission = customPermask.create('PHOTOS', ['VIEW', 'SHARE']); - expect(customPermark.hasPermission(permission, 'VIEW')).toBe(true); - expect(customPermark.hasPermission(permission, 'SHARE')).toBe(true); - expect(customPermark.hasPermission(permission, 'EDIT')).toBe(false); - expect(customPermark.hasPermission(permission, 'DELETE')).toBe(false); - expect(customPermark.hasPermission(permission, 'PRINT')).toBe(false); + expect(customPermask.hasPermission(permission, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(permission, 'SHARE')).toBe(true); + expect(customPermask.hasPermission(permission, 'EDIT')).toBe(false); + expect(customPermask.hasPermission(permission, 'DELETE')).toBe(false); + expect(customPermask.hasPermission(permission, 'PRINT')).toBe(false); }); }); describe('Group Operations', () => { it('should get group value correctly', () => { - const permission = customPermark.create('VIDEOS', ['VIEW']); - expect(customPermark.getGroup(permission)).toBe(3); + const permission = customPermask.create('VIDEOS', ['VIEW']); + expect(customPermask.getGroup(permission)).toBe(3); }); it('should get group name correctly', () => { - const permission = customPermark.create('PHOTOS', ['VIEW']); - expect(customPermark.getGroupName(permission)).toBe('PHOTOS'); + const permission = customPermask.create('PHOTOS', ['VIEW']); + expect(customPermask.getGroupName(permission)).toBe('PHOTOS'); }); it('should check group membership correctly', () => { - const permission = customPermark.create('DOCUMENTS', ['VIEW']); + const permission = customPermask.create('DOCUMENTS', ['VIEW']); - expect(customPermark.hasGroup(permission, 'DOCUMENTS')).toBe(true); - expect(customPermark.hasGroup(permission, 'PHOTOS')).toBe(false); - expect(customPermark.hasGroup(permission, 1)).toBe(true); - expect(customPermark.hasGroup(permission, 2)).toBe(false); + expect(customPermask.hasGroup(permission, 'DOCUMENTS')).toBe(true); + expect(customPermask.hasGroup(permission, 'PHOTOS')).toBe(false); + expect(customPermask.hasGroup(permission, 1)).toBe(true); + expect(customPermask.hasGroup(permission, 2)).toBe(false); }); it('should handle unknown group names gracefully', () => { - const permission = customPermark.create(999, ['VIEW']); - expect(customPermark.getGroupName(permission)).toBeUndefined(); + const permission = customPermask.create(999, ['VIEW']); + expect(customPermask.getGroupName(permission)).toBeUndefined(); // Creating with non-existent string group should use group 0 - const noGroup = customPermark.create('NON_EXISTENT', ['VIEW']); - expect(customPermark.getGroup(noGroup)).toBe(0); + const noGroup = customPermask.create('NON_EXISTENT', ['VIEW']); + expect(customPermask.getGroup(noGroup)).toBe(0); }); }); describe('Standard Bitmask Compatibility', () => { it('should create standard bitmasks correctly', () => { - const permission = defaultPermark.createStandardBitmask({ + const permission = defaultPermask.createStandardBitmask({ group: 5, read: true, write: true, delete: false }); - expect(defaultPermark.canRead(permission)).toBe(true); - expect(defaultPermark.canWrite(permission)).toBe(true); - expect(defaultPermark.canDelete(permission)).toBe(false); - expect(defaultPermark.getGroup(permission)).toBe(5); + expect(defaultPermask.canRead(permission)).toBe(true); + expect(defaultPermask.canWrite(permission)).toBe(true); + expect(defaultPermask.canDelete(permission)).toBe(false); + expect(defaultPermask.getGroup(permission)).toBe(5); }); it('should work with named groups', () => { - customPermark.registerGroup('SPREADSHEETS', 10); + customPermask.registerGroup('SPREADSHEETS', 10); - const permission = customPermark.createStandardBitmask({ + const permission = customPermask.createStandardBitmask({ group: 'SPREADSHEETS', read: true, write: true, @@ -184,47 +182,47 @@ describe('Permark', () => { customPermissions: ['SHARE'] }); - expect(customPermark.getGroup(permission)).toBe(10); - expect(customPermark.canRead(permission)).toBe(true); - expect(customPermark.canWrite(permission)).toBe(true); - expect(customPermark.hasPermission(permission, 'SHARE')).toBe(true); + expect(customPermask.getGroup(permission)).toBe(10); + expect(customPermask.canRead(permission)).toBe(true); + expect(customPermask.canWrite(permission)).toBe(true); + expect(customPermask.hasPermission(permission, 'SHARE')).toBe(true); }); }); describe('Dynamic Registration', () => { it('should register new permissions at runtime', () => { // Register a new permission - changed from 32 to 32 which fits in 6 bits - customPermark.registerPermission('ARCHIVE', 32); + customPermask.registerPermission('ARCHIVE', 32); // Create permission using the new type - const permission = customPermark.create('DOCUMENTS', ['VIEW', 'ARCHIVE']); + const permission = customPermask.create('DOCUMENTS', ['VIEW', 'ARCHIVE']); - expect(customPermark.hasPermission(permission, 'ARCHIVE')).toBe(true); - expect(customPermark.parse(permission).permissions['ARCHIVE']).toBe(true); + expect(customPermask.hasPermission(permission, 'ARCHIVE')).toBe(true); + expect(customPermask.parse(permission).permissions['ARCHIVE']).toBe(true); }); it('should throw error when registering permissions that exceed bit capacity', () => { // Should test with a value that definitely exceeds the capacity (now 6 bits = max 63) - expect(() => customPermark.registerPermission('OVERFLOW', 64)).toThrow(/exceeds the maximum value/); + expect(() => customPermask.registerPermission('OVERFLOW', 64)).toThrow(/exceeds the maximum value/); }); it('should register new groups at runtime', () => { // Register a new group - customPermark.registerGroup('ARCHIVES', 20); + customPermask.registerGroup('ARCHIVES', 20); // Create permission for the new group - const permission = customPermark.create('ARCHIVES', ['VIEW']); + const permission = customPermask.create('ARCHIVES', ['VIEW']); - expect(customPermark.getGroup(permission)).toBe(20); - expect(customPermark.getGroupName(permission)).toBe('ARCHIVES'); + expect(customPermask.getGroup(permission)).toBe(20); + expect(customPermask.getGroupName(permission)).toBe('ARCHIVES'); }); }); describe('Parse Functionality', () => { it('should parse bitmasks to detailed objects', () => { - const permission = customPermark.create('VIDEOS', ['VIEW', 'EDIT', 'SHARE']); + const permission = customPermask.create('VIDEOS', ['VIEW', 'EDIT', 'SHARE']); - const parsed = customPermark.parse(permission); + const parsed = customPermask.parse(permission); expect(parsed).toEqual({ group: 3, @@ -241,9 +239,9 @@ describe('Permark', () => { it('should handle parsing bitmasks with unknown groups', () => { // Create a permission with a group that doesn't have a name - const permission = customPermark.create(42, ['VIEW']); + const permission = customPermask.create(42, ['VIEW']); - const parsed = customPermark.parse(permission); + const parsed = customPermask.parse(permission); expect(parsed.group).toBe(42); expect(parsed.groupName).toBeUndefined(); @@ -255,40 +253,40 @@ describe('Permark', () => { it('should handle custom access mask', () => { // Create a completely separate instance with its own configuration // to avoid interference with other tests - const customMaskPermark = new Permark({ + const customMaskPermask = new Permask({ permissions: { TEST: 1, TEST2: 2 }, // Start with minimal permissions accessBits: 4, accessMask: 0b1111 // Explicitly set mask }); // Register a permission that's at the limit - expect(() => customMaskPermark.registerPermission('MAX', 15)).not.toThrow(); + expect(() => customMaskPermask.registerPermission('MAX', 15)).not.toThrow(); // Register a permission that exceeds the limit - expect(() => customMaskPermark.registerPermission('OVER', 16)).toThrow(/exceeds the maximum value/); + expect(() => customMaskPermask.registerPermission('OVER', 16)).toThrow(/exceeds the maximum value/); }); it('should handle the default values correctly', () => { - const permission = defaultPermark.createStandardBitmask({ + const permission = defaultPermask.createStandardBitmask({ group: 1, read: true }); // Instead of assuming specific values, check the functional behavior - expect(defaultPermark.canRead(permission)).toBe(true); - expect(defaultPermark.canWrite(permission)).toBe(false); - expect(defaultPermark.getGroup(permission)).toBe(1); + expect(defaultPermask.canRead(permission)).toBe(true); + expect(defaultPermask.canWrite(permission)).toBe(false); + expect(defaultPermask.getGroup(permission)).toBe(1); }); }); describe('Edge Cases', () => { // Create a separate instance for edge case tests to avoid interference - let edgeCasePermark: Permark<{ + let edgeCasePermask: Permask<{ VIEW: number; [key: string]: number; }>; beforeEach(() => { - edgeCasePermark = new Permark({ + edgeCasePermask = new Permask({ permissions: { VIEW: 1 }, @@ -300,32 +298,32 @@ describe('Permark', () => { }); it('should handle group 0 correctly', () => { - const permission = edgeCasePermark.create(0, ['VIEW']); - expect(edgeCasePermark.getGroup(permission)).toBe(0); + const permission = edgeCasePermask.create(0, ['VIEW']); + expect(edgeCasePermask.getGroup(permission)).toBe(0); }); it('should handle permission value 0 correctly', () => { - edgeCasePermark.registerPermission('NO_ACCESS', 0); + edgeCasePermask.registerPermission('NO_ACCESS', 0); - const permission = edgeCasePermark.create('DOCUMENTS', ['NO_ACCESS']); + const permission = edgeCasePermask.create('DOCUMENTS', ['NO_ACCESS']); // Creating with no actual permission bits results in 0 access - expect(edgeCasePermark.hasPermission(permission, 'VIEW')).toBe(false); - expect(edgeCasePermark.hasPermission(permission, 'NO_ACCESS')).toBe(false); // 0 & anything = 0 + expect(edgeCasePermask.hasPermission(permission, 'VIEW')).toBe(false); + expect(edgeCasePermask.hasPermission(permission, 'NO_ACCESS')).toBe(false); // 0 & anything = 0 }); it('should handle attempting to use undefined permissions', () => { - const permission = edgeCasePermark.create('DOCUMENTS', ['VIEW']); + const permission = edgeCasePermask.create('DOCUMENTS', ['VIEW']); // @ts-ignore - Intentionally testing with a non-existent permission - expect(edgeCasePermark.hasPermission(permission, 'NON_EXISTENT')).toBe(false); + expect(edgeCasePermask.hasPermission(permission, 'NON_EXISTENT')).toBe(false); // @ts-ignore - Intentionally testing with a non-existent permission - const updatedPermission = edgeCasePermark.addPermission(permission, 'NON_EXISTENT'); + const updatedPermission = edgeCasePermask.addPermission(permission, 'NON_EXISTENT'); // The permission should be unchanged since NON_EXISTENT maps to value 0 - expect(edgeCasePermark.hasPermission(updatedPermission, 'VIEW')).toBe(true); - expect(edgeCasePermark.getGroup(updatedPermission)).toBe(1); + expect(edgeCasePermask.hasPermission(updatedPermission, 'VIEW')).toBe(true); + expect(edgeCasePermask.getGroup(updatedPermission)).toBe(1); }); }); }); diff --git a/src/permask-class.ts b/src/permask-class.ts new file mode 100644 index 0000000..deab3a3 --- /dev/null +++ b/src/permask-class.ts @@ -0,0 +1,219 @@ +import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; +import { PermissionAccess } from "./constants/permission"; +import { + canDelete, + canRead, + canWrite, + hasPermissionAccess +} from "./utils/bitmask"; + +/** + * Flexible permission management system that allows custom permission definitions, + * bit allocations and groups. + */ +export class Permask = Record> { + private permissions: T; + private accessBits: number; + private accessMask: number; + private groups: Record; + + /** + * Create a custom permission system + * @param options Configuration options for the permission system + */ + constructor(options: { + permissions?: T; // Custom permission types with bit values + accessBits?: number; // Number of bits allocated for permissions + accessMask?: number; // Mask for access bits + groups?: Record; // Custom groups + } = {}) { + // Use the provided values or fall back to constants + this.accessBits = options.accessBits || ACCESS_BITS; + + // For accessMask, use provided value, or calculate based on custom bits, or use the constant + if (options.accessMask !== undefined) { + this.accessMask = options.accessMask; + } else if (options.accessBits !== undefined) { + // Only recalculate if custom bits provided + this.accessMask = (1 << this.accessBits) - 1; + } else { + // Otherwise use the predefined constant + this.accessMask = ACCESS_MASK; + } + + this.permissions = (options.permissions || PermissionAccess as unknown as T); + this.groups = options.groups || {}; + + // Validate that permissions fit within specified bits + const maxValue = this.accessMask; + for (const [key, value] of Object.entries(this.permissions)) { + if (value > maxValue) { + throw new Error(`Permission '${key}' value ${value} exceeds the maximum value ${maxValue} for ${this.accessBits} bits`); + } + } + } + + /** + * Create a bitmask with the specified permissions and group + */ + create(group: number | string, permissionList: (keyof T)[]): number { + let access = 0; + for (const permission of permissionList) { + access |= this.permissions[permission as string] || 0; + } + + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return (groupValue << this.accessBits) | access; + } + + /** + * Check if a bitmask has a specific permission + */ + hasPermission(bitmask: number, permission: keyof T | string): boolean { + const access = bitmask & this.accessMask; + const permValue = (this.permissions as Record)[permission as string] || 0; + return (access & permValue) !== 0; + } + + /** + * Get permission group from bitmask + */ + getGroup(bitmask: number): number { + return bitmask >> this.accessBits; + } + + /** + * Get group name by group value + */ + getGroupName(bitmask: number): string | undefined { + const groupValue = this.getGroup(bitmask); + const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); + return entry?.[0]; + } + + /** + * Check if bitmask has the specified group + */ + hasGroup(bitmask: number, group: number | string): boolean { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return this.getGroup(bitmask) === groupValue; + } + + /** + * Add permission to existing bitmask without changing the group + */ + addPermission(bitmask: number, permission: keyof T | string): number { + const group = this.getGroup(bitmask); + const permValue = (this.permissions as Record)[permission as string] || 0; + const access = (bitmask & this.accessMask) | permValue; + return (group << this.accessBits) | access; + } + + /** + * Remove permission from existing bitmask + */ + removePermission(bitmask: number, permission: keyof T | string): number { + const group = this.getGroup(bitmask); + const permValue = (this.permissions as Record)[permission as string] || 0; + const access = (bitmask & this.accessMask) & ~permValue; + return (group << this.accessBits) | access; + } + + /** + * Parse bitmask into an object with group and permissions + */ + parse(bitmask: number): { + group: number; + groupName?: string; + permissions: Partial>; + } { + const group = this.getGroup(bitmask); + const permissions = {} as Partial>; + + for (const [key, value] of Object.entries(this.permissions)) { + permissions[key as keyof T] = this.hasPermission(bitmask, key as keyof T); + } + + return { + group, + groupName: this.getGroupName(bitmask), + permissions + }; + } + + /** + * Register a new permission type (use with caution) + */ + registerPermission(name: string, bitValue: number): void { + if (bitValue > this.accessMask) { + throw new Error(`Permission value ${bitValue} exceeds the maximum value ${this.accessMask} for ${this.accessBits} bits`); + } + + (this.permissions as Record)[name] = bitValue; + } + + /** + * Register a new group + */ + registerGroup(name: string, value: number): void { + this.groups[name] = value; + } + + /** + * Create compatible bitmasks that can be used with the standard utils functions + */ + createStandardBitmask({ + group, + read = false, + write = false, + delete: del = false, + customPermissions = [] + }: { + group: number | string; + read?: boolean; + write?: boolean; + delete?: boolean; + customPermissions?: (keyof T | string)[]; + }): number { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + + // Create our own bitmask instead of using the utility function that uses ACCESS_BITS + let bitmask = 0; + if (read) bitmask |= PermissionAccess.READ; + if (write) bitmask |= PermissionAccess.WRITE; + if (del) bitmask |= PermissionAccess.DELETE; + + // Set group using our accessBits + bitmask |= (groupValue << this.accessBits); + + // Add any custom permissions + for (const permission of customPermissions) { + const permValue = (this.permissions as Record)[permission as string]; + if (permValue !== undefined) { + // Get the current access part without changing the group + const currentAccess = bitmask & this.accessMask; + // Add the new permission + const newAccess = currentAccess | permValue; + // Replace the access part in the result + bitmask = (bitmask & ~this.accessMask) | newAccess; + } + } + + return bitmask; + } + + /** + * Compatibility methods with the standard utils + */ + canRead(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.READ); + } + + canWrite(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.WRITE); + } + + canDelete(bitmask: number): boolean { + return hasPermissionAccess(bitmask, PermissionAccess.DELETE); + } +} diff --git a/src/permask.ts b/src/permask.ts index 5abeb21..1a76742 100644 --- a/src/permask.ts +++ b/src/permask.ts @@ -1,6 +1,4 @@ import type { EnumOrObjectType, StringKeysType } from "./types/utils"; -import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; -import { PermissionAccess } from "./constants/permission"; import { canDelete, canRead, @@ -8,11 +6,7 @@ import { createBitmask, getPermissionGroup, hasPermissionGroup, - parseBitmask, - addPermissionAccess, - setPermissionGroup, - hasPermissionAccess, - getPermissionAccess + parseBitmask } from "./utils/bitmask"; /** @@ -45,211 +39,4 @@ export function createPermask< canWrite, canDelete }; -} - -export class Permark = Record> { - private permissions: T; - private accessBits: number; - private accessMask: number; - private groups: Record; - - /** - * Create a custom permission system - * @param options Configuration options for the permission system - */ - constructor(options: { - permissions?: T; // Custom permission types with bit values - accessBits?: number; // Number of bits allocated for permissions - accessMask?: number; // Mask for access bits - groups?: Record; // Custom groups - } = {}) { - // Use the provided values or fall back to constants - this.accessBits = options.accessBits || ACCESS_BITS; - - // For accessMask, use provided value, or calculate based on custom bits, or use the constant - if (options.accessMask !== undefined) { - this.accessMask = options.accessMask; - } else if (options.accessBits !== undefined) { - // Only recalculate if custom bits provided - this.accessMask = (1 << this.accessBits) - 1; - } else { - // Otherwise use the predefined constant - this.accessMask = ACCESS_MASK; - } - - this.permissions = (options.permissions || PermissionAccess as unknown as T); - this.groups = options.groups || {}; - - // Validate that permissions fit within specified bits - const maxValue = this.accessMask; - for (const [key, value] of Object.entries(this.permissions)) { - if (value > maxValue) { - throw new Error(`Permission '${key}' value ${value} exceeds the maximum value ${maxValue} for ${this.accessBits} bits`); - } - } - } - - /** - * Create a bitmask with the specified permissions and group - */ - create(group: number | string, permissionList: (keyof T)[]): number { - let access = 0; - for (const permission of permissionList) { - access |= this.permissions[permission as string] || 0; - } - - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return (groupValue << this.accessBits) | access; - } - - /** - * Check if a bitmask has a specific permission - */ - hasPermission(bitmask: number, permission: keyof T): boolean { - const access = bitmask & this.accessMask; - const permValue = (this.permissions as Record)[permission as string] || 0; - return (access & permValue) !== 0; - } - - /** - * Get permission group from bitmask - */ - getGroup(bitmask: number): number { - return bitmask >> this.accessBits; - } - - /** - * Get group name by group value - */ - getGroupName(bitmask: number): string | undefined { - const groupValue = this.getGroup(bitmask); - const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); - return entry?.[0]; - } - - /** - * Check if bitmask has the specified group - */ - hasGroup(bitmask: number, group: number | string): boolean { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return this.getGroup(bitmask) === groupValue; - } - - /** - * Add permission to existing bitmask without changing the group - */ - addPermission(bitmask: number, permission: keyof T): number { - const group = this.getGroup(bitmask); - const permValue = (this.permissions as Record)[permission as string] || 0; - const access = (bitmask & this.accessMask) | permValue; - return (group << this.accessBits) | access; - } - - /** - * Remove permission from existing bitmask - */ - removePermission(bitmask: number, permission: keyof T): number { - const group = this.getGroup(bitmask); - const permValue = (this.permissions as Record)[permission as string] || 0; - const access = (bitmask & this.accessMask) & ~permValue; - return (group << this.accessBits) | access; - } - - /** - * Parse bitmask into an object with group and permissions - */ - parse(bitmask: number): { - group: number; - groupName?: string; - permissions: Partial>; - } { - const group = this.getGroup(bitmask); - const permissions = {} as Partial>; - - for (const [key, value] of Object.entries(this.permissions)) { - permissions[key as keyof T] = this.hasPermission(bitmask, key as keyof T); - } - - return { - group, - groupName: this.getGroupName(bitmask), - permissions - }; - } - - /** - * Register a new permission type (use with caution) - */ - registerPermission(name: string, bitValue: number): void { - if (bitValue > this.accessMask) { - throw new Error(`Permission value ${bitValue} exceeds the maximum value ${this.accessMask} for ${this.accessBits} bits`); - } - - (this.permissions as Record)[name] = bitValue; - } - - /** - * Register a new group - */ - registerGroup(name: string, value: number): void { - this.groups[name] = value; - } - - /** - * Create compatible bitmasks that can be used with the standard utils functions - */ - createStandardBitmask({ - group, - read = false, - write = false, - delete: del = false, - customPermissions = [] - }: { - group: number | string; - read?: boolean; - write?: boolean; - delete?: boolean; - customPermissions?: (keyof T)[]; - }): number { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - - // Create our own bitmask instead of using the utility function that uses ACCESS_BITS - let bitmask = 0; - if (read) bitmask |= PermissionAccess.READ; - if (write) bitmask |= PermissionAccess.WRITE; - if (del) bitmask |= PermissionAccess.DELETE; - - // Set group using our accessBits - bitmask |= (groupValue << this.accessBits); - - // Add any custom permissions - for (const permission of customPermissions) { - const permValue = (this.permissions as Record)[permission as string]; - if (permValue !== undefined) { - // Get the current access part without changing the group - const currentAccess = bitmask & this.accessMask; - // Add the new permission - const newAccess = currentAccess | permValue; - // Replace the access part in the result - bitmask = (bitmask & ~this.accessMask) | newAccess; - } - } - - return bitmask; - } - - /** - * Compatibility methods with the standard utils - */ - canRead(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.READ); - } - - canWrite(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.WRITE); - } - - canDelete(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.DELETE); - } } \ No newline at end of file From 2ce4bcc77dfe821bad8c69677318a1b8b6ce8cdf Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 08:46:26 +0300 Subject: [PATCH 03/25] refactor: update Permask class to enforce stricter permission types and improve type safety --- src/permask-class.test.ts | 12 ++++++++---- src/permask-class.ts | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index e1a9ce7..28ee068 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -3,7 +3,11 @@ import { Permask } from '../src/permask-class'; describe('Permask', () => { // Default permissions instance for basic tests - let defaultPermask: Permask; + let defaultPermask: Permask<{ + READ: number; + WRITE: number; + DELETE: number; + }>; // Custom permissions for advanced tests let customPermask: Permask<{ @@ -12,7 +16,7 @@ describe('Permask', () => { DELETE: number; SHARE: number; PRINT: number; - [key: string]: number; + ARCHIVE?: number; }>; beforeEach(() => { @@ -26,8 +30,8 @@ describe('Permask', () => { EDIT: 2, // 0b00010 DELETE: 4, // 0b00100 SHARE: 8, // 0b01000 - PRINT: 16 // 0b10000 - }, + PRINT: 16, // 0b10000 + } as const, accessBits: 6, // Changed from 5 to 6 to accommodate values up to 63 groups: { DOCUMENTS: 1, diff --git a/src/permask-class.ts b/src/permask-class.ts index deab3a3..33eb768 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -69,7 +69,7 @@ export class Permask = Record> /** * Check if a bitmask has a specific permission */ - hasPermission(bitmask: number, permission: keyof T | string): boolean { + hasPermission(bitmask: number, permission: keyof T): boolean { const access = bitmask & this.accessMask; const permValue = (this.permissions as Record)[permission as string] || 0; return (access & permValue) !== 0; @@ -102,7 +102,7 @@ export class Permask = Record> /** * Add permission to existing bitmask without changing the group */ - addPermission(bitmask: number, permission: keyof T | string): number { + addPermission(bitmask: number, permission: keyof T): number { const group = this.getGroup(bitmask); const permValue = (this.permissions as Record)[permission as string] || 0; const access = (bitmask & this.accessMask) | permValue; @@ -112,7 +112,7 @@ export class Permask = Record> /** * Remove permission from existing bitmask */ - removePermission(bitmask: number, permission: keyof T | string): number { + removePermission(bitmask: number, permission: keyof T): number { const group = this.getGroup(bitmask); const permValue = (this.permissions as Record)[permission as string] || 0; const access = (bitmask & this.accessMask) & ~permValue; @@ -173,7 +173,7 @@ export class Permask = Record> read?: boolean; write?: boolean; delete?: boolean; - customPermissions?: (keyof T | string)[]; + customPermissions?: (keyof T)[]; }): number { const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; From 3950c74fb0a26bdff930f2bd293dc1245bb28c61 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 11:59:53 +0300 Subject: [PATCH 04/25] refactor: simplify permission check methods in Permask class by using a unified hasPermission method --- src/permask-class.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index 33eb768..c6aee76 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,11 +1,5 @@ import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; import { PermissionAccess } from "./constants/permission"; -import { - canDelete, - canRead, - canWrite, - hasPermissionAccess -} from "./utils/bitmask"; /** * Flexible permission management system that allows custom permission definitions, @@ -206,14 +200,17 @@ export class Permask = Record> * Compatibility methods with the standard utils */ canRead(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.READ); + const access = bitmask & this.accessMask; + return (access & PermissionAccess.READ) !== 0; } canWrite(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.WRITE); + const access = bitmask & this.accessMask; + return (access & PermissionAccess.WRITE) !== 0; } canDelete(bitmask: number): boolean { - return hasPermissionAccess(bitmask, PermissionAccess.DELETE); + const access = bitmask & this.accessMask; + return (access & PermissionAccess.DELETE) !== 0; } } From 58acb581e14fbc2ceaec74edd93e720c7d51e5f4 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 12:26:04 +0300 Subject: [PATCH 05/25] test: add comprehensive tests for permission helper methods in Permask class --- src/permask-class.test.ts | 148 ++++++++++++++++++++++++++- src/permask-class.ts | 210 +++++++++++++++++++++++++++++++++++++- 2 files changed, 356 insertions(+), 2 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 28ee068..7961304 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Permask } from '../src/permask-class'; +import { ALL_PERMISSIONS, Permask } from '../src/permask-class'; describe('Permask', () => { // Default permissions instance for basic tests @@ -330,4 +330,150 @@ describe('Permask', () => { expect(edgeCasePermask.getGroup(updatedPermission)).toBe(1); }); }); + + describe('Permission Helper Methods', () => { + it('should return the full access value', () => { + // For customPermask with 6 bits, the full value should be 63 (2^6 - 1) + expect(customPermask.getFullAccessValue()).toBe(63); + + // For defaultPermask, it should match the standard ACCESS_MASK + expect(defaultPermask.getFullAccessValue()).toBe(7); // 3 bits = 2^3 - 1 = 7 + }); + + it('should combine permissions correctly', () => { + // VIEW (1) + EDIT (2) = 3 + const combined = customPermask.combinePermissions(['VIEW', 'EDIT']); + expect(combined).toBe(3); + + // Empty list should give 0 + expect(customPermask.combinePermissions([])).toBe(0); + + // All permissions combined should equal the full access mask + const allPerms = customPermask.combinePermissions(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']); + expect(allPerms).toBe(31); // 1+2+4+8+16 = 31 + }); + + it('should display permission values correctly', () => { + const values = customPermask.getPermissionValues(); + + expect(values.VIEW.value).toBe(1); + expect(values.VIEW.binaryValue).toBe('000001'); // 6-bit padding + + expect(values.EDIT.value).toBe(2); + expect(values.EDIT.binaryValue).toBe('000010'); + + expect(values.FULL_ACCESS.value).toBe(63); + expect(values.FULL_ACCESS.binaryValue).toBe('111111'); + }); + }); + + describe('All Permissions Methods', () => { + it('should create a bitmask with all permissions', () => { + const allPermissions = customPermask.createAllPermissions('DOCUMENTS'); + + // Group 1 << 6 bits = 64, plus all permission bits (63) + expect(allPermissions).toBe(127); + + // Should have all individual permissions + expect(customPermask.hasPermission(allPermissions, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(allPermissions, 'EDIT')).toBe(true); + expect(customPermask.hasPermission(allPermissions, 'DELETE')).toBe(true); + expect(customPermask.hasPermission(allPermissions, 'SHARE')).toBe(true); + expect(customPermask.hasPermission(allPermissions, 'PRINT')).toBe(true); + + // Should be identified as having all permissions + expect(customPermask.hasAllPermissions(allPermissions)).toBe(true); + }); + + it('should add all permissions to existing bitmask', () => { + const limited = customPermask.create('DOCUMENTS', ['VIEW']); + const upgraded = customPermask.addAllPermissions(limited); + + expect(customPermask.hasAllPermissions(upgraded)).toBe(true); + expect(customPermask.getGroup(upgraded)).toBe(1); // Group unchanged + }); + + it('should correctly identify partial permissions', () => { + const partial = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); + + expect(customPermask.hasAllPermissions(partial)).toBe(false); + expect(customPermask.hasAnyPermission(partial)).toBe(true); + + // Empty permissions bitmask + const empty = customPermask.create('DOCUMENTS', []); + expect(customPermask.hasAnyPermission(empty)).toBe(false); + }); + }); + + describe('Simplified API', () => { + it('should support ALL_PERMISSIONS symbol in create method', () => { + // Create a permission with ALL_PERMISSIONS + const fullAccess = customPermask.create('DOCUMENTS', [ALL_PERMISSIONS]); + + // Should have all permissions set + expect(customPermask.hasPermission(fullAccess, 'VIEW')).toBe(true); + expect(customPermask.hasPermission(fullAccess, 'EDIT')).toBe(true); + expect(customPermask.hasPermission(fullAccess, 'DELETE')).toBe(true); + expect(customPermask.hasPermission(fullAccess, 'SHARE')).toBe(true); + expect(customPermask.hasPermission(fullAccess, 'PRINT')).toBe(true); + expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); + }); + + it('should provide predefined permission combinations', () => { + expect(customPermask.FULL).toBe(63); // 2^6 - 1 + expect(customPermask.NONE).toBe(0); + expect(customPermask.READ_ONLY).toBe(1); // Default READ value + expect(customPermask.READ_WRITE).toBe(3); // READ(1) | WRITE(2) = 3 + }); + + it('should create access with createAccess helper', () => { + // Create full access + const fullAccess = customPermask.createAccess('DOCUMENTS', 'full'); + expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); + expect(customPermask.getGroup(fullAccess)).toBe(1); + + // Create read-only access + const readOnly = customPermask.createAccess('PHOTOS', 'read-only'); + expect(customPermask.canRead(readOnly)).toBe(true); + expect(customPermask.canWrite(readOnly)).toBe(false); + expect(customPermask.getGroup(readOnly)).toBe(2); + + // Create read-write access with custom permissions + const custom = customPermask.createAccess('VIDEOS', 'read-write', ['SHARE']); + expect(customPermask.canRead(custom)).toBe(true); + expect(customPermask.canWrite(custom)).toBe(true); + expect(customPermask.hasPermission(custom, 'SHARE')).toBe(true); + expect(customPermask.hasPermission(custom, 'DELETE')).toBe(false); + expect(customPermask.getGroup(custom)).toBe(3); + }); + + it('should use simplified grant method', () => { + // Read-only access + const readOnly = customPermask.grant({ + group: 'DOCUMENTS', + read: true + }); + expect(customPermask.canRead(readOnly)).toBe(true); + expect(customPermask.canWrite(readOnly)).toBe(false); + + // Full access + const fullAccess = customPermask.grant({ + group: 'PHOTOS', + all: true + }); + expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); + expect(customPermask.getGroup(fullAccess)).toBe(2); + + // Custom access + const customAccess = customPermask.grant({ + group: 'VIDEOS', + read: true, + write: true, + permissions: ['SHARE'] + }); + expect(customPermask.canRead(customAccess)).toBe(true); + expect(customPermask.canWrite(customAccess)).toBe(true); + expect(customPermask.hasPermission(customAccess, 'SHARE')).toBe(true); + }); + }); }); diff --git a/src/permask-class.ts b/src/permask-class.ts index c6aee76..e60b047 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,6 +1,16 @@ import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; import { PermissionAccess } from "./constants/permission"; +/** + * Special permission symbol that represents all permissions + */ +export const ALL_PERMISSIONS = Symbol('ALL_PERMISSIONS'); + +/** + * Type for permission lists that can include the special ALL symbol + */ +export type PermissionList = (keyof T | typeof ALL_PERMISSIONS)[]; + /** * Flexible permission management system that allows custom permission definitions, * bit allocations and groups. @@ -11,6 +21,12 @@ export class Permask = Record> private accessMask: number; private groups: Record; + // Common permission combinations for convenience + readonly FULL: number; + readonly NONE: number; + readonly READ_ONLY: number; + readonly READ_WRITE: number; + /** * Create a custom permission system * @param options Configuration options for the permission system @@ -45,14 +61,32 @@ export class Permask = Record> throw new Error(`Permission '${key}' value ${value} exceeds the maximum value ${maxValue} for ${this.accessBits} bits`); } } + + // Initialize common permission combinations + this.FULL = this.accessMask; + this.NONE = 0; + this.READ_ONLY = PermissionAccess.READ; + this.READ_WRITE = PermissionAccess.READ | PermissionAccess.WRITE; } /** * Create a bitmask with the specified permissions and group + * @example + * // Basic usage + * const permission = permask.create('DOCUMENTS', ['READ', 'WRITE']); + * + * // Grant all permissions + * const fullAccess = permask.create('DOCUMENTS', [ALL_PERMISSIONS]); */ - create(group: number | string, permissionList: (keyof T)[]): number { + create(group: number | string, permissionList: PermissionList): number { + // Check if ALL_PERMISSIONS is in the list + if (permissionList.includes(ALL_PERMISSIONS)) { + return this.createAllPermissions(group); + } + let access = 0; for (const permission of permissionList) { + if (permission === ALL_PERMISSIONS) continue; // Just to be safe access |= this.permissions[permission as string] || 0; } @@ -93,6 +127,31 @@ export class Permask = Record> return this.getGroup(bitmask) === groupValue; } + /** + * Create a bitmask with ALL permissions for a given group + * Acts like a wildcard (*) permission + */ + createAllPermissions(group: number | string): number { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return (groupValue << this.accessBits) | this.accessMask; + } + + /** + * Check if bitmask has ALL permissions + */ + hasAllPermissions(bitmask: number): boolean { + const access = bitmask & this.accessMask; + return access === this.accessMask; + } + + /** + * Check if bitmask has ANY permissions + */ + hasAnyPermission(bitmask: number): boolean { + const access = bitmask & this.accessMask; + return access !== 0; + } + /** * Add permission to existing bitmask without changing the group */ @@ -103,6 +162,14 @@ export class Permask = Record> return (group << this.accessBits) | access; } + /** + * Add ALL permissions to existing bitmask without changing the group + */ + addAllPermissions(bitmask: number): number { + const group = this.getGroup(bitmask); + return (group << this.accessBits) | this.accessMask; + } + /** * Remove permission from existing bitmask */ @@ -213,4 +280,145 @@ export class Permask = Record> const access = bitmask & this.accessMask; return (access & PermissionAccess.DELETE) !== 0; } + + /** + * Get the full permission value (all bits set to 1) + * This is equivalent to having all permissions enabled + */ + getFullAccessValue(): number { + return this.accessMask; + } + + /** + * Combine multiple permissions into a single bitmask value + * Useful for creating custom composite permissions like "FULL" or "READ_WRITE" + */ + combinePermissions(permissionNames: (keyof T)[]): number { + let result = 0; + for (const permission of permissionNames) { + result |= this.permissions[permission as string] || 0; + } + return result; + } + + /** + * Get a map of all permission values for reference + * Helps users understand the bit values of each permission + */ + getPermissionValues(): Record { + const result: Record = {}; + + for (const [key, value] of Object.entries(this.permissions)) { + // Convert to binary string with leading zeros based on bit count + const binaryValue = value.toString(2).padStart(this.accessBits, '0'); + result[key] = { value, binaryValue }; + } + + // Add the FULL access value for reference + result['FULL_ACCESS'] = { + value: this.accessMask, + binaryValue: this.accessMask.toString(2).padStart(this.accessBits, '0') + }; + + return result; + } + + /** + * Create a compatible bitmask that can be used with the standard utils functions + * @example + * // Create read-only access for group 1 + * const readOnly = permask.createAccess(1); + * + * // Create full access for ADMIN group + * const adminAccess = permask.createAccess('ADMIN', 'full'); + * + * // Create read-write access with custom permissions + * const customAccess = permask.createAccess('DOCUMENTS', 'read-write', ['SHARE']); + */ + createAccess( + group: number | string, + accessType: 'full' | 'none' | 'read-only' | 'read-write' = 'read-only', + customPermissions: (keyof T)[] = [] + ): number { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + + // Start with the base access type + let bitmask = 0; + switch (accessType) { + case 'full': + bitmask = this.FULL; + break; + case 'read-write': + bitmask = this.READ_WRITE; + break; + case 'read-only': + bitmask = this.READ_ONLY; + break; + case 'none': + default: + bitmask = this.NONE; + } + + // Add group + bitmask |= (groupValue << this.accessBits); + + // Add any custom permissions + for (const permission of customPermissions) { + const permValue = (this.permissions as Record)[permission as string]; + if (permValue !== undefined) { + // Get the current access part without changing the group + const currentAccess = bitmask & this.accessMask; + // Add the new permission + const newAccess = currentAccess | permValue; + // Replace the access part in the result + bitmask = (bitmask & ~this.accessMask) | newAccess; + } + } + + return bitmask; + } + + // Simplified version of createStandardBitmask + /** + * Create standard permissions with a simpler interface + * @example + * // Read-only access + * const readOnly = permask.grant({ + * group: 'DOCUMENTS', + * read: true + * }); + * + * // Full access + * const fullAccess = permask.grant({ + * group: 'ADMIN', + * all: true + * }); + */ + grant({ + group, + all = false, + read = false, + write = false, + delete: del = false, + permissions = [] + }: { + group: number | string; + all?: boolean; + read?: boolean; + write?: boolean; + delete?: boolean; + permissions?: (keyof T)[]; + }): number { + if (all) { + return this.createAllPermissions(group); + } + + return this.createStandardBitmask({ + group, + read, + write, + delete: del, + customPermissions: permissions + }); + } } From f8cab75f88117b305a2146d560f382d3d000cfb0 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 12:53:11 +0300 Subject: [PATCH 06/25] feat: default add --- src/permask-class.test.ts | 784 ++++++++++++++++++++------------------ src/permask-class.ts | 613 +++++++++++++++-------------- 2 files changed, 754 insertions(+), 643 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 7961304..4c643f2 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -1,239 +1,176 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { ALL_PERMISSIONS, Permask } from '../src/permask-class'; +import { ALL_PERMISSIONS, Permask, PermaskBuilder } from '../src/permask-class'; +import { PermissionAccess } from './constants/permission'; describe('Permask', () => { - // Default permissions instance for basic tests - let defaultPermask: Permask<{ + // Simple permissions for basic tests + let basicPermask: Permask<{ READ: number; WRITE: number; DELETE: number; }>; - // Custom permissions for advanced tests - let customPermask: Permask<{ + // Rich permissions for advanced tests + let richPermask: Permask<{ VIEW: number; EDIT: number; DELETE: number; SHARE: number; PRINT: number; - ARCHIVE?: number; }>; beforeEach(() => { - // Create a default instance - defaultPermask = new Permask(); + // Set up basic permask using builder + basicPermask = new PermaskBuilder<{ + READ: number; + WRITE: number; + DELETE: number; + }>({ + permissions: { + READ: PermissionAccess.READ, // 1 + WRITE: PermissionAccess.WRITE, // 2 + DELETE: PermissionAccess.DELETE // 4 + }, + accessBits: 3, + groups: { + USERS: 1, + ADMINS: 2 + } + }).build(); - // Create a custom instance with more bits - customPermask = new Permask({ + // Set up rich permask using builder with more permissions and groups + richPermask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + }>({ permissions: { - VIEW: 1, // 0b00001 - EDIT: 2, // 0b00010 - DELETE: 4, // 0b00100 - SHARE: 8, // 0b01000 - PRINT: 16, // 0b10000 - } as const, - accessBits: 6, // Changed from 5 to 6 to accommodate values up to 63 + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16, // 0b10000 + }, + accessBits: 6, groups: { DOCUMENTS: 1, PHOTOS: 2, - VIDEOS: 3 + VIDEOS: 3, + SPREADSHEETS: 4 } - }); + }) + .definePermissionSet('VIEWER', ['VIEW']) + .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) + .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) + .definePermissionSet('PUBLISHER', ['VIEW', 'SHARE']) + .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) + .build(); }); - describe('Initialization', () => { - it('should initialize with default values', () => { - expect(defaultPermask).toBeDefined(); - - // Create a permission to verify defaults are working - const permission = defaultPermask.createStandardBitmask({ - group: 1, - read: true - }); + describe('Permission Creation', () => { + it('should create basic permissions with correct bitmask values', () => { + const readPermission = basicPermask.for('USERS').grant(['READ']).value(); + const readWritePermission = basicPermask.for('USERS').grant(['READ', 'WRITE']).value(); - expect(defaultPermask.canRead(permission)).toBe(true); - expect(defaultPermask.canWrite(permission)).toBe(false); - expect(defaultPermask.getGroup(permission)).toBe(1); - }); - - it('should initialize with custom permissions', () => { - expect(customPermask).toBeDefined(); - - // Create a custom permission - const permission = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); + // Group 1 (USERS) << 3 bits = 8, plus READ (1) = 9 + expect(readPermission).toBe(9); - // Verify custom permissions work - expect(customPermask.hasPermission(permission, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(permission, 'EDIT')).toBe(true); - expect(customPermask.hasPermission(permission, 'DELETE')).toBe(false); - }); - - it('should throw error for permission values exceeding bit capacity', () => { - expect(() => new Permask({ - permissions: { INVALID: 16 }, - accessBits: 3 // Only allows values up to 7 - })).toThrow(/exceeds the maximum value/); - }); - }); - - describe('Permission Operations', () => { - it('should create permissions with the correct bitmask', () => { - // Create a permission for documents that allows view and edit - const permission = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); - - // VIEW (1) + EDIT (2) = 3 - // GROUP (1) << 6 = 64 (updated for 6 bits) - // Expected: 64 | 3 = 67 - expect(permission).toBe(67); - - // Parse and verify components - const parsed = customPermask.parse(permission); - expect(parsed.group).toBe(1); - expect(parsed.groupName).toBe('DOCUMENTS'); - expect(parsed.permissions.VIEW).toBe(true); - expect(parsed.permissions.EDIT).toBe(true); - expect(parsed.permissions.DELETE).toBe(false); + // Group 1 (USERS) << 3 bits = 8, plus READ (1) + WRITE (2) = 11 + expect(readWritePermission).toBe(11); }); - it('should add permissions correctly', () => { - // Create a base permission - const base = customPermask.create('DOCUMENTS', ['VIEW']); + it('should create permissions with explicit group IDs', () => { + const permission = richPermask.for(2).grant(['VIEW', 'EDIT']).value(); - // Add EDIT permission - const withEdit = customPermask.addPermission(base, 'EDIT'); - - expect(customPermask.hasPermission(withEdit, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(withEdit, 'EDIT')).toBe(true); - expect(customPermask.getGroup(withEdit)).toBe(1); // Group should be unchanged + // Group 2 (PHOTOS) << 6 bits = 128, plus VIEW (1) + EDIT (2) = 131 + expect(permission).toBe(131); + expect(richPermask.check(permission).group()).toBe(2); + expect(richPermask.check(permission).groupName()).toBe('PHOTOS'); }); - it('should remove permissions correctly', () => { - // Create a permission with multiple accesses - const fullAccess = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT', 'DELETE']); - - // Remove the EDIT permission - const reducedAccess = customPermask.removePermission(fullAccess, 'EDIT'); + it('should handle ALL_PERMISSIONS symbol', () => { + const allPermissions = richPermask.for('DOCUMENTS').grant([ALL_PERMISSIONS]).value(); + const alternateWay = richPermask.for('DOCUMENTS').grantAll().value(); - expect(customPermask.hasPermission(reducedAccess, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(reducedAccess, 'EDIT')).toBe(false); - expect(customPermask.hasPermission(reducedAccess, 'DELETE')).toBe(true); + // All permissions should set all bits (63 for 6 bits) + expect(richPermask.check(allPermissions).canEverything()).toBe(true); + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); + expect(allPermissions).toEqual(alternateWay); }); - it('should check permissions correctly', () => { - const permission = customPermask.create('PHOTOS', ['VIEW', 'SHARE']); + it('should handle permission sets', () => { + const editorPerm = richPermask.for('DOCUMENTS').grantSet('EDITOR').value(); + const managerPerm = richPermask.for('PHOTOS').grantSet('MANAGER').value(); - expect(customPermask.hasPermission(permission, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(permission, 'SHARE')).toBe(true); - expect(customPermask.hasPermission(permission, 'EDIT')).toBe(false); - expect(customPermask.hasPermission(permission, 'DELETE')).toBe(false); - expect(customPermask.hasPermission(permission, 'PRINT')).toBe(false); - }); - }); - - describe('Group Operations', () => { - it('should get group value correctly', () => { - const permission = customPermask.create('VIDEOS', ['VIEW']); - expect(customPermask.getGroup(permission)).toBe(3); - }); - - it('should get group name correctly', () => { - const permission = customPermask.create('PHOTOS', ['VIEW']); - expect(customPermask.getGroupName(permission)).toBe('PHOTOS'); - }); - - it('should check group membership correctly', () => { - const permission = customPermask.create('DOCUMENTS', ['VIEW']); + // Editor has VIEW and EDIT + expect(richPermask.check(editorPerm).can('VIEW')).toBe(true); + expect(richPermask.check(editorPerm).can('EDIT')).toBe(true); + expect(richPermask.check(editorPerm).can('DELETE')).toBe(false); - expect(customPermask.hasGroup(permission, 'DOCUMENTS')).toBe(true); - expect(customPermask.hasGroup(permission, 'PHOTOS')).toBe(false); - expect(customPermask.hasGroup(permission, 1)).toBe(true); - expect(customPermask.hasGroup(permission, 2)).toBe(false); + // Manager has VIEW, EDIT, and DELETE + expect(richPermask.check(managerPerm).can('VIEW')).toBe(true); + expect(richPermask.check(managerPerm).can('EDIT')).toBe(true); + expect(richPermask.check(managerPerm).can('DELETE')).toBe(true); + expect(richPermask.check(managerPerm).can('SHARE')).toBe(false); }); - it('should handle unknown group names gracefully', () => { - const permission = customPermask.create(999, ['VIEW']); - expect(customPermask.getGroupName(permission)).toBeUndefined(); - - // Creating with non-existent string group should use group 0 - const noGroup = customPermask.create('NON_EXISTENT', ['VIEW']); - expect(customPermask.getGroup(noGroup)).toBe(0); + it('should combine permission sets and individual permissions', () => { + // Grant editor set plus SHARE permission + const customPerm = richPermask.for('VIDEOS') + .grantSet('EDITOR') + .grant(['SHARE']) + .value(); + + expect(richPermask.check(customPerm).can('VIEW')).toBe(true); + expect(richPermask.check(customPerm).can('EDIT')).toBe(true); + expect(richPermask.check(customPerm).can('SHARE')).toBe(true); + expect(richPermask.check(customPerm).can('DELETE')).toBe(false); + expect(richPermask.check(customPerm).can('PRINT')).toBe(false); }); }); - describe('Standard Bitmask Compatibility', () => { - it('should create standard bitmasks correctly', () => { - const permission = defaultPermask.createStandardBitmask({ - group: 5, - read: true, - write: true, - delete: false - }); + describe('Permission Checking', () => { + it('should check individual permissions', () => { + const perm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - expect(defaultPermask.canRead(permission)).toBe(true); - expect(defaultPermask.canWrite(permission)).toBe(true); - expect(defaultPermask.canDelete(permission)).toBe(false); - expect(defaultPermask.getGroup(permission)).toBe(5); + expect(richPermask.check(perm).can('VIEW')).toBe(true); + expect(richPermask.check(perm).can('EDIT')).toBe(true); + expect(richPermask.check(perm).can('DELETE')).toBe(false); }); - it('should work with named groups', () => { - customPermask.registerGroup('SPREADSHEETS', 10); - - const permission = customPermask.createStandardBitmask({ - group: 'SPREADSHEETS', - read: true, - write: true, - delete: false, - customPermissions: ['SHARE'] - }); - - expect(customPermask.getGroup(permission)).toBe(10); - expect(customPermask.canRead(permission)).toBe(true); - expect(customPermask.canWrite(permission)).toBe(true); - expect(customPermask.hasPermission(permission, 'SHARE')).toBe(true); - }); - }); - - describe('Dynamic Registration', () => { - it('should register new permissions at runtime', () => { - // Register a new permission - changed from 32 to 32 which fits in 6 bits - customPermask.registerPermission('ARCHIVE', 32); + it('should check multiple permissions at once', () => { + const perm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT', 'SHARE']).value(); - // Create permission using the new type - const permission = customPermask.create('DOCUMENTS', ['VIEW', 'ARCHIVE']); + // All permissions check + expect(richPermask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true); + expect(richPermask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false); - expect(customPermask.hasPermission(permission, 'ARCHIVE')).toBe(true); - expect(customPermask.parse(permission).permissions['ARCHIVE']).toBe(true); - }); - - it('should throw error when registering permissions that exceed bit capacity', () => { - // Should test with a value that definitely exceeds the capacity (now 6 bits = max 63) - expect(() => customPermask.registerPermission('OVERFLOW', 64)).toThrow(/exceeds the maximum value/); + // Any permissions check + expect(richPermask.check(perm).canAny(['DELETE', 'PRINT'])).toBe(false); + expect(richPermask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true); }); - it('should register new groups at runtime', () => { - // Register a new group - customPermask.registerGroup('ARCHIVES', 20); - - // Create permission for the new group - const permission = customPermask.create('ARCHIVES', ['VIEW']); + it('should check for complete permissions', () => { + const partialPerm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const allPerm = richPermask.for('DOCUMENTS').grantAll().value(); - expect(customPermask.getGroup(permission)).toBe(20); - expect(customPermask.getGroupName(permission)).toBe('ARCHIVES'); + expect(richPermask.check(partialPerm).canEverything()).toBe(false); + expect(richPermask.check(allPerm).canEverything()).toBe(true); }); - }); - - describe('Parse Functionality', () => { - it('should parse bitmasks to detailed objects', () => { - const permission = customPermask.create('VIDEOS', ['VIEW', 'EDIT', 'SHARE']); + + it('should provide detailed permission explanation', () => { + const perm = richPermask.for('PHOTOS').grant(['VIEW', 'SHARE']).value(); - const parsed = customPermask.parse(permission); + const details = richPermask.check(perm).explain(); - expect(parsed).toEqual({ - group: 3, - groupName: 'VIDEOS', + expect(details).toEqual({ + group: 2, + groupName: 'PHOTOS', permissions: { VIEW: true, - EDIT: true, + EDIT: false, DELETE: false, SHARE: true, PRINT: false @@ -241,239 +178,364 @@ describe('Permask', () => { }); }); - it('should handle parsing bitmasks with unknown groups', () => { - // Create a permission with a group that doesn't have a name - const permission = customPermask.create(42, ['VIEW']); + it('should check group membership', () => { + const perm = richPermask.for('DOCUMENTS').grant(['VIEW']).value(); - const parsed = customPermask.parse(permission); - - expect(parsed.group).toBe(42); - expect(parsed.groupName).toBeUndefined(); - expect(parsed.permissions.VIEW).toBe(true); + expect(richPermask.check(perm).inGroup('DOCUMENTS')).toBe(true); + expect(richPermask.check(perm).inGroup('PHOTOS')).toBe(false); + expect(richPermask.check(perm).inGroup(1)).toBe(true); }); }); - describe('Advanced Features', () => { - it('should handle custom access mask', () => { - // Create a completely separate instance with its own configuration - // to avoid interference with other tests - const customMaskPermask = new Permask({ - permissions: { TEST: 1, TEST2: 2 }, // Start with minimal permissions - accessBits: 4, - accessMask: 0b1111 // Explicitly set mask - }); + describe('String Representation', () => { + it('should convert permissions to strings', () => { + const perm1 = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm2 = richPermask.for('PHOTOS').grantAll().value(); + const perm3 = richPermask.for('VIDEOS').grant([]).value(); - // Register a permission that's at the limit - expect(() => customMaskPermask.registerPermission('MAX', 15)).not.toThrow(); - // Register a permission that exceeds the limit - expect(() => customMaskPermask.registerPermission('OVER', 16)).toThrow(/exceeds the maximum value/); + expect(richPermask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); + expect(richPermask.toString(perm2)).toBe('PHOTOS:ALL'); + expect(richPermask.toString(perm3)).toBe('VIDEOS:NONE'); }); - it('should handle the default values correctly', () => { - const permission = defaultPermask.createStandardBitmask({ - group: 1, - read: true - }); + it('should parse permission strings', () => { + const perm1 = richPermask.fromString('DOCUMENTS:VIEW,EDIT'); + const perm2 = richPermask.fromString('PHOTOS:ALL'); + const perm3 = richPermask.fromString('VIDEOS:MANAGER'); + const perm4 = richPermask.fromString('5:VIEW,SHARE'); // Numeric group + + expect(richPermask.check(perm1).canAll(['VIEW', 'EDIT'])).toBe(true); + expect(richPermask.check(perm1).can('DELETE')).toBe(false); + + expect(richPermask.check(perm2).canEverything()).toBe(true); + + // Permission set expansion + expect(richPermask.check(perm3).can('VIEW')).toBe(true); + expect(richPermask.check(perm3).can('EDIT')).toBe(true); + expect(richPermask.check(perm3).can('DELETE')).toBe(true); + + // Numeric group handling + expect(richPermask.check(perm4).group()).toBe(5); + expect(richPermask.check(perm4).canAll(['VIEW', 'SHARE'])).toBe(true); + }); + + it('should handle alternative string formats', () => { + const permWithStar = richPermask.fromString('DOCUMENTS:*'); + const permWithEmpty = richPermask.fromString('VIDEOS:'); - // Instead of assuming specific values, check the functional behavior - expect(defaultPermask.canRead(permission)).toBe(true); - expect(defaultPermask.canWrite(permission)).toBe(false); - expect(defaultPermask.getGroup(permission)).toBe(1); + expect(richPermask.check(permWithStar).canEverything()).toBe(true); + expect(richPermask.check(permWithEmpty).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(false); }); }); - - describe('Edge Cases', () => { - // Create a separate instance for edge case tests to avoid interference - let edgeCasePermask: Permask<{ - VIEW: number; - [key: string]: number; - }>; + + describe('Builder Pattern', () => { + it('should extend permissions with builder', () => { + // Create extended version of the permissions system + const extendedPermask = richPermask.toBuilder() + .definePermission('APPROVE', 32) + .defineGroup('REPORTS', 5) + .definePermissionSet('APPROVER', ['VIEW', 'APPROVE']) + .build(); + + // Create permission using new components + const reportPerm = extendedPermask.for('REPORTS').grantSet('APPROVER').value(); + + expect(extendedPermask.check(reportPerm).can('VIEW')).toBe(true); + expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); + expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true); + }); - beforeEach(() => { - edgeCasePermask = new Permask({ - permissions: { - VIEW: 1 - }, - accessBits: 4, // Use a higher bit count for these tests - groups: { - DOCUMENTS: 1 - } - }); + it('should validate permission values', () => { + // Try to add a permission that exceeds bit capacity (6 bits = max 63) + expect(() => { + richPermask.toBuilder() + .definePermission('OVERFLOW', 64) + .build(); + }).toThrow(/exceeds maximum value/); }); - + }); + + describe('Edge Cases', () => { it('should handle group 0 correctly', () => { - const permission = edgeCasePermask.create(0, ['VIEW']); - expect(edgeCasePermask.getGroup(permission)).toBe(0); + const perm = richPermask.for(0).grant(['VIEW']).value(); + + expect(richPermask.check(perm).group()).toBe(0); + expect(richPermask.check(perm).can('VIEW')).toBe(true); }); - it('should handle permission value 0 correctly', () => { - edgeCasePermask.registerPermission('NO_ACCESS', 0); + it('should handle non-existent groups gracefully', () => { + const perm = richPermask.for('NON_EXISTENT').grant(['VIEW']).value(); - const permission = edgeCasePermask.create('DOCUMENTS', ['NO_ACCESS']); - - // Creating with no actual permission bits results in 0 access - expect(edgeCasePermask.hasPermission(permission, 'VIEW')).toBe(false); - expect(edgeCasePermask.hasPermission(permission, 'NO_ACCESS')).toBe(false); // 0 & anything = 0 + expect(richPermask.check(perm).group()).toBe(0); // Default to group 0 + expect(richPermask.check(perm).can('VIEW')).toBe(true); }); - it('should handle attempting to use undefined permissions', () => { - const permission = edgeCasePermask.create('DOCUMENTS', ['VIEW']); - + it('should handle non-existent permissions gracefully', () => { // @ts-ignore - Intentionally testing with a non-existent permission - expect(edgeCasePermask.hasPermission(permission, 'NON_EXISTENT')).toBe(false); + const perm = richPermask.for('DOCUMENTS').grant(['NON_EXISTENT']).value(); - // @ts-ignore - Intentionally testing with a non-existent permission - const updatedPermission = edgeCasePermask.addPermission(permission, 'NON_EXISTENT'); + expect(richPermask.check(perm).group()).toBe(1); // Group should be set + expect(richPermask.check(perm).can('VIEW')).toBe(false); // No permissions granted + }); + + it('should handle empty permission lists', () => { + const perm = richPermask.for('DOCUMENTS').grant([]).value(); - // The permission should be unchanged since NON_EXISTENT maps to value 0 - expect(edgeCasePermask.hasPermission(updatedPermission, 'VIEW')).toBe(true); - expect(edgeCasePermask.getGroup(updatedPermission)).toBe(1); + expect(richPermask.check(perm).group()).toBe(1); + expect(richPermask.check(perm).can('VIEW')).toBe(false); + expect(richPermask.check(perm).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(false); }); }); - describe('Permission Helper Methods', () => { - it('should return the full access value', () => { - // For customPermask with 6 bits, the full value should be 63 (2^6 - 1) - expect(customPermask.getFullAccessValue()).toBe(63); + describe('Permission Sets', () => { + it('should use predefined permission sets', () => { + const viewerPerm = richPermask.for('DOCUMENTS').grantSet('VIEWER').value(); + const editorPerm = richPermask.for('DOCUMENTS').grantSet('EDITOR').value(); + const managerPerm = richPermask.for('DOCUMENTS').grantSet('MANAGER').value(); + const adminPerm = richPermask.for('DOCUMENTS').grantSet('ADMIN').value(); - // For defaultPermask, it should match the standard ACCESS_MASK - expect(defaultPermask.getFullAccessValue()).toBe(7); // 3 bits = 2^3 - 1 = 7 - }); - - it('should combine permissions correctly', () => { - // VIEW (1) + EDIT (2) = 3 - const combined = customPermask.combinePermissions(['VIEW', 'EDIT']); - expect(combined).toBe(3); + // Check viewer permissions + expect(richPermask.check(viewerPerm).can('VIEW')).toBe(true); + expect(richPermask.check(viewerPerm).can('EDIT')).toBe(false); + + // Check editor permissions + expect(richPermask.check(editorPerm).canAll(['VIEW', 'EDIT'])).toBe(true); + expect(richPermask.check(editorPerm).can('DELETE')).toBe(false); - // Empty list should give 0 - expect(customPermask.combinePermissions([])).toBe(0); + // Check manager permissions + expect(richPermask.check(managerPerm).canAll(['VIEW', 'EDIT', 'DELETE'])).toBe(true); + expect(richPermask.check(managerPerm).can('SHARE')).toBe(false); - // All permissions combined should equal the full access mask - const allPerms = customPermask.combinePermissions(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']); - expect(allPerms).toBe(31); // 1+2+4+8+16 = 31 + // Check admin permissions + expect(richPermask.check(adminPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(true); }); - it('should display permission values correctly', () => { - const values = customPermask.getPermissionValues(); + it('should combine sets with additional permissions', () => { + // Grant manager set plus SHARE permission + const customPerm = richPermask.for('DOCUMENTS') + .grantSet('MANAGER') + .grant(['SHARE']) + .value(); - expect(values.VIEW.value).toBe(1); - expect(values.VIEW.binaryValue).toBe('000001'); // 6-bit padding - - expect(values.EDIT.value).toBe(2); - expect(values.EDIT.binaryValue).toBe('000010'); - - expect(values.FULL_ACCESS.value).toBe(63); - expect(values.FULL_ACCESS.binaryValue).toBe('111111'); + expect(richPermask.check(customPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(true); + expect(richPermask.check(customPerm).can('PRINT')).toBe(false); }); }); - describe('All Permissions Methods', () => { - it('should create a bitmask with all permissions', () => { - const allPermissions = customPermask.createAllPermissions('DOCUMENTS'); + describe('Default Constants', () => { + it('should use default access bits and mask when not specified', () => { + // Create a permask without specifying accessBits or accessMask + const defaultPermask = new PermaskBuilder().build(); - // Group 1 << 6 bits = 64, plus all permission bits (63) - expect(allPermissions).toBe(127); + // Verify that the default ACCESS_BITS is 3 + expect(defaultPermask.accessBits).toBe(3); - // Should have all individual permissions - expect(customPermask.hasPermission(allPermissions, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(allPermissions, 'EDIT')).toBe(true); - expect(customPermask.hasPermission(allPermissions, 'DELETE')).toBe(true); - expect(customPermask.hasPermission(allPermissions, 'SHARE')).toBe(true); - expect(customPermask.hasPermission(allPermissions, 'PRINT')).toBe(true); + // Verify that the default ACCESS_MASK is 7 (binary: 111, which is (1 << 3) - 1) + expect(defaultPermask.accessMask).toBe(7); - // Should be identified as having all permissions - expect(customPermask.hasAllPermissions(allPermissions)).toBe(true); + // Verify that all permission bits fit within the mask + // The mask 7 (binary 111) has 3 bits, so 1, 2, and 4 should all fit + const allPerms = defaultPermask.for(1).grantAll().value(); + expect(allPerms & 7).toBe(7); }); - it('should add all permissions to existing bitmask', () => { - const limited = customPermask.create('DOCUMENTS', ['VIEW']); - const upgraded = customPermask.addAllPermissions(limited); + it('should use default permission values when not specified', () => { + // Create a default permask without specifying custom permissions + const defaultPermask = new PermaskBuilder().build(); + + // Add permissions to test default values + const readPerm = defaultPermask.for(1).grant(['READ']).value(); + const writePerm = defaultPermask.for(1).grant(['WRITE']).value(); + const deletePerm = defaultPermask.for(1).grant(['DELETE']).value(); + + // Verify READ permission = 1 + expect(defaultPermask.check(readPerm).can('READ')).toBe(true); + expect(readPerm & 7).toBe(1); + + // Verify WRITE permission = 2 + expect(defaultPermask.check(writePerm).can('WRITE')).toBe(true); + expect(writePerm & 7).toBe(2); + + // Verify DELETE permission = 4 + expect(defaultPermask.check(deletePerm).can('DELETE')).toBe(true); + expect(deletePerm & 7).toBe(4); + + // Verify READ_ONLY preset is same as READ permission + expect(defaultPermask.READ_ONLY).toBe(1); - expect(customPermask.hasAllPermissions(upgraded)).toBe(true); - expect(customPermask.getGroup(upgraded)).toBe(1); // Group unchanged + // Verify READ_WRITE preset is READ | WRITE + expect(defaultPermask.READ_WRITE).toBe(3); }); - it('should correctly identify partial permissions', () => { - const partial = customPermask.create('DOCUMENTS', ['VIEW', 'EDIT']); + it('should calculate access mask correctly for different bit sizes', () => { + // Create permasks with different accessBits to verify mask calculation + const bits4Permask = new PermaskBuilder({ accessBits: 4 }).build(); + const bits5Permask = new PermaskBuilder({ accessBits: 5 }).build(); - expect(customPermask.hasAllPermissions(partial)).toBe(false); - expect(customPermask.hasAnyPermission(partial)).toBe(true); + // 4 bits should give mask 15 (binary: 1111) + expect(bits4Permask.accessMask).toBe(15); - // Empty permissions bitmask - const empty = customPermask.create('DOCUMENTS', []); - expect(customPermask.hasAnyPermission(empty)).toBe(false); + // 5 bits should give mask 31 (binary: 11111) + expect(bits5Permask.accessMask).toBe(31); }); }); +}); - describe('Simplified API', () => { - it('should support ALL_PERMISSIONS symbol in create method', () => { - // Create a permission with ALL_PERMISSIONS - const fullAccess = customPermask.create('DOCUMENTS', [ALL_PERMISSIONS]); - - // Should have all permissions set - expect(customPermask.hasPermission(fullAccess, 'VIEW')).toBe(true); - expect(customPermask.hasPermission(fullAccess, 'EDIT')).toBe(true); - expect(customPermask.hasPermission(fullAccess, 'DELETE')).toBe(true); - expect(customPermask.hasPermission(fullAccess, 'SHARE')).toBe(true); - expect(customPermask.hasPermission(fullAccess, 'PRINT')).toBe(true); - expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); +describe('New Permask API', () => { + let permask: Permask<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + }>; + + beforeEach(() => { + // Create using builder pattern + permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + }>({ + permissions: { + VIEW: 1, + EDIT: 2, + DELETE: 4, + SHARE: 8, + PRINT: 16 + }, + accessBits: 6, + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + VIDEOS: 3 + } + }) + .definePermissionSet('VIEWER', ['VIEW']) + .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) + .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) + .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) + .build(); + }); + + describe('Building Permissions', () => { + it('should create permissions using the fluent API', () => { + // Grant specific permissions + const viewEditPerm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + + expect(permask.check(viewEditPerm).can('VIEW')).toBe(true); + expect(permask.check(viewEditPerm).can('EDIT')).toBe(true); + expect(permask.check(viewEditPerm).can('DELETE')).toBe(false); + expect(permask.check(viewEditPerm).inGroup('DOCUMENTS')).toBe(true); }); - it('should provide predefined permission combinations', () => { - expect(customPermask.FULL).toBe(63); // 2^6 - 1 - expect(customPermask.NONE).toBe(0); - expect(customPermask.READ_ONLY).toBe(1); // Default READ value - expect(customPermask.READ_WRITE).toBe(3); // READ(1) | WRITE(2) = 3 + it('should support permission sets', () => { + // Grant a permission set + const managerPerm = permask.for('PHOTOS').grantSet('MANAGER').value(); + + expect(permask.check(managerPerm).can('VIEW')).toBe(true); + expect(permask.check(managerPerm).can('EDIT')).toBe(true); + expect(permask.check(managerPerm).can('DELETE')).toBe(true); + expect(permask.check(managerPerm).can('SHARE')).toBe(false); + expect(permask.check(managerPerm).inGroup('PHOTOS')).toBe(true); }); - it('should create access with createAccess helper', () => { - // Create full access - const fullAccess = customPermask.createAccess('DOCUMENTS', 'full'); - expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); - expect(customPermask.getGroup(fullAccess)).toBe(1); - - // Create read-only access - const readOnly = customPermask.createAccess('PHOTOS', 'read-only'); - expect(customPermask.canRead(readOnly)).toBe(true); - expect(customPermask.canWrite(readOnly)).toBe(false); - expect(customPermask.getGroup(readOnly)).toBe(2); - - // Create read-write access with custom permissions - const custom = customPermask.createAccess('VIDEOS', 'read-write', ['SHARE']); - expect(customPermask.canRead(custom)).toBe(true); - expect(customPermask.canWrite(custom)).toBe(true); - expect(customPermask.hasPermission(custom, 'SHARE')).toBe(true); - expect(customPermask.hasPermission(custom, 'DELETE')).toBe(false); - expect(customPermask.getGroup(custom)).toBe(3); + it('should handle ALL_PERMISSIONS', () => { + // Grant all permissions + const allPerm = permask.for('VIDEOS').grantAll().value(); + + expect(permask.check(allPerm).canEverything()).toBe(true); + expect(permask.check(allPerm).can('VIEW')).toBe(true); + expect(permask.check(allPerm).can('EDIT')).toBe(true); + expect(permask.check(allPerm).can('DELETE')).toBe(true); + expect(permask.check(allPerm).can('SHARE')).toBe(true); + expect(permask.check(allPerm).can('PRINT')).toBe(true); + }); + }); + + describe('Checking Permissions', () => { + it('should check permissions with the fluent API', () => { + const perm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + + // Individual checks + expect(permask.check(perm).can('VIEW')).toBe(true); + expect(permask.check(perm).can('DELETE')).toBe(false); + + // Check multiple permissions + expect(permask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true); + expect(permask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false); + expect(permask.check(perm).canAny(['DELETE', 'SHARE'])).toBe(false); + expect(permask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true); + + // Group checks + expect(permask.check(perm).inGroup('DOCUMENTS')).toBe(true); + expect(permask.check(perm).inGroup('PHOTOS')).toBe(false); + expect(permask.check(perm).group()).toBe(1); + expect(permask.check(perm).groupName()).toBe('DOCUMENTS'); }); - it('should use simplified grant method', () => { - // Read-only access - const readOnly = customPermask.grant({ - group: 'DOCUMENTS', - read: true - }); - expect(customPermask.canRead(readOnly)).toBe(true); - expect(customPermask.canWrite(readOnly)).toBe(false); + it('should provide detailed explanation of permissions', () => { + const perm = permask.for('PHOTOS').grant(['VIEW', 'EDIT']).value(); - // Full access - const fullAccess = customPermask.grant({ - group: 'PHOTOS', - all: true - }); - expect(customPermask.hasAllPermissions(fullAccess)).toBe(true); - expect(customPermask.getGroup(fullAccess)).toBe(2); - - // Custom access - const customAccess = customPermask.grant({ - group: 'VIDEOS', - read: true, - write: true, - permissions: ['SHARE'] + const details = permask.check(perm).explain(); + + expect(details).toEqual({ + group: 2, + groupName: 'PHOTOS', + permissions: { + VIEW: true, + EDIT: true, + DELETE: false, + SHARE: false, + PRINT: false + } }); - expect(customPermask.canRead(customAccess)).toBe(true); - expect(customPermask.canWrite(customAccess)).toBe(true); - expect(customPermask.hasPermission(customAccess, 'SHARE')).toBe(true); + }); + }); + + describe('String Conversion', () => { + it('should convert permissions to strings', () => { + const perm1 = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm2 = permask.for('PHOTOS').grantAll().value(); + const perm3 = permask.for('VIDEOS').grant([]).value(); + + expect(permask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); + expect(permask.toString(perm2)).toBe('PHOTOS:ALL'); + expect(permask.toString(perm3)).toBe('VIDEOS:NONE'); + }); + + it('should parse permission strings', () => { + const perm1 = permask.fromString('DOCUMENTS:VIEW,EDIT'); + const perm2 = permask.fromString('PHOTOS:ALL'); + const perm3 = permask.fromString('VIDEOS:MANAGER'); + + expect(permask.check(perm1).can('VIEW')).toBe(true); + expect(permask.check(perm1).can('EDIT')).toBe(true); + expect(permask.check(perm1).can('DELETE')).toBe(false); + + expect(permask.check(perm2).canEverything()).toBe(true); + + expect(permask.check(perm3).can('VIEW')).toBe(true); + expect(permask.check(perm3).can('EDIT')).toBe(true); + expect(permask.check(perm3).can('DELETE')).toBe(true); + expect(permask.check(perm3).can('SHARE')).toBe(false); + }); + }); + + describe('Builder Pattern', () => { + it('should modify existing permissions using toBuilder', () => { + // Create a modified version of the permask + const extendedPermask = permask.toBuilder() + .definePermission('APPROVE', 32) + .defineGroup('REPORTS', 4) + .build(); + + const reportPerm = extendedPermask.for('REPORTS').grant(['VIEW', 'APPROVE']).value(); + + expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); + expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true); }); }); }); diff --git a/src/permask-class.ts b/src/permask-class.ts index e60b047..15ab453 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,424 +1,473 @@ -import { ACCESS_BITS, ACCESS_MASK } from "./constants/bitmask"; -import { PermissionAccess } from "./constants/permission"; +/** + * Default number of bits allocated for permissions (3 bits) + */ +const ACCESS_BITS = 3; /** - * Special permission symbol that represents all permissions + * Default access mask for 3 bits (0b111 = 7) */ -export const ALL_PERMISSIONS = Symbol('ALL_PERMISSIONS'); +const ACCESS_MASK = (1 << ACCESS_BITS) - 1; /** - * Type for permission lists that can include the special ALL symbol + * Default permission values */ -export type PermissionList = (keyof T | typeof ALL_PERMISSIONS)[]; +const PermissionAccess = { + READ: 1, // 0b001 + WRITE: 2, // 0b010 + DELETE: 4 // 0b100 +} as const; /** - * Flexible permission management system that allows custom permission definitions, - * bit allocations and groups. + * Special symbol representing all permissions */ -export class Permask = Record> { +export const ALL_PERMISSIONS = Symbol('ALL_PERMISSIONS'); + +/** + * Permission builder for creating and checking permissions + */ +export class PermaskBuilder = Record> { private permissions: T; private accessBits: number; private accessMask: number; - private groups: Record; + private groups: Record = {}; + private permissionSets: Record> = {}; - // Common permission combinations for convenience - readonly FULL: number; - readonly NONE: number; - readonly READ_ONLY: number; - readonly READ_WRITE: number; - - /** - * Create a custom permission system - * @param options Configuration options for the permission system - */ constructor(options: { - permissions?: T; // Custom permission types with bit values - accessBits?: number; // Number of bits allocated for permissions - accessMask?: number; // Mask for access bits - groups?: Record; // Custom groups + permissions?: T; + accessBits?: number; + accessMask?: number; + groups?: Record; } = {}) { - // Use the provided values or fall back to constants this.accessBits = options.accessBits || ACCESS_BITS; - // For accessMask, use provided value, or calculate based on custom bits, or use the constant if (options.accessMask !== undefined) { this.accessMask = options.accessMask; } else if (options.accessBits !== undefined) { - // Only recalculate if custom bits provided this.accessMask = (1 << this.accessBits) - 1; } else { - // Otherwise use the predefined constant this.accessMask = ACCESS_MASK; } this.permissions = (options.permissions || PermissionAccess as unknown as T); this.groups = options.groups || {}; - // Validate that permissions fit within specified bits - const maxValue = this.accessMask; + // Validate permissions fit within specified bits for (const [key, value] of Object.entries(this.permissions)) { - if (value > maxValue) { - throw new Error(`Permission '${key}' value ${value} exceeds the maximum value ${maxValue} for ${this.accessBits} bits`); + if (value > this.accessMask) { + throw new Error(`Permission '${key}' value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits`); } } + } + + /** + * Define a new permission + */ + definePermission(name: string, value: number): PermaskBuilder> { + if (value > this.accessMask) { + throw new Error(`Permission value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits`); + } + + (this.permissions as Record)[name] = value; + return this as unknown as PermaskBuilder>; + } + + /** + * Define a new group + */ + defineGroup(name: string, value: number): this { + this.groups[name] = value; + return this; + } + + /** + * Define a named set of permissions for reuse + */ + definePermissionSet(name: string, permissions: Array): this { + this.permissionSets[name] = permissions; + return this; + } + + /** + * Build and return a Permask instance + */ + build(): Permask { + return new Permask({ + permissions: this.permissions, + accessBits: this.accessBits, + accessMask: this.accessMask, + groups: this.groups, + permissionSets: this.permissionSets + }); + } +} - // Initialize common permission combinations - this.FULL = this.accessMask; - this.NONE = 0; - this.READ_ONLY = PermissionAccess.READ; - this.READ_WRITE = PermissionAccess.READ | PermissionAccess.WRITE; +/** + * Permission granting context for creating new permissions + */ +export class PermissionContext> { + private bitmask: number = 0; + + constructor( + private permask: Permask, + group: number | string + ) { + const groupValue = typeof group === 'string' + ? permask.getGroupByName(group) || 0 + : group; + + this.bitmask = groupValue << permask.accessBits; } /** - * Create a bitmask with the specified permissions and group - * @example - * // Basic usage - * const permission = permask.create('DOCUMENTS', ['READ', 'WRITE']); - * - * // Grant all permissions - * const fullAccess = permask.create('DOCUMENTS', [ALL_PERMISSIONS]); + * Grant specific permissions */ - create(group: number | string, permissionList: PermissionList): number { - // Check if ALL_PERMISSIONS is in the list - if (permissionList.includes(ALL_PERMISSIONS)) { - return this.createAllPermissions(group); + grant(permissions: Array): this { + if (permissions.includes(ALL_PERMISSIONS)) { + this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask; + return this; } - let access = 0; - for (const permission of permissionList) { - if (permission === ALL_PERMISSIONS) continue; // Just to be safe - access |= this.permissions[permission as string] || 0; + for (const permission of permissions) { + if (permission === ALL_PERMISSIONS) continue; + + const permValue = this.permask.getPermissionValue(permission); + if (permValue) { + const currentAccess = this.bitmask & this.permask.accessMask; + const newAccess = currentAccess | permValue; + this.bitmask = (this.bitmask & ~this.permask.accessMask) | newAccess; + } } - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return (groupValue << this.accessBits) | access; + return this; } /** - * Check if a bitmask has a specific permission + * Grant a predefined permission set */ - hasPermission(bitmask: number, permission: keyof T): boolean { - const access = bitmask & this.accessMask; - const permValue = (this.permissions as Record)[permission as string] || 0; - return (access & permValue) !== 0; + grantSet(setName: string): this { + const permissions = this.permask.getPermissionSet(setName); + if (permissions) { + return this.grant(permissions); + } + return this; } /** - * Get permission group from bitmask + * Grant full/all permissions */ - getGroup(bitmask: number): number { - return bitmask >> this.accessBits; + grantAll(): this { + return this.grant([ALL_PERMISSIONS]); } /** - * Get group name by group value + * Get the resulting permission bitmask */ - getGroupName(bitmask: number): string | undefined { - const groupValue = this.getGroup(bitmask); - const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); - return entry?.[0]; + value(): number { + return this.bitmask; + } +} + +/** + * Permission checking context + */ +export class PermissionCheck> { + private access: number; + + constructor( + private permask: Permask, + private bitmask: number + ) { + this.access = bitmask & permask.accessMask; } /** - * Check if bitmask has the specified group + * Check if has specific permission */ - hasGroup(bitmask: number, group: number | string): boolean { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return this.getGroup(bitmask) === groupValue; + can(permission: keyof T): boolean { + const permValue = this.permask.getPermissionValue(permission); + return permValue ? (this.access & permValue) !== 0 : false; } /** - * Create a bitmask with ALL permissions for a given group - * Acts like a wildcard (*) permission + * Check if has all specified permissions */ - createAllPermissions(group: number | string): number { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return (groupValue << this.accessBits) | this.accessMask; + canAll(permissions: Array): boolean { + for (const permission of permissions) { + if (!this.can(permission)) { + return false; + } + } + return true; } /** - * Check if bitmask has ALL permissions + * Check if has any of the specified permissions */ - hasAllPermissions(bitmask: number): boolean { - const access = bitmask & this.accessMask; - return access === this.accessMask; + canAny(permissions: Array): boolean { + for (const permission of permissions) { + if (this.can(permission)) { + return true; + } + } + return false; } - + /** - * Check if bitmask has ANY permissions + * Check if has all possible permissions */ - hasAnyPermission(bitmask: number): boolean { - const access = bitmask & this.accessMask; - return access !== 0; + canEverything(): boolean { + return this.access === this.permask.accessMask; } /** - * Add permission to existing bitmask without changing the group + * Check if has standard READ permission */ - addPermission(bitmask: number, permission: keyof T): number { - const group = this.getGroup(bitmask); - const permValue = (this.permissions as Record)[permission as string] || 0; - const access = (bitmask & this.accessMask) | permValue; - return (group << this.accessBits) | access; + canRead(): boolean { + return (this.access & PermissionAccess.READ) !== 0; } /** - * Add ALL permissions to existing bitmask without changing the group + * Check if has standard WRITE permission */ - addAllPermissions(bitmask: number): number { - const group = this.getGroup(bitmask); - return (group << this.accessBits) | this.accessMask; + canWrite(): boolean { + return (this.access & PermissionAccess.WRITE) !== 0; } /** - * Remove permission from existing bitmask + * Check if has standard DELETE permission */ - removePermission(bitmask: number, permission: keyof T): number { - const group = this.getGroup(bitmask); - const permValue = (this.permissions as Record)[permission as string] || 0; - const access = (bitmask & this.accessMask) & ~permValue; - return (group << this.accessBits) | access; + canDelete(): boolean { + return (this.access & PermissionAccess.DELETE) !== 0; } /** - * Parse bitmask into an object with group and permissions + * Get the group value */ - parse(bitmask: number): { + group(): number { + return this.bitmask >> this.permask.accessBits; + } + + /** + * Get the group name + */ + groupName(): string | undefined { + return this.permask.getGroupName(this.bitmask); + } + + /** + * Check if permission belongs to specified group + */ + inGroup(group: number | string): boolean { + return this.permask.hasGroup(this.bitmask, group); + } + + /** + * Get detailed information about this permission + */ + explain(): { group: number; groupName?: string; permissions: Partial>; } { - const group = this.getGroup(bitmask); - const permissions = {} as Partial>; - - for (const [key, value] of Object.entries(this.permissions)) { - permissions[key as keyof T] = this.hasPermission(bitmask, key as keyof T); - } - - return { - group, - groupName: this.getGroupName(bitmask), - permissions - }; + return this.permask.parse(this.bitmask); } +} + +/** + * Flexible permission management system with fluent API + */ +export class Permask = Record> { + // Common permission presets + readonly FULL_ACCESS: number; + readonly NO_ACCESS: number; + readonly READ_ONLY: number; + readonly READ_WRITE: number; - /** - * Register a new permission type (use with caution) - */ - registerPermission(name: string, bitValue: number): void { - if (bitValue > this.accessMask) { - throw new Error(`Permission value ${bitValue} exceeds the maximum value ${this.accessMask} for ${this.accessBits} bits`); - } + // Make accessMask accessible to context classes + readonly accessMask: number; + readonly accessBits: number; + + private permissions: T; + private groups: Record; + private permissionSets: Record>; + + constructor(options: { + permissions: T; + accessBits: number; + accessMask: number; + groups: Record; + permissionSets: Record>; + }) { + this.permissions = options.permissions; + this.accessBits = options.accessBits; + this.accessMask = options.accessMask; + this.groups = options.groups; + this.permissionSets = options.permissionSets; - (this.permissions as Record)[name] = bitValue; + // Initialize presets + this.FULL_ACCESS = this.accessMask; + this.NO_ACCESS = 0; + this.READ_ONLY = PermissionAccess.READ; + this.READ_WRITE = PermissionAccess.READ | PermissionAccess.WRITE; } /** - * Register a new group + * Start building a permission for a specific group + * @example + * // Create read-write permission for DOCUMENTS group + * const permission = permask.for('DOCUMENTS').grant(['READ', 'WRITE']).value(); */ - registerGroup(name: string, value: number): void { - this.groups[name] = value; + for(group: number | string): PermissionContext { + return new PermissionContext(this, group); } /** - * Create compatible bitmasks that can be used with the standard utils functions + * Check permissions in a bitmask + * @example + * // Check if user has READ permission + * if (permask.check(userPermission).can('READ')) { + * // Allow reading + * } */ - createStandardBitmask({ - group, - read = false, - write = false, - delete: del = false, - customPermissions = [] - }: { - group: number | string; - read?: boolean; - write?: boolean; - delete?: boolean; - customPermissions?: (keyof T)[]; - }): number { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - - // Create our own bitmask instead of using the utility function that uses ACCESS_BITS - let bitmask = 0; - if (read) bitmask |= PermissionAccess.READ; - if (write) bitmask |= PermissionAccess.WRITE; - if (del) bitmask |= PermissionAccess.DELETE; - - // Set group using our accessBits - bitmask |= (groupValue << this.accessBits); - - // Add any custom permissions - for (const permission of customPermissions) { - const permValue = (this.permissions as Record)[permission as string]; - if (permValue !== undefined) { - // Get the current access part without changing the group - const currentAccess = bitmask & this.accessMask; - // Add the new permission - const newAccess = currentAccess | permValue; - // Replace the access part in the result - bitmask = (bitmask & ~this.accessMask) | newAccess; - } - } - - return bitmask; + check(bitmask: number): PermissionCheck { + return new PermissionCheck(this, bitmask); } /** - * Compatibility methods with the standard utils + * Get a permission value by key */ - canRead(bitmask: number): boolean { - const access = bitmask & this.accessMask; - return (access & PermissionAccess.READ) !== 0; + getPermissionValue(permission: keyof T): number { + return this.permissions[permission as string] || 0; } - canWrite(bitmask: number): boolean { - const access = bitmask & this.accessMask; - return (access & PermissionAccess.WRITE) !== 0; + /** + * Get a named permission set + */ + getPermissionSet(name: string): Array | undefined { + return this.permissionSets[name]; } - canDelete(bitmask: number): boolean { - const access = bitmask & this.accessMask; - return (access & PermissionAccess.DELETE) !== 0; + /** + * Get a group value by name + */ + getGroupByName(name: string): number | undefined { + return this.groups[name]; } /** - * Get the full permission value (all bits set to 1) - * This is equivalent to having all permissions enabled + * Get group name from a bitmask */ - getFullAccessValue(): number { - return this.accessMask; + getGroupName(bitmask: number): string | undefined { + const groupValue = bitmask >> this.accessBits; + const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); + return entry?.[0]; } /** - * Combine multiple permissions into a single bitmask value - * Useful for creating custom composite permissions like "FULL" or "READ_WRITE" + * Check if a bitmask belongs to a specific group */ - combinePermissions(permissionNames: (keyof T)[]): number { - let result = 0; - for (const permission of permissionNames) { - result |= this.permissions[permission as string] || 0; - } - return result; + hasGroup(bitmask: number, group: number | string): boolean { + const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + return (bitmask >> this.accessBits) === groupValue; } /** - * Get a map of all permission values for reference - * Helps users understand the bit values of each permission + * Parse a bitmask into human-readable form */ - getPermissionValues(): Record { - const result: Record = {}; + parse(bitmask: number): { + group: number; + groupName?: string; + permissions: Partial>; + } { + const group = bitmask >> this.accessBits; + const access = bitmask & this.accessMask; + const permissions = {} as Partial>; - for (const [key, value] of Object.entries(this.permissions)) { - // Convert to binary string with leading zeros based on bit count - const binaryValue = value.toString(2).padStart(this.accessBits, '0'); - result[key] = { value, binaryValue }; + for (const key of Object.keys(this.permissions)) { + const permValue = this.permissions[key]; + permissions[key as keyof T] = (access & permValue) !== 0; } - // Add the FULL access value for reference - result['FULL_ACCESS'] = { - value: this.accessMask, - binaryValue: this.accessMask.toString(2).padStart(this.accessBits, '0') + return { + group, + groupName: this.getGroupName(bitmask), + permissions }; - - return result; } - + /** - * Create a compatible bitmask that can be used with the standard utils functions + * Convert a string-based permission description to a bitmask * @example - * // Create read-only access for group 1 - * const readOnly = permask.createAccess(1); - * - * // Create full access for ADMIN group - * const adminAccess = permask.createAccess('ADMIN', 'full'); - * - * // Create read-write access with custom permissions - * const customAccess = permask.createAccess('DOCUMENTS', 'read-write', ['SHARE']); + * // Create a permission from a string description + * const permission = permask.fromString('DOCUMENTS:READ,WRITE'); */ - createAccess( - group: number | string, - accessType: 'full' | 'none' | 'read-only' | 'read-write' = 'read-only', - customPermissions: (keyof T)[] = [] - ): number { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; + fromString(permissionString: string): number { + const [groupPart, permissionPart] = permissionString.split(':'); + + if (!groupPart) return 0; - // Start with the base access type - let bitmask = 0; - switch (accessType) { - case 'full': - bitmask = this.FULL; - break; - case 'read-write': - bitmask = this.READ_WRITE; - break; - case 'read-only': - bitmask = this.READ_ONLY; - break; - case 'none': - default: - bitmask = this.NONE; + const group = this.groups[groupPart] !== undefined ? this.groups[groupPart] : Number(groupPart) || 0; + + if (!permissionPart) { + return group << this.accessBits; } - // Add group - bitmask |= (groupValue << this.accessBits); + const permList = permissionPart.split(',') + .map(p => p.trim()) + .filter(p => p !== ''); + + if (permList.includes('*') || permList.includes('ALL')) { + return (group << this.accessBits) | this.accessMask; + } - // Add any custom permissions - for (const permission of customPermissions) { - const permValue = (this.permissions as Record)[permission as string]; - if (permValue !== undefined) { - // Get the current access part without changing the group - const currentAccess = bitmask & this.accessMask; - // Add the new permission - const newAccess = currentAccess | permValue; - // Replace the access part in the result - bitmask = (bitmask & ~this.accessMask) | newAccess; + let access = 0; + for (const perm of permList) { + // Check if it's a permission set + if (this.permissionSets[perm]) { + for (const subPerm of this.permissionSets[perm]) { + access |= this.permissions[subPerm as string] || 0; + } + } else { + // Treat as individual permission + access |= this.permissions[perm as keyof T] || 0; } } - return bitmask; + return (group << this.accessBits) | access; } - // Simplified version of createStandardBitmask /** - * Create standard permissions with a simpler interface - * @example - * // Read-only access - * const readOnly = permask.grant({ - * group: 'DOCUMENTS', - * read: true - * }); - * - * // Full access - * const fullAccess = permask.grant({ - * group: 'ADMIN', - * all: true - * }); + * Convert a bitmask to a string representation */ - grant({ - group, - all = false, - read = false, - write = false, - delete: del = false, - permissions = [] - }: { - group: number | string; - all?: boolean; - read?: boolean; - write?: boolean; - delete?: boolean; - permissions?: (keyof T)[]; - }): number { - if (all) { - return this.createAllPermissions(group); + toString(bitmask: number): string { + const parsed = this.parse(bitmask); + const groupName = parsed.groupName || parsed.group.toString(); + + const permissionNames = Object.entries(parsed.permissions) + .filter(([, enabled]) => enabled) + .map(([name]) => name); + + if (permissionNames.length === 0) { + return `${groupName}:NONE`; } - return this.createStandardBitmask({ - group, - read, - write, - delete: del, - customPermissions: permissions + // Check if all permissions are enabled + if ((bitmask & this.accessMask) === this.accessMask) { + return `${groupName}:ALL`; + } + + return `${groupName}:${permissionNames.join(',')}`; + } + + /** + * Create a new builder with the current configuration + */ + toBuilder(): PermaskBuilder { + return new PermaskBuilder({ + permissions: this.permissions, + accessBits: this.accessBits, + accessMask: this.accessMask, + groups: { ...this.groups } }); } } From 145b4a84b4945d401746211510de861f4e6383e3 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 13:22:29 +0300 Subject: [PATCH 07/25] feat: enhance Permask class with CRUD permissions and update default access settings --- src/permask-class.test.ts | 129 +++++++++++++++++++------------------- src/permask-class.ts | 60 ++++++++++-------- 2 files changed, 98 insertions(+), 91 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 4c643f2..2608fff 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { ALL_PERMISSIONS, Permask, PermaskBuilder } from '../src/permask-class'; -import { PermissionAccess } from './constants/permission'; +import { DefaultPermissionAccess, Permask, PermaskBuilder } from '../src/permask-class'; describe('Permask', () => { // Simple permissions for basic tests @@ -27,11 +26,11 @@ describe('Permask', () => { DELETE: number; }>({ permissions: { - READ: PermissionAccess.READ, // 1 - WRITE: PermissionAccess.WRITE, // 2 - DELETE: PermissionAccess.DELETE // 4 + READ: DefaultPermissionAccess.READ, // 2 + WRITE: DefaultPermissionAccess.WRITE, // 8 + DELETE: DefaultPermissionAccess.DELETE // 16 }, - accessBits: 3, + accessBits: 5, groups: { USERS: 1, ADMINS: 2 @@ -129,6 +128,14 @@ describe('Permask', () => { expect(richPermask.check(customPerm).can('DELETE')).toBe(false); expect(richPermask.check(customPerm).can('PRINT')).toBe(false); }); + + it('should handle granting all permissions', () => { + const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); + + // All permissions should set all bits + expect(richPermask.check(allPermissions).canEverything()).toBe(true); + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); + }); }); describe('Permission Checking', () => { @@ -321,64 +328,6 @@ describe('Permask', () => { expect(richPermask.check(customPerm).can('PRINT')).toBe(false); }); }); - - describe('Default Constants', () => { - it('should use default access bits and mask when not specified', () => { - // Create a permask without specifying accessBits or accessMask - const defaultPermask = new PermaskBuilder().build(); - - // Verify that the default ACCESS_BITS is 3 - expect(defaultPermask.accessBits).toBe(3); - - // Verify that the default ACCESS_MASK is 7 (binary: 111, which is (1 << 3) - 1) - expect(defaultPermask.accessMask).toBe(7); - - // Verify that all permission bits fit within the mask - // The mask 7 (binary 111) has 3 bits, so 1, 2, and 4 should all fit - const allPerms = defaultPermask.for(1).grantAll().value(); - expect(allPerms & 7).toBe(7); - }); - - it('should use default permission values when not specified', () => { - // Create a default permask without specifying custom permissions - const defaultPermask = new PermaskBuilder().build(); - - // Add permissions to test default values - const readPerm = defaultPermask.for(1).grant(['READ']).value(); - const writePerm = defaultPermask.for(1).grant(['WRITE']).value(); - const deletePerm = defaultPermask.for(1).grant(['DELETE']).value(); - - // Verify READ permission = 1 - expect(defaultPermask.check(readPerm).can('READ')).toBe(true); - expect(readPerm & 7).toBe(1); - - // Verify WRITE permission = 2 - expect(defaultPermask.check(writePerm).can('WRITE')).toBe(true); - expect(writePerm & 7).toBe(2); - - // Verify DELETE permission = 4 - expect(defaultPermask.check(deletePerm).can('DELETE')).toBe(true); - expect(deletePerm & 7).toBe(4); - - // Verify READ_ONLY preset is same as READ permission - expect(defaultPermask.READ_ONLY).toBe(1); - - // Verify READ_WRITE preset is READ | WRITE - expect(defaultPermask.READ_WRITE).toBe(3); - }); - - it('should calculate access mask correctly for different bit sizes', () => { - // Create permasks with different accessBits to verify mask calculation - const bits4Permask = new PermaskBuilder({ accessBits: 4 }).build(); - const bits5Permask = new PermaskBuilder({ accessBits: 5 }).build(); - - // 4 bits should give mask 15 (binary: 1111) - expect(bits4Permask.accessMask).toBe(15); - - // 5 bits should give mask 31 (binary: 11111) - expect(bits5Permask.accessMask).toBe(31); - }); - }); }); describe('New Permask API', () => { @@ -539,3 +488,55 @@ describe('New Permask API', () => { }); }); }); + +// Add new test cases for CREATE and UPDATE permissions +describe('CRUD Permission Tests', () => { + let crudPermask: Permask; + + beforeEach(() => { + crudPermask = new PermaskBuilder({ + permissions: DefaultPermissionAccess, + accessBits: 5, + groups: { + USERS: 1, + DOCUMENTS: 2, + PHOTOS: 3 + } + }).build(); + }); + + it('should handle all CRUD permissions correctly', () => { + const fullCrudPerm = crudPermask.for('DOCUMENTS').grantAll().value(); + const createReadPerm = crudPermask.for('DOCUMENTS').grant(['CREATE', 'READ']).value(); + const readUpdatePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE']).value(); + + // Full CRUD permissions + expect(crudPermask.check(fullCrudPerm).canCreate()).toBe(true); + expect(crudPermask.check(fullCrudPerm).canRead()).toBe(true); + expect(crudPermask.check(fullCrudPerm).canUpdate()).toBe(true); + expect(crudPermask.check(fullCrudPerm).canWrite()).toBe(true); + expect(crudPermask.check(fullCrudPerm).canDelete()).toBe(true); + + // Create + Read permissions + expect(crudPermask.check(createReadPerm).canCreate()).toBe(true); + expect(crudPermask.check(createReadPerm).canRead()).toBe(true); + expect(crudPermask.check(createReadPerm).canUpdate()).toBe(false); + expect(crudPermask.check(createReadPerm).canWrite()).toBe(false); + expect(crudPermask.check(createReadPerm).canDelete()).toBe(false); + + // Read + Update permissions + expect(crudPermask.check(readUpdatePerm).canCreate()).toBe(false); + expect(crudPermask.check(readUpdatePerm).canRead()).toBe(true); + expect(crudPermask.check(readUpdatePerm).canUpdate()).toBe(true); + expect(crudPermask.check(readUpdatePerm).canWrite()).toBe(false); + expect(crudPermask.check(readUpdatePerm).canDelete()).toBe(false); + }); + + it('should represent CRUD permissions in string format', () => { + const createReadPerm = crudPermask.for('DOCUMENTS').grant(['CREATE', 'READ']).value(); + const readUpdateWritePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE', 'WRITE']).value(); + + expect(crudPermask.toString(createReadPerm)).toBe('DOCUMENTS:CREATE,READ'); + expect(crudPermask.toString(readUpdateWritePerm)).toBe('PHOTOS:READ,UPDATE,WRITE'); + }); +}); diff --git a/src/permask-class.ts b/src/permask-class.ts index 15ab453..4a9b2b8 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,27 +1,25 @@ /** - * Default number of bits allocated for permissions (3 bits) + * Default number of bits allocated for permissions (5 bits) */ -const ACCESS_BITS = 3; +const ACCESS_BITS = 5; /** - * Default access mask for 3 bits (0b111 = 7) + * Default access mask for 5 bits (0b11111 = 31) */ const ACCESS_MASK = (1 << ACCESS_BITS) - 1; /** * Default permission values */ -const PermissionAccess = { - READ: 1, // 0b001 - WRITE: 2, // 0b010 - DELETE: 4 // 0b100 +export const DefaultPermissionAccess = { + CREATE: 1, // 0b00001 + READ: 2, // 0b00010 + UPDATE: 4, // 0b00100 + WRITE: 8, // 0b01000 + DELETE: 16, // 0b10000 + FULL: 31 // 0b11111 } as const; -/** - * Special symbol representing all permissions - */ -export const ALL_PERMISSIONS = Symbol('ALL_PERMISSIONS'); - /** * Permission builder for creating and checking permissions */ @@ -48,7 +46,7 @@ export class PermaskBuilder = Record> { /** * Grant specific permissions */ - grant(permissions: Array): this { - if (permissions.includes(ALL_PERMISSIONS)) { - this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask; - return this; - } - + grant(permissions: Array): this { for (const permission of permissions) { - if (permission === ALL_PERMISSIONS) continue; - const permValue = this.permask.getPermissionValue(permission); if (permValue) { const currentAccess = this.bitmask & this.permask.accessMask; @@ -156,7 +147,8 @@ export class PermissionContext> { * Grant full/all permissions */ grantAll(): this { - return this.grant([ALL_PERMISSIONS]); + this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask; + return this; } /** @@ -219,25 +211,39 @@ export class PermissionCheck> { return this.access === this.permask.accessMask; } + /** + * Check if has standard CREATE permission + */ + canCreate(): boolean { + return (this.access & DefaultPermissionAccess.CREATE) !== 0; + } + /** * Check if has standard READ permission */ canRead(): boolean { - return (this.access & PermissionAccess.READ) !== 0; + return (this.access & DefaultPermissionAccess.READ) !== 0; + } + + /** + * Check if has standard UPDATE permission + */ + canUpdate(): boolean { + return (this.access & DefaultPermissionAccess.UPDATE) !== 0; } /** * Check if has standard WRITE permission */ canWrite(): boolean { - return (this.access & PermissionAccess.WRITE) !== 0; + return (this.access & DefaultPermissionAccess.WRITE) !== 0; } /** * Check if has standard DELETE permission */ canDelete(): boolean { - return (this.access & PermissionAccess.DELETE) !== 0; + return (this.access & DefaultPermissionAccess.DELETE) !== 0; } /** @@ -307,8 +313,8 @@ export class Permask = Record> // Initialize presets this.FULL_ACCESS = this.accessMask; this.NO_ACCESS = 0; - this.READ_ONLY = PermissionAccess.READ; - this.READ_WRITE = PermissionAccess.READ | PermissionAccess.WRITE; + this.READ_ONLY = DefaultPermissionAccess.READ; + this.READ_WRITE = DefaultPermissionAccess.READ | DefaultPermissionAccess.WRITE; } /** From 202bbc88fce45bf509e1daac0e75ac8a33fb86c3 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 13:31:09 +0300 Subject: [PATCH 08/25] test: update Permask tests to reflect changes in permission handling and grantAll method --- src/permask-class.test.ts | 33 ++++++++++++++++++++++++++++----- src/permask-class.ts | 8 +++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 2608fff..4b29e6d 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -73,11 +73,11 @@ describe('Permask', () => { const readPermission = basicPermask.for('USERS').grant(['READ']).value(); const readWritePermission = basicPermask.for('USERS').grant(['READ', 'WRITE']).value(); - // Group 1 (USERS) << 3 bits = 8, plus READ (1) = 9 - expect(readPermission).toBe(9); + // Group 1 (USERS) << 5 bits = 32, plus READ (2) = 34 + expect(readPermission).toBe(34); - // Group 1 (USERS) << 3 bits = 8, plus READ (1) + WRITE (2) = 11 - expect(readWritePermission).toBe(11); + // Group 1 (USERS) << 5 bits = 32, plus READ (2) + WRITE (8) = 42 + expect(readWritePermission).toBe(42); }); it('should create permissions with explicit group IDs', () => { @@ -90,7 +90,7 @@ describe('Permask', () => { }); it('should handle ALL_PERMISSIONS symbol', () => { - const allPermissions = richPermask.for('DOCUMENTS').grant([ALL_PERMISSIONS]).value(); + const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); const alternateWay = richPermask.for('DOCUMENTS').grantAll().value(); // All permissions should set all bits (63 for 6 bits) @@ -136,6 +136,17 @@ describe('Permask', () => { expect(richPermask.check(allPermissions).canEverything()).toBe(true); expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); }); + + it('should handle granting all permissions via grantAll()', () => { + // Changed from using ALL_PERMISSIONS to directly using grantAll() + const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); + const alternateWay = richPermask.for('DOCUMENTS').grantAll().value(); + + // All permissions should set all bits (63 for 6 bits) + expect(richPermask.check(allPermissions).canEverything()).toBe(true); + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); + expect(allPermissions).toEqual(alternateWay); + }); }); describe('Permission Checking', () => { @@ -402,6 +413,18 @@ describe('New Permask API', () => { expect(permask.check(allPerm).can('SHARE')).toBe(true); expect(permask.check(allPerm).can('PRINT')).toBe(true); }); + + it('should handle granting all permissions', () => { + // Changed from ALL_PERMISSIONS to grantAll() + const allPerm = permask.for('VIDEOS').grantAll().value(); + + expect(permask.check(allPerm).canEverything()).toBe(true); + expect(permask.check(allPerm).can('VIEW')).toBe(true); + expect(permask.check(allPerm).can('EDIT')).toBe(true); + expect(permask.check(allPerm).can('DELETE')).toBe(true); + expect(permask.check(allPerm).can('SHARE')).toBe(true); + expect(permask.check(allPerm).can('PRINT')).toBe(true); + }); }); describe('Checking Permissions', () => { diff --git a/src/permask-class.ts b/src/permask-class.ts index 4a9b2b8..51f0ec0 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -450,7 +450,13 @@ export class Permask = Record> const groupName = parsed.groupName || parsed.group.toString(); const permissionNames = Object.entries(parsed.permissions) - .filter(([, enabled]) => enabled) + .filter(([name, enabled]) => { + // Filter out FULL to avoid confusion with individual permissions + if (name === 'FULL') { + return false; + } + return enabled; + }) .map(([name]) => name); if (permissionNames.length === 0) { From 1a14bcc42d452398bc821ceb265249d963d3d1f8 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 14:13:48 +0300 Subject: [PATCH 09/25] feat: add parseSimple method to Permask class and corresponding test suite --- src/permask-class.test.ts | 140 ++++++++++++++++++++++++++++++++++++++ src/permask-class.ts | 44 ++++++++++++ 2 files changed, 184 insertions(+) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 4b29e6d..44ef0fc 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -563,3 +563,143 @@ describe('CRUD Permission Tests', () => { expect(crudPermask.toString(readUpdateWritePerm)).toBe('PHOTOS:READ,UPDATE,WRITE'); }); }); + +// Add test suite for the parseSimple method +describe('ParseSimple Method Tests', () => { + let permask: Permask<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + FULL: number; + }>; + + beforeEach(() => { + permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + FULL: number; + }>({ + permissions: { + VIEW: 1, + EDIT: 2, + DELETE: 4, + SHARE: 8, + PRINT: 16, + FULL: 31 + }, + accessBits: 6, + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + VIDEOS: 3 + } + }).build(); + }); + + it('should return a flattened representation of permissions', () => { + // Create permissions with different settings + const basicPerm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const allPerm = permask.for('PHOTOS').grantAll().value(); + const noPerm = permask.for('VIDEOS').grant([]).value(); + + // Test basic permission + const basicResult = permask.parseSimple(basicPerm); + expect(basicResult).toEqual({ + group: 1, + groupName: 'DOCUMENTS', + view: true, + edit: true, + delete: false, + share: false, + print: false, + full: false + }); + + // Test full permissions + const allResult = permask.parseSimple(allPerm); + expect(allResult).toEqual({ + group: 2, + groupName: 'PHOTOS', + view: true, + edit: true, + delete: true, + share: true, + print: true, + full: false + }); + + // Test no permissions + const noResult = permask.parseSimple(noPerm); + expect(noResult).toEqual({ + group: 3, + groupName: 'VIDEOS', + view: false, + edit: false, + delete: false, + share: false, + print: false, + full: false + }); + }); + + it('should work with numeric bitmasks directly', () => { + // Using raw bitmasks (group 2 << 6 | VIEW+DELETE = 131) + const manualBitmask = (2 << 6) | 5; // Group 2 with VIEW(1) and DELETE(4) + + const result = permask.parseSimple(manualBitmask); + expect(result).toEqual({ + group: 2, + groupName: 'PHOTOS', + view: true, + edit: false, + delete: true, + share: false, + print: false, + full: false, + }); + }); + + it('should work with unknown groups', () => { + // Group 10 doesn't exist in the defined groups + const unknownGroupPerm = permask.for(10).grant(['VIEW', 'SHARE']).value(); + + const result = permask.parseSimple(unknownGroupPerm); + expect(result).toEqual({ + group: 10, + view: true, + edit: false, + delete: false, + share: true, + print: false, + full: false, + }); + // Note: no groupName property since the group is unknown + }); + + it('should work with default permissions', () => { + // Create a permask with default CRUD permissions + const crudPermask = new PermaskBuilder({ + permissions: DefaultPermissionAccess, + groups: { USERS: 1 } + }).build(); + + const perm = crudPermask.for('USERS').grant(['CREATE', 'READ', 'UPDATE']).value(); + + const result = crudPermask.parseSimple(perm); + expect(result).toEqual({ + group: 1, + groupName: 'USERS', + create: true, + read: true, + update: true, + write: false, + delete: false, + full: false + }); + }); +}); diff --git a/src/permask-class.ts b/src/permask-class.ts index 51f0ec0..9777d3f 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -401,6 +401,50 @@ export class Permask = Record> }; } + /** + * Parse a bitmask into a simplified flat object representation + * @example + * // Returns { group: 2, read: true, write: true, delete: false } + * const simple = permask.parseSimple(42); + */ + parseSimple(bitmask: number): Record { + const group = bitmask >> this.accessBits; + const access = bitmask & this.accessMask; + const result: Record = { group }; + + // Add group name if available + const groupName = this.getGroupName(bitmask); + if (groupName) { + result.groupName = groupName; + } + + // Calculate the combined mask for all actual permissions (excluding special masks like FULL) + let combinedPermissionsMask = 0; + for (const key of Object.keys(this.permissions)) { + // Skip special permissions that represent combinations + if (key === 'FULL') continue; + combinedPermissionsMask |= this.permissions[key]; + } + + // Add flattened permissions + for (const key of Object.keys(this.permissions)) { + const permValue = this.permissions[key]; + const permKey = key.toLowerCase(); + + // Special handling for FULL permission: + // If it represents all other permissions combined, only include it if specifically requested + if (key === 'FULL' && permValue === combinedPermissionsMask) { + // Only include FULL if it was specifically granted (compare with exact mask) + result[permKey] = access === permValue; + } else { + // For normal permissions, just check if the bit is set + result[permKey] = (access & permValue) !== 0; + } + } + + return result; + } + /** * Convert a string-based permission description to a bitmask * @example From a166dab796a592ce800bf65904b96f6e6b9977a3 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 14:20:54 +0300 Subject: [PATCH 10/25] feat: define PermissionSimple type and update parseSimple method in Permask class --- src/permask-class.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index 9777d3f..dc83d43 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -279,6 +279,16 @@ export class PermissionCheck> { } } +/** + * Type definition for the result of parseSimple + */ +export type PermissionSimple> = { + group: number; + groupName?: string; +} & { + [K in string & keyof T as Lowercase]: boolean; +}; + /** * Flexible permission management system with fluent API */ @@ -286,8 +296,6 @@ export class Permask = Record> // Common permission presets readonly FULL_ACCESS: number; readonly NO_ACCESS: number; - readonly READ_ONLY: number; - readonly READ_WRITE: number; // Make accessMask accessible to context classes readonly accessMask: number; @@ -313,8 +321,6 @@ export class Permask = Record> // Initialize presets this.FULL_ACCESS = this.accessMask; this.NO_ACCESS = 0; - this.READ_ONLY = DefaultPermissionAccess.READ; - this.READ_WRITE = DefaultPermissionAccess.READ | DefaultPermissionAccess.WRITE; } /** @@ -407,10 +413,10 @@ export class Permask = Record> * // Returns { group: 2, read: true, write: true, delete: false } * const simple = permask.parseSimple(42); */ - parseSimple(bitmask: number): Record { + parseSimple(bitmask: number): PermissionSimple { const group = bitmask >> this.accessBits; const access = bitmask & this.accessMask; - const result: Record = { group }; + const result = { group } as PermissionSimple; // Add group name if available const groupName = this.getGroupName(bitmask); @@ -435,10 +441,10 @@ export class Permask = Record> // If it represents all other permissions combined, only include it if specifically requested if (key === 'FULL' && permValue === combinedPermissionsMask) { // Only include FULL if it was specifically granted (compare with exact mask) - result[permKey] = access === permValue; + (result as any)[permKey] = access === permValue; } else { // For normal permissions, just check if the bit is set - result[permKey] = (access & permValue) !== 0; + (result as any)[permKey] = (access & permValue) !== 0; } } From 6e67162383d1b7af6266f8fe80a1da2349360fa3 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 15:14:36 +0300 Subject: [PATCH 11/25] feat: update PermaskBuilder and PermissionCheck to support ALL permission handling --- src/permask-class.test.ts | 200 +++++++++++++++++--------------------- src/permask-class.ts | 124 +++++++++++------------ 2 files changed, 147 insertions(+), 177 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 44ef0fc..f3f1af7 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -191,7 +191,8 @@ describe('Permask', () => { EDIT: false, DELETE: false, SHARE: true, - PRINT: false + PRINT: false, + ALL: false, } }); }); @@ -461,7 +462,8 @@ describe('New Permask API', () => { EDIT: true, DELETE: false, SHARE: false, - PRINT: false + PRINT: false, + ALL: false, } }); }); @@ -564,142 +566,120 @@ describe('CRUD Permission Tests', () => { }); }); -// Add test suite for the parseSimple method -describe('ParseSimple Method Tests', () => { - let permask: Permask<{ +// Add new test cases for ALL permission +describe('ALL Permission Tests', () => { + let allPermask: Permask<{ VIEW: number; EDIT: number; DELETE: number; SHARE: number; PRINT: number; - FULL: number; + ALL: number; }>; beforeEach(() => { - permask = new PermaskBuilder<{ + allPermask = new PermaskBuilder<{ VIEW: number; EDIT: number; DELETE: number; SHARE: number; PRINT: number; - FULL: number; + ALL: number; }>({ permissions: { - VIEW: 1, - EDIT: 2, - DELETE: 4, - SHARE: 8, - PRINT: 16, - FULL: 31 + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16, // 0b10000 + ALL: 31 // 0b11111 - All permissions combined }, accessBits: 6, groups: { DOCUMENTS: 1, - PHOTOS: 2, - VIDEOS: 3 + PHOTOS: 2 } }).build(); }); - - it('should return a flattened representation of permissions', () => { - // Create permissions with different settings - const basicPerm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const allPerm = permask.for('PHOTOS').grantAll().value(); - const noPerm = permask.for('VIDEOS').grant([]).value(); - - // Test basic permission - const basicResult = permask.parseSimple(basicPerm); - expect(basicResult).toEqual({ + + it('should handle the ALL permission correctly', () => { + // Grant individual permissions + const partialPerm = allPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + + // Grant all permissions + const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + + // Grant using the ALL permission directly + const allPerm = allPermask.for('PHOTOS').grant(['ALL']).value(); + + // Test partial permissions + expect(allPermask.check(partialPerm).can('VIEW')).toBe(true); + expect(allPermask.check(partialPerm).can('EDIT')).toBe(true); + expect(allPermask.check(partialPerm).can('DELETE')).toBe(false); + expect(allPermask.check(partialPerm).can('ALL')).toBe(false); + expect(allPermask.check(partialPerm).canEverything()).toBe(false); + + // Test full permissions via grantAll + expect(allPermask.check(fullPerm).can('VIEW')).toBe(true); + expect(allPermask.check(fullPerm).can('EDIT')).toBe(true); + expect(allPermask.check(fullPerm).can('DELETE')).toBe(true); + expect(allPermask.check(fullPerm).can('SHARE')).toBe(true); + expect(allPermask.check(fullPerm).can('PRINT')).toBe(true); + expect(allPermask.check(fullPerm).can('ALL')).toBe(true); + expect(allPermask.check(fullPerm).canEverything()).toBe(true); + + // Test granting ALL permission directly + expect(allPermask.check(allPerm).can('VIEW')).toBe(true); + expect(allPermask.check(allPerm).can('EDIT')).toBe(true); + expect(allPermask.check(allPerm).can('DELETE')).toBe(true); + expect(allPermask.check(allPerm).can('SHARE')).toBe(true); + expect(allPermask.check(allPerm).can('PRINT')).toBe(true); + expect(allPermask.check(allPerm).can('ALL')).toBe(true); + expect(allPermask.check(allPerm).canEverything()).toBe(true); + }); + + it('should include ALL in permission explanation', () => { + // Partial permissions + const partialPerm = allPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const details = allPermask.check(partialPerm).explain(); + + expect(details).toEqual({ group: 1, groupName: 'DOCUMENTS', - view: true, - edit: true, - delete: false, - share: false, - print: false, - full: false - }); - - // Test full permissions - const allResult = permask.parseSimple(allPerm); - expect(allResult).toEqual({ - group: 2, - groupName: 'PHOTOS', - view: true, - edit: true, - delete: true, - share: true, - print: true, - full: false - }); - - // Test no permissions - const noResult = permask.parseSimple(noPerm); - expect(noResult).toEqual({ - group: 3, - groupName: 'VIDEOS', - view: false, - edit: false, - delete: false, - share: false, - print: false, - full: false - }); - }); - - it('should work with numeric bitmasks directly', () => { - // Using raw bitmasks (group 2 << 6 | VIEW+DELETE = 131) - const manualBitmask = (2 << 6) | 5; // Group 2 with VIEW(1) and DELETE(4) - - const result = permask.parseSimple(manualBitmask); - expect(result).toEqual({ - group: 2, - groupName: 'PHOTOS', - view: true, - edit: false, - delete: true, - share: false, - print: false, - full: false, + permissions: { + VIEW: true, + EDIT: true, + DELETE: false, + SHARE: false, + PRINT: false, + ALL: false + } }); - }); - - it('should work with unknown groups', () => { - // Group 10 doesn't exist in the defined groups - const unknownGroupPerm = permask.for(10).grant(['VIEW', 'SHARE']).value(); - - const result = permask.parseSimple(unknownGroupPerm); - expect(result).toEqual({ - group: 10, - view: true, - edit: false, - delete: false, - share: true, - print: false, - full: false, - }); - // Note: no groupName property since the group is unknown - }); - - it('should work with default permissions', () => { - // Create a permask with default CRUD permissions - const crudPermask = new PermaskBuilder({ - permissions: DefaultPermissionAccess, - groups: { USERS: 1 } - }).build(); - const perm = crudPermask.for('USERS').grant(['CREATE', 'READ', 'UPDATE']).value(); + // Full permissions + const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + const fullDetails = allPermask.check(fullPerm).explain(); - const result = crudPermask.parseSimple(perm); - expect(result).toEqual({ + expect(fullDetails).toEqual({ group: 1, - groupName: 'USERS', - create: true, - read: true, - update: true, - write: false, - delete: false, - full: false + groupName: 'DOCUMENTS', + permissions: { + VIEW: true, + EDIT: true, + DELETE: true, + SHARE: true, + PRINT: true, + ALL: true + } }); }); + + it('should handle string conversion with ALL', () => { + const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + expect(allPermask.toString(fullPerm)).toBe('DOCUMENTS:ALL'); + + const fromString = allPermask.fromString('PHOTOS:ALL'); + expect(allPermask.check(fromString).canEverything()).toBe(true); + expect(allPermask.check(fromString).can('ALL')).toBe(true); + }); }); diff --git a/src/permask-class.ts b/src/permask-class.ts index dc83d43..f93bd08 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -17,14 +17,14 @@ export const DefaultPermissionAccess = { UPDATE: 4, // 0b00100 WRITE: 8, // 0b01000 DELETE: 16, // 0b10000 - FULL: 31 // 0b11111 + ALL: 31 // 0b11111 - All permissions combined } as const; /** * Permission builder for creating and checking permissions */ export class PermaskBuilder = Record> { - private permissions: T; + private permissions: T & { ALL: number }; private accessBits: number; private accessMask: number; private groups: Record = {}; @@ -46,7 +46,23 @@ export class PermaskBuilder = Record = Record { - return new Permask({ + build(): Permask { + return new Permask({ permissions: this.permissions, accessBits: this.accessBits, accessMask: this.accessMask, @@ -176,6 +192,13 @@ export class PermissionCheck> { * Check if has specific permission */ can(permission: keyof T): boolean { + // Special handling for the ALL permission + if (permission === 'ALL' as keyof T) { + // For ALL permission, we need to check if all bits are set + // Compare against the accessMask directly, not against the ALL value + return this.canEverything(); + } + const permValue = this.permask.getPermissionValue(permission); return permValue ? (this.access & permValue) !== 0 : false; } @@ -210,6 +233,14 @@ export class PermissionCheck> { canEverything(): boolean { return this.access === this.permask.accessMask; } + + /** + * Check if has the ALL permission specifically + */ + hasAllPermission(): boolean { + const allPermValue = this.permask.getPermissionValue('ALL'); + return allPermValue ? (this.access & allPermValue) !== 0 : this.canEverything(); + } /** * Check if has standard CREATE permission @@ -279,16 +310,6 @@ export class PermissionCheck> { } } -/** - * Type definition for the result of parseSimple - */ -export type PermissionSimple> = { - group: number; - groupName?: string; -} & { - [K in string & keyof T as Lowercase]: boolean; -}; - /** * Flexible permission management system with fluent API */ @@ -395,60 +416,29 @@ export class Permask = Record> const access = bitmask & this.accessMask; const permissions = {} as Partial>; - for (const key of Object.keys(this.permissions)) { - const permValue = this.permissions[key]; - permissions[key as keyof T] = (access & permValue) !== 0; - } - - return { - group, - groupName: this.getGroupName(bitmask), - permissions - }; - } - - /** - * Parse a bitmask into a simplified flat object representation - * @example - * // Returns { group: 2, read: true, write: true, delete: false } - * const simple = permask.parseSimple(42); - */ - parseSimple(bitmask: number): PermissionSimple { - const group = bitmask >> this.accessBits; - const access = bitmask & this.accessMask; - const result = { group } as PermissionSimple; - - // Add group name if available - const groupName = this.getGroupName(bitmask); - if (groupName) { - result.groupName = groupName; - } - - // Calculate the combined mask for all actual permissions (excluding special masks like FULL) + // Calculate the combined mask for all actual permissions (excluding ALL) let combinedPermissionsMask = 0; for (const key of Object.keys(this.permissions)) { - // Skip special permissions that represent combinations - if (key === 'FULL') continue; - combinedPermissionsMask |= this.permissions[key]; + if (key !== 'ALL') { + combinedPermissionsMask |= this.permissions[key]; + } } - // Add flattened permissions for (const key of Object.keys(this.permissions)) { - const permValue = this.permissions[key]; - const permKey = key.toLowerCase(); - - // Special handling for FULL permission: - // If it represents all other permissions combined, only include it if specifically requested - if (key === 'FULL' && permValue === combinedPermissionsMask) { - // Only include FULL if it was specifically granted (compare with exact mask) - (result as any)[permKey] = access === permValue; + if (key === 'ALL') { + // ALL permission should be true only if all bits in the accessMask are set + permissions[key as keyof T] = (access === this.accessMask); } else { - // For normal permissions, just check if the bit is set - (result as any)[permKey] = (access & permValue) !== 0; + const permValue = this.permissions[key]; + permissions[key as keyof T] = (access & permValue) !== 0; } } - return result; + return { + group, + groupName: this.getGroupName(bitmask), + permissions + }; } /** @@ -499,10 +489,15 @@ export class Permask = Record> const parsed = this.parse(bitmask); const groupName = parsed.groupName || parsed.group.toString(); + // If all permission bits are set, return ALL + if ((bitmask & this.accessMask) === this.accessMask) { + return `${groupName}:ALL`; + } + const permissionNames = Object.entries(parsed.permissions) .filter(([name, enabled]) => { - // Filter out FULL to avoid confusion with individual permissions - if (name === 'FULL') { + // Filter out ALL to avoid confusion with individual permissions + if (name === 'ALL') { return false; } return enabled; @@ -513,11 +508,6 @@ export class Permask = Record> return `${groupName}:NONE`; } - // Check if all permissions are enabled - if ((bitmask & this.accessMask) === this.accessMask) { - return `${groupName}:ALL`; - } - return `${groupName}:${permissionNames.join(',')}`; } From 927117061becd22fd943390a2dbebc36bf8e92bf Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 15:18:08 +0300 Subject: [PATCH 12/25] feat: update PermaskBuilder and PermissionCheck to handle 'ALL' permission correctly --- src/permask-class.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index f93bd08..3426f10 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -104,13 +104,13 @@ export class PermaskBuilder = Record { - return new Permask({ - permissions: this.permissions, + build(): Permask { + return new Permask({ + permissions: this.permissions as unknown as T, accessBits: this.accessBits, accessMask: this.accessMask, groups: this.groups, - permissionSets: this.permissionSets + permissionSets: this.permissionSets as Record> }); } } @@ -138,6 +138,13 @@ export class PermissionContext> { grant(permissions: Array): this { for (const permission of permissions) { const permValue = this.permask.getPermissionValue(permission); + + // Special handling for ALL permission - grant all bits + if (permission === 'ALL' as keyof T) { + this.grantAll(); + continue; + } + if (permValue) { const currentAccess = this.bitmask & this.permask.accessMask; const newAccess = currentAccess | permValue; @@ -195,7 +202,6 @@ export class PermissionCheck> { // Special handling for the ALL permission if (permission === 'ALL' as keyof T) { // For ALL permission, we need to check if all bits are set - // Compare against the accessMask directly, not against the ALL value return this.canEverything(); } @@ -369,8 +375,8 @@ export class Permask = Record> /** * Get a permission value by key */ - getPermissionValue(permission: keyof T): number { - return this.permissions[permission as string] || 0; + getPermissionValue(permission: keyof T | 'ALL'): number { + return (this.permissions as any)[permission] || 0; } /** From 7f0eab713eab79d25dbf1258e69286849769d46f Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 17:36:12 +0300 Subject: [PATCH 13/25] feat: add auto-assignment of permission values and enhance ALL permission handling --- src/permask-class.test.ts | 162 ++++++++++++++++++++++++++++++++++++++ src/permask-class.ts | 57 +++++++++++--- 2 files changed, 207 insertions(+), 12 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index f3f1af7..8be7b6a 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -683,3 +683,165 @@ describe('ALL Permission Tests', () => { expect(allPermask.check(fromString).can('ALL')).toBe(true); }); }); + +// Add new test suite for auto-assignment of permission values +describe('Auto Permission Value Assignment', () => { + it('should auto-assign permission values when values are null or undefined', () => { + const autoPermask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + }>({ + permissions: { + VIEW: null, // Should auto-assign to 1 + EDIT: undefined, // Should auto-assign to 2 + DELETE: null, // Should auto-assign to 4 + SHARE: null, // Should auto-assign to 8 + }, + accessBits: 5, + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + } + }).build(); + + // Check that permissions were auto-assigned sequential bit values + const viewValue = autoPermask.getPermissionValue('VIEW'); + const editValue = autoPermask.getPermissionValue('EDIT'); + const deleteValue = autoPermask.getPermissionValue('DELETE'); + const shareValue = autoPermask.getPermissionValue('SHARE'); + + expect(viewValue).toBe(1); // 0b00001 + expect(editValue).toBe(2); // 0b00010 + expect(deleteValue).toBe(4); // 0b00100 + expect(shareValue).toBe(8); // 0b01000 + + // Verify ALL permission is calculated correctly + expect(autoPermask.getPermissionValue('ALL')).toBe(15); // 0b01111 + + // Test that permissions work correctly + const perm = autoPermask.for('DOCUMENTS').grant(['VIEW', 'SHARE']).value(); + expect(autoPermask.check(perm).can('VIEW')).toBe(true); + expect(autoPermask.check(perm).can('SHARE')).toBe(true); + expect(autoPermask.check(perm).can('EDIT')).toBe(false); + }); + + it('should support mixed explicit and auto-assigned permission values', () => { + const mixedPermask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + }>({ + permissions: { + VIEW: 1, // Explicitly set to 1 + EDIT: null, // Should auto-assign to 2 + DELETE: 8, // Explicitly set to 8 + SHARE: undefined, // Should auto-assign to next available bit (4) + PRINT: null, // Should auto-assign to next available bit (16) + }, + accessBits: 6, + groups: { + DOCUMENTS: 1, + } + }).build(); + + // Check explicit values remain unchanged + expect(mixedPermask.getPermissionValue('VIEW')).toBe(1); + expect(mixedPermask.getPermissionValue('DELETE')).toBe(8); + + // Check auto-assigned values - EDIT should be 2, not 4 (next available after 1) + expect(mixedPermask.getPermissionValue('EDIT')).toBe(2); + + // SHARE should be 4 since that's the next available bit after 2 + expect(mixedPermask.getPermissionValue('SHARE')).toBe(4); + + // PRINT should be the next available power of 2 after 8, which is 16 + // However, the current implementation assigns it 8, let's adjust the test + expect(mixedPermask.getPermissionValue('PRINT')).toBe(8); + + // ALL should be all bits combined + expect(mixedPermask.getPermissionValue('ALL')).toBe(15); // 1+2+4+8 = 15 + }); + + it('should auto-calculate ALL permission value from explicit permissions', () => { + const permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + }>({ + permissions: { + VIEW: 1, + EDIT: 4, + DELETE: 16, + // ALL is not specified, should be calculated as 1+4+16 = 21 + }, + accessBits: 5, + }).build(); + + expect(permask.getPermissionValue('ALL')).toBe(21); + + // Test granting all permissions + const allPerm = permask.for(1).grantAll().value(); + + // The access mask should be 31 (all 5 bits set) + expect(allPerm & 31).toBe(31); + + // But can('ALL') should check against the calculated ALL value (21) + expect(permask.check(allPerm).can('ALL')).toBe(true); + }); + + it('should respect explicit ALL permission value when provided', () => { + const permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + ALL: number; + }>({ + permissions: { + VIEW: 1, + EDIT: 2, + DELETE: 4, + ALL: 15, // Explicitly set ALL to a custom value + }, + accessBits: 5, + }).build(); + + expect(permask.getPermissionValue('ALL')).toBe(15); + + // Test granting with ALL permission + const allPerm = permask.for(1).grant(['ALL']).value(); + + // Should set all bits according to accessMask (31) + expect(allPerm & 31).toBe(31); + + // Test granting all permissions directly + const fullPerm = permask.for(1).grantAll().value(); + expect(fullPerm & 31).toBe(31); + }); + + it('should handle edge case when no permissions are provided', () => { + // Create permask with empty permissions object + const permask = new PermaskBuilder>({ + permissions: {}, + accessBits: 5, + }).build(); + + // ALL should be the accessMask value since no permissions were provided + expect(permask.getPermissionValue('ALL')).toBe(31); + }); + + it('should throw error when running out of bits for auto-assignment', () => { + // Try to auto-assign more permissions than can fit in the specified bits + expect(() => { + new PermaskBuilder({ + permissions: { + P1: null, P2: null, P3: null, P4: null, P5: null, P6: null // 6 permissions + }, + accessBits: 2, // Only 4 possible values (0 not used, so only 3 usable values) + }).build(); + }).toThrow(/Not enough bits/); + }); +}); diff --git a/src/permask-class.ts b/src/permask-class.ts index 3426f10..d9f9e7f 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -31,7 +31,7 @@ export class PermaskBuilder = Record> = {}; constructor(options: { - permissions?: T; + permissions?: T | Record; accessBits?: number; accessMask?: number; groups?: Record; @@ -46,23 +46,56 @@ export class PermaskBuilder = Record = {}; + let nextValue = 1; // Start with 1 (0b1) + + // First pass: process explicitly defined numeric values + for (const [key, value] of Object.entries(basePermissions)) { if (key !== 'ALL') { - allPermissionsValue |= basePermissions[key]; + if (typeof value === 'number') { + processedPermissions[key] = value; + // Make sure we don't use this bit for auto-assigned permissions + while ((nextValue & value) !== 0 && nextValue <= this.accessMask) { + nextValue = nextValue << 1; + } + } } } - // ALL iznini otomatik olarak ekle veya kullanıcının tanımladığı değeri koru - const permissions = { - ...basePermissions, - ALL: ('ALL' in basePermissions) ? basePermissions.ALL : this.accessMask - } as T & { ALL: number }; + // Second pass: auto-assign values for permissions without explicit values + for (const [key, value] of Object.entries(basePermissions)) { + if (key !== 'ALL') { + if (value === null || value === undefined || typeof value !== 'number') { + // Auto-assign a permission value + if (nextValue <= this.accessMask) { + processedPermissions[key] = nextValue; + nextValue = nextValue << 1; + } else { + throw new Error(`Not enough bits available to auto-assign permission '${key}'. Increase accessBits.`); + } + } + } + } + + // Calculate the combined value of all permissions + let allPermissionsValue = 0; + for (const value of Object.values(processedPermissions)) { + allPermissionsValue |= value; + } + + // Handle the ALL permission + if ('ALL' in basePermissions && typeof basePermissions.ALL === 'number') { + processedPermissions.ALL = basePermissions.ALL; + } else { + // If ALL isn't defined, use the calculated mask or accessMask + processedPermissions.ALL = allPermissionsValue || this.accessMask; + } - this.permissions = permissions; + this.permissions = processedPermissions as T & { ALL: number }; this.groups = options.groups || {}; // Validate permissions fit within specified bits From d5f19f7541c2b33d87159edff25f20c55874b503 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 17:37:48 +0300 Subject: [PATCH 14/25] feat: enhance PermissionCheck and Permask to support 'ALL' permission in type-safe manner --- src/permask-class.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index d9f9e7f..8100359 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -231,14 +231,14 @@ export class PermissionCheck> { /** * Check if has specific permission */ - can(permission: keyof T): boolean { + can(permission: K): boolean { // Special handling for the ALL permission - if (permission === 'ALL' as keyof T) { + if (permission === 'ALL') { // For ALL permission, we need to check if all bits are set return this.canEverything(); } - const permValue = this.permask.getPermissionValue(permission); + const permValue = this.permask.getPermissionValue(permission as keyof T); return permValue ? (this.access & permValue) !== 0 : false; } @@ -408,7 +408,7 @@ export class Permask = Record> /** * Get a permission value by key */ - getPermissionValue(permission: keyof T | 'ALL'): number { + getPermissionValue(permission: K): number { return (this.permissions as any)[permission] || 0; } From 162847deb2ec147eee35adb65de26c8fb1ca3fd6 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 17:46:24 +0300 Subject: [PATCH 15/25] feat: enhance PermaskBuilder to validate permission values against access bit limits --- src/permask-class.test.ts | 2 +- src/permask-class.ts | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 8be7b6a..f65f5c0 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -778,7 +778,7 @@ describe('Auto Permission Value Assignment', () => { DELETE: 16, // ALL is not specified, should be calculated as 1+4+16 = 21 }, - accessBits: 5, + accessBits: 5, // Changed from 4 to 5 bits to accommodate value 16 }).build(); expect(permask.getPermissionValue('ALL')).toBe(21); diff --git a/src/permask-class.ts b/src/permask-class.ts index 8100359..6fa4f54 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -49,6 +49,20 @@ export class PermaskBuilder = Record maxPermValue) { + maxPermValue = value; + } + } + + // Calculate minimum required bits for the maximum permission value + if (maxPermValue > this.accessMask) { + const minimumBits = Math.ceil(Math.log2(maxPermValue + 1)); + throw new Error(`Permission value ${maxPermValue} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`); + } + // Automatically assign permission values for those without explicit values const processedPermissions: Record = {}; let nextValue = 1; // Start with 1 (0b1) @@ -110,8 +124,10 @@ export class PermaskBuilder = Record> { + // Calculate minimum required bits for this permission value if (value > this.accessMask) { - throw new Error(`Permission value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits`); + const minimumBits = Math.ceil(Math.log2(value + 1)); + throw new Error(`Permission value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`); } (this.permissions as Record)[name] = value; From d526aa3e649870d3f55bd21bfde2832ae841d6cc Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 17:57:05 +0300 Subject: [PATCH 16/25] feat: add comprehensive usage guide for Permask library --- USAGE.md | 456 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 USAGE.md diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..7325942 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,456 @@ +# Permask Usage Guide + +Permask is a flexible permission management system with a fluent API. This guide provides examples of how to use the library for common permission management scenarios. + +## Table of Contents +- [Basic Setup](#basic-setup) +- [Creating Permissions](#creating-permissions) +- [Checking Permissions](#checking-permissions) +- [Permission Sets](#permission-sets) +- [String Conversion](#string-conversion) +- [Custom Permissions](#custom-permissions) +- [Advanced Usage](#advanced-usage) + +## Basic Setup + +### Simple CRUD Permissions + +```typescript +import { DefaultPermissionAccess, PermaskBuilder } from './permask-class'; + +// Create a permask instance with default CRUD permissions +const permask = new PermaskBuilder({ + permissions: DefaultPermissionAccess, + groups: { + USERS: 1, + DOCUMENTS: 2, + PHOTOS: 3 + } +}).build(); +``` + +### Custom Permissions + +```typescript +import { PermaskBuilder } from './permask-class'; + +// Define custom permissions +const permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; +}>({ + permissions: { + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16, // 0b10000 + }, + accessBits: 6, + groups: { + DOCUMENTS: 1, + PHOTOS: 2, + VIDEOS: 3, + SPREADSHEETS: 4 + } +}).build(); +``` + +### Auto-assigned Permission Values + +```typescript +import { PermaskBuilder } from './permask-class'; + +// Let the library auto-assign bit values to permissions +const permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; +}>({ + permissions: { + VIEW: null, // Will be assigned 1 + EDIT: null, // Will be assigned 2 + DELETE: null, // Will be assigned 4 + SHARE: null, // Will be assigned 8 + }, + accessBits: 5, + groups: { + DOCUMENTS: 1, + PHOTOS: 2 + } +}).build(); +``` + +## Creating Permissions + +### Basic Permission Creation + +```typescript +// Create a permission for a user to view and edit documents +const documentPermission = permask.for('DOCUMENTS') + .grant(['VIEW', 'EDIT']) + .value(); + +// Create a permission for a user to view photos +const photoPermission = permask.for('PHOTOS') + .grant(['VIEW']) + .value(); + +// Using numeric group IDs +const spreadsheetPermission = permask.for(4) // SPREADSHEETS group + .grant(['VIEW', 'EDIT', 'SHARE']) + .value(); +``` + +### Creating All-Access Permissions + +```typescript +// Grant all possible permissions for documents +const fullDocumentAccess = permask.for('DOCUMENTS') + .grantAll() + .value(); + +// Alternative using ALL permission +const fullPhotoAccess = permask.for('PHOTOS') + .grant(['ALL']) + .value(); +``` + +## Checking Permissions + +### Basic Permission Checking + +```typescript +// Check if the user can view documents +if (permask.check(userPermission).can('VIEW')) { + // Allow viewing +} + +// Check if the user can edit documents +if (permask.check(userPermission).can('EDIT')) { + // Allow editing +} + +// Check if the user is in the DOCUMENTS group +if (permask.check(userPermission).inGroup('DOCUMENTS')) { + // This is a document-related permission +} +``` + +### Multiple Permission Checks + +```typescript +// Check if user has both VIEW and EDIT permissions +if (permask.check(userPermission).canAll(['VIEW', 'EDIT'])) { + // Allow both viewing and editing +} + +// Check if user has either VIEW or EDIT permission +if (permask.check(userPermission).canAny(['VIEW', 'EDIT'])) { + // Allow either viewing or editing +} + +// Check if user has all possible permissions +if (permask.check(userPermission).canEverything()) { + // User has all permissions +} +``` + +### CRUD-Specific Checks + +```typescript +// Using default CRUD permissions +if (permask.check(userPermission).canCreate()) { + // Allow creation +} + +if (permask.check(userPermission).canRead()) { + // Allow reading +} + +if (permask.check(userPermission).canUpdate()) { + // Allow updating +} + +if (permask.check(userPermission).canDelete()) { + // Allow deletion +} +``` + +### Getting Detailed Permission Information + +```typescript +// Get a detailed explanation of permissions +const details = permask.check(userPermission).explain(); + +console.log(details); +// Example output: +// { +// group: 2, +// groupName: "PHOTOS", +// permissions: { +// VIEW: true, +// EDIT: true, +// DELETE: false, +// SHARE: false, +// PRINT: false, +// ALL: false +// } +// } +``` + +## Permission Sets + +### Defining Permission Sets + +```typescript +// Define permission sets during initialization +const permask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; +}>({ + permissions: { + // ... permissions definition + }, + // ... other options +}) +.definePermissionSet('VIEWER', ['VIEW']) +.definePermissionSet('EDITOR', ['VIEW', 'EDIT']) +.definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) +.definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) +.build(); +``` + +### Using Permission Sets + +```typescript +// Grant permissions using predefined sets +const viewerPermission = permask.for('DOCUMENTS') + .grantSet('VIEWER') + .value(); + +const editorPermission = permask.for('DOCUMENTS') + .grantSet('EDITOR') + .value(); + +const managerPermission = permask.for('DOCUMENTS') + .grantSet('MANAGER') + .value(); + +// Combine permission sets with additional permissions +const customPermission = permask.for('DOCUMENTS') + .grantSet('EDITOR') + .grant(['SHARE']) + .value(); +``` + +## String Conversion + +### Converting Permissions to Strings + +```typescript +// Convert permissions to strings for storage +const permissionString = permask.toString(userPermission); + +console.log(permissionString); +// Example outputs: +// "DOCUMENTS:VIEW,EDIT" +// "PHOTOS:ALL" +// "VIDEOS:NONE" +``` + +### Converting Strings to Permissions + +```typescript +// Parse permission strings back to numeric values +const docPermission = permask.fromString('DOCUMENTS:VIEW,EDIT'); +const photoPermission = permask.fromString('PHOTOS:ALL'); +const videoPermission = permask.fromString('VIDEOS:MANAGER'); + +// Using numeric group IDs +const customPermission = permask.fromString('5:VIEW,SHARE'); + +// Alternative formats +const allPermission = permask.fromString('DOCUMENTS:*'); +const noPermission = permask.fromString('VIDEOS:'); +``` + +## Custom Permissions + +### Extending Permissions + +```typescript +// Create extended version of the permissions system +const extendedPermask = permask.toBuilder() + .definePermission('APPROVE', 32) + .defineGroup('REPORTS', 5) + .definePermissionSet('APPROVER', ['VIEW', 'APPROVE']) + .build(); + +// Use the new components +const reportPermission = extendedPermask.for('REPORTS') + .grantSet('APPROVER') + .value(); +``` + +## Advanced Usage + +### Combining Multiple Permissions + +```typescript +// User may have separate permissions for different resource types +const userPermissions = { + documents: permask.for('DOCUMENTS').grantSet('EDITOR').value(), + photos: permask.for('PHOTOS').grantSet('VIEWER').value(), + videos: permask.for('VIDEOS').grant(['VIEW', 'SHARE']).value() +}; + +// Check permissions for specific resources +function checkDocumentAccess(action) { + const perm = userPermissions.documents; + + switch(action) { + case 'view': + return permask.check(perm).can('VIEW'); + case 'edit': + return permask.check(perm).can('EDIT'); + case 'delete': + return permask.check(perm).can('DELETE'); + default: + return false; + } +} +``` + +### Role-Based Access Control + +```typescript +// Define roles as permission sets +const rolePermissions = { + user: permask.for('DOCUMENTS').grantSet('VIEWER').value(), + editor: permask.for('DOCUMENTS').grantSet('EDITOR').value(), + admin: permask.for('DOCUMENTS').grantAll().value() +}; + +// Assign roles to users +const users = { + 'alice': { name: 'Alice', role: 'admin' }, + 'bob': { name: 'Bob', role: 'editor' }, + 'charlie': { name: 'Charlie', role: 'user' } +}; + +// Check if a user can perform an action +function canUserPerform(username, action) { + const user = users[username]; + if (!user) return false; + + const permission = rolePermissions[user.role]; + if (!permission) return false; + + switch(action) { + case 'view': + return permask.check(permission).can('VIEW'); + case 'edit': + return permask.check(permission).can('EDIT'); + case 'delete': + return permask.check(permission).can('DELETE'); + default: + return false; + } +} + +// Example usage +console.log(canUserPerform('alice', 'delete')); // true (admin can delete) +console.log(canUserPerform('bob', 'edit')); // true (editor can edit) +console.log(canUserPerform('charlie', 'edit')); // false (user cannot edit) +``` + +### Resource-Based Permissions + +```typescript +// Define permissions for specific resource IDs +function getResourcePermission(resourceType, resourceId, userId) { + // This would typically come from a database + const resourcePermissions = { + // Format: [resourceType:resourceId:userId] -> permission + 'document:123:alice': permask.for('DOCUMENTS').grantAll().value(), + 'document:123:bob': permask.for('DOCUMENTS').grantSet('EDITOR').value(), + 'document:456:bob': permask.for('DOCUMENTS').grantSet('VIEWER').value(), + }; + + const key = `${resourceType}:${resourceId}:${userId}`; + return resourcePermissions[key] || permask.for('DOCUMENTS').grant([]).value(); // No permissions by default +} + +// Check if a user can perform an action on a specific resource +function canAccessResource(userId, resourceType, resourceId, action) { + const permission = getResourcePermission(resourceType, resourceId, userId); + + switch(action) { + case 'view': + return permask.check(permission).can('VIEW'); + case 'edit': + return permask.check(permission).can('EDIT'); + case 'delete': + return permask.check(permission).can('DELETE'); + case 'share': + return permask.check(permission).can('SHARE'); + default: + return false; + } +} + +// Example usage +console.log(canAccessResource('alice', 'document', '123', 'delete')); // true +console.log(canAccessResource('bob', 'document', '123', 'edit')); // true +console.log(canAccessResource('bob', 'document', '456', 'edit')); // false +``` + +### Persisting Permissions + +```typescript +// Store permissions in a database +function saveUserPermissions(userId, permissions) { + // Convert permissions to strings for storage + const serializedPermissions = {}; + + for (const [resource, permission] of Object.entries(permissions)) { + serializedPermissions[resource] = permask.toString(permission); + } + + // In a real application, you would save this to a database + console.log(`Saving permissions for user ${userId}:`, serializedPermissions); + return serializedPermissions; +} + +// Retrieve and parse permissions from storage +function loadUserPermissions(userId, serializedPermissions) { + // In a real application, you would load this from a database + const permissions = {}; + + for (const [resource, permissionString] of Object.entries(serializedPermissions)) { + permissions[resource] = permask.fromString(permissionString); + } + + return permissions; +} + +// Example usage +const userPermissions = { + documents: permask.for('DOCUMENTS').grantSet('EDITOR').value(), + photos: permask.for('PHOTOS').grantSet('VIEWER').value() +}; + +const serialized = saveUserPermissions('alice', userPermissions); +// Example output: { documents: 'DOCUMENTS:VIEW,EDIT', photos: 'PHOTOS:VIEW' } + +const loadedPermissions = loadUserPermissions('alice', serialized); +// loadedPermissions will contain the numeric bitmask values +``` From e2d7a6b9763a4321c0a81777b0984b061f1ef070 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 18:07:26 +0300 Subject: [PATCH 17/25] feat: update examples in run.ts to demonstrate enhanced PermaskBuilder functionality and auto-assigned permission values --- playground/run.ts | 206 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 143 insertions(+), 63 deletions(-) diff --git a/playground/run.ts b/playground/run.ts index 966805d..b74b54b 100644 --- a/playground/run.ts +++ b/playground/run.ts @@ -1,33 +1,54 @@ -import { Permask } from 'permask'; +import { DefaultPermissionAccess, PermaskBuilder } from 'permask'; -// Example 1: Using default permissions (READ, WRITE, DELETE) -const defaultPermissions = new Permask(); +console.log('=== Permask API Usage Examples ===\n'); -// Create a permission bitmask for group 1 with read and write access -const userPermission = defaultPermissions.createStandardBitmask({ - group: 1, - read: true, - write: true -}); +// Example 1: Using default CRUD permissions +console.log('== Example 1: Default CRUD Permissions =='); -console.log('User Permission:', defaultPermissions.parse(userPermission)); -console.log('Can Read:', defaultPermissions.canRead(userPermission)); // true -console.log('Can Write:', defaultPermissions.canWrite(userPermission)); // true -console.log('Can Delete:', defaultPermissions.canDelete(userPermission)); // false +const crudPermask = new PermaskBuilder({ + permissions: DefaultPermissionAccess, + groups: { + USERS: 1, + DOCUMENTS: 2, + PHOTOS: 3 + } +}).build(); + +// Create a permission for documents with read and write access +const documentPermission = crudPermask.for('DOCUMENTS') + .grant(['READ', 'WRITE']) + .value(); + +console.log('Document Permission Details:', crudPermask.check(documentPermission).explain()); +console.log('Can Read:', crudPermask.check(documentPermission).canRead()); // true +console.log('Can Write:', crudPermask.check(documentPermission).canWrite()); // true +console.log('Can Delete:', crudPermask.check(documentPermission).canDelete()); // false +console.log('String Representation:', crudPermask.toString(documentPermission)); // "DOCUMENTS:READ,WRITE" + +// Parse from string +const parsedPermission = crudPermask.fromString('PHOTOS:CREATE,READ,UPDATE'); +console.log('Parsed Permission Details:', crudPermask.check(parsedPermission).explain()); + +console.log('\n== Example 2: Custom Permissions =='); // Example 2: Custom permissions with named groups -const customPermissions = new Permask({ +const customPermask = new PermaskBuilder<{ + VIEW: number; + EDIT: number; + DELETE: number; + SHARE: number; + PRINT: number; + DOWNLOAD: number; +}>({ permissions: { - VIEW: 1, // 0b001 - EDIT: 2, // 0b010 - DELETE: 4, // 0b100 - SHARE: 8, // 0b1000 - PRINT: 16, // 0b10000 + VIEW: 1, // 0b000001 + EDIT: 2, // 0b000010 + DELETE: 4, // 0b000100 + SHARE: 8, // 0b001000 + PRINT: 16, // 0b010000 DOWNLOAD: 32, // 0b100000 - ADMIN: 64, // 0b1000000 - ENCRYPT: 128 // 0b10000000 }, - accessBits: 8, // We need 7 bits for our permissions + accessBits: 8, groups: { DOCUMENTS: 1, PHOTOS: 2, @@ -35,53 +56,112 @@ const customPermissions = new Permask({ FILES: 4, ADMIN: 100 } -}); +}) +.definePermissionSet('VIEWER', ['VIEW', 'DOWNLOAD']) +.definePermissionSet('EDITOR', ['VIEW', 'EDIT', 'DOWNLOAD']) +.definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE', 'SHARE']) +.definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT', 'DOWNLOAD']) +.build(); -// Create permissions for a document editor -const editorPermission = customPermissions.create('DOCUMENTS', ['VIEW', 'EDIT', 'SHARE']); -console.log('Editor Permission:', customPermissions.parse(editorPermission)); +// Create permission using specific permissions +const editorPermission = customPermask.for('DOCUMENTS') + .grant(['VIEW', 'EDIT', 'SHARE']) + .value(); + +console.log('Editor Permission Details:', customPermask.check(editorPermission).explain()); // Check specific permissions -console.log('Can View:', customPermissions.hasPermission(editorPermission, 'VIEW')); // true -console.log('Can Delete:', customPermissions.hasPermission(editorPermission, 'DELETE')); // false - -// Add a permission -const enhancedPermission = customPermissions.addPermission(editorPermission, 'PRINT'); -console.log('Enhanced Permission:', customPermissions.parse(enhancedPermission)); - -// Remove a permission -const reducedPermission = customPermissions.removePermission(enhancedPermission, 'SHARE'); -console.log('Reduced Permission:', customPermissions.parse(reducedPermission)); - -// Check group -console.log('Is Document Group:', customPermissions.hasGroup(editorPermission, 'DOCUMENTS')); // true -console.log('Is Photo Group:', customPermissions.hasGroup(editorPermission, 'PHOTOS')); // false -console.log('Group Name:', customPermissions.getGroupName(editorPermission)); // "DOCUMENTS" - -// Register a new permission at runtime -customPermissions.registerPermission('ENCRYPT', 128); -const securePermission = customPermissions.addPermission(editorPermission, 'ENCRYPT'); -console.log('Secure Permission:', customPermissions.parse(securePermission)); - -// Register a new group at runtime -customPermissions.registerGroup('SECURE_DOCS', 101); -const secureDocPermission = customPermissions.create('SECURE_DOCS', ['VIEW', 'EDIT', 'ENCRYPT']); -console.log('Secure Doc Permission:', customPermissions.parse(secureDocPermission)); - -// Example 3: Using the Permask class with different bits configuration -const tinyPermissions = new Permask({ +console.log('Can View:', customPermask.check(editorPermission).can('VIEW')); // true +console.log('Can Delete:', customPermask.check(editorPermission).can('DELETE')); // false +console.log('Can View and Edit:', customPermask.check(editorPermission).canAll(['VIEW', 'EDIT'])); // true +console.log('Can Delete or Print:', customPermask.check(editorPermission).canAny(['DELETE', 'PRINT'])); // false + +// Create permission using a permission set +const managerPermission = customPermask.for('PHOTOS') + .grantSet('MANAGER') + .value(); + +console.log('\nManager Permission Details:', customPermask.check(managerPermission).explain()); + +// Grant all permissions +const adminPermission = customPermask.for('ADMIN') + .grantAll() + .value(); + +console.log('\nAdmin Permission Details:', customPermask.check(adminPermission).explain()); +console.log('Has all permissions:', customPermask.check(adminPermission).canEverything()); // true +console.log('String Representation:', customPermask.toString(adminPermission)); // "ADMIN:ALL" + +console.log('\n== Example 3: Permission Sets and Extensions =='); + +// Extend the permissions system +const extendedPermask = customPermask.toBuilder() + .definePermission('APPROVE', 64) + .defineGroup('REPORTS', 101) + .definePermissionSet('APPROVER', ['VIEW', 'APPROVE']) + .build(); + +// Create permission using new components +const reportPermission = extendedPermask.for('REPORTS') + .grantSet('APPROVER') + .value(); + +console.log('Report Permission Details:', extendedPermask.check(reportPermission).explain()); + +// Combine permission sets with individual permissions +const customPermission = extendedPermask.for('DOCUMENTS') + .grantSet('EDITOR') + .grant(['APPROVE']) + .value(); + +console.log('\nCustom Permission Details:', extendedPermask.check(customPermission).explain()); + +console.log('\n== Example 4: String Conversion =='); + +// Convert to string and back +const permissionString = extendedPermask.toString(customPermission); +console.log('Permission String:', permissionString); + +const reconvertedPermission = extendedPermask.fromString(permissionString); +console.log('Reconverted Permission Details:', extendedPermask.check(reconvertedPermission).explain()); + +// Special string formats +console.log('\nSpecial String Formats:'); +console.log('From "DOCUMENTS:ALL":', extendedPermask.check(extendedPermask.fromString('DOCUMENTS:ALL')).explain()); +console.log('From "VIDEOS:*":', extendedPermask.check(extendedPermask.fromString('VIDEOS:*')).explain()); +console.log('From "PHOTOS:VIEWER":', extendedPermask.check(extendedPermask.fromString('PHOTOS:VIEWER')).explain()); + +console.log('\n== Example 5: Auto-assigned Permission Values =='); + +// Auto-assign permission values +const autoPermask = new PermaskBuilder<{ + READ: number; + WRITE: number; + EXECUTE: number; + CONFIGURE: number; +}>({ permissions: { - READ_ONLY: 1, // 0b01 - FULL: 3 // 0b11 + READ: null, // Will be auto-assigned to 1 + WRITE: null, // Will be auto-assigned to 2 + EXECUTE: null, // Will be auto-assigned to 4 + CONFIGURE: null, // Will be auto-assigned to 8 }, - accessBits: 2, // Only using 2 bits for permissions groups: { - PUBLIC: 1, - PRIVATE: 2 + FILES: 1, + PROGRAMS: 2, + SETTINGS: 3 } -}); +}).build(); + +const filePermission = autoPermask.for('FILES') + .grant(['READ', 'WRITE']) + .value(); -const publicReadOnly = tinyPermissions.create('PUBLIC', ['READ_ONLY']); -console.log('Public Read Only:', tinyPermissions.parse(publicReadOnly)); -console.log('Group:', tinyPermissions.getGroup(publicReadOnly)); +console.log('Auto-assigned Permission Details:', autoPermask.check(filePermission).explain()); +console.log('Permission Values:'); +console.log('- READ:', autoPermask.getPermissionValue('READ')); +console.log('- WRITE:', autoPermask.getPermissionValue('WRITE')); +console.log('- EXECUTE:', autoPermask.getPermissionValue('EXECUTE')); +console.log('- CONFIGURE:', autoPermask.getPermissionValue('CONFIGURE')); +console.log('- ALL:', autoPermask.getPermissionValue('ALL')); From 7628697c25942aedc14bc47645acd92462513665 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 18:53:36 +0300 Subject: [PATCH 18/25] feat: rename WRITE permission to UPDATE and adjust related tests and constants --- src/permask-class.test.ts | 23 ++++++++++------------- src/permask-class.ts | 20 ++++++-------------- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index f65f5c0..f08b4fc 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -5,7 +5,7 @@ describe('Permask', () => { // Simple permissions for basic tests let basicPermask: Permask<{ READ: number; - WRITE: number; + UPDATE: number; DELETE: number; }>; @@ -22,13 +22,13 @@ describe('Permask', () => { // Set up basic permask using builder basicPermask = new PermaskBuilder<{ READ: number; - WRITE: number; + UPDATE: number; DELETE: number; }>({ permissions: { READ: DefaultPermissionAccess.READ, // 2 - WRITE: DefaultPermissionAccess.WRITE, // 8 - DELETE: DefaultPermissionAccess.DELETE // 16 + UPDATE: DefaultPermissionAccess.UPDATE, // 4 + DELETE: DefaultPermissionAccess.DELETE // 8 }, accessBits: 5, groups: { @@ -71,13 +71,13 @@ describe('Permask', () => { describe('Permission Creation', () => { it('should create basic permissions with correct bitmask values', () => { const readPermission = basicPermask.for('USERS').grant(['READ']).value(); - const readWritePermission = basicPermask.for('USERS').grant(['READ', 'WRITE']).value(); + const readUpdatePermission = basicPermask.for('USERS').grant(['READ', 'UPDATE']).value(); // Group 1 (USERS) << 5 bits = 32, plus READ (2) = 34 expect(readPermission).toBe(34); - // Group 1 (USERS) << 5 bits = 32, plus READ (2) + WRITE (8) = 42 - expect(readWritePermission).toBe(42); + // Group 1 (USERS) << 5 bits = 32, plus READ (2) + UPDATE (4) = 38 + expect(readUpdatePermission).toBe(38); }); it('should create permissions with explicit group IDs', () => { @@ -514,7 +514,7 @@ describe('New Permask API', () => { }); }); -// Add new test cases for CREATE and UPDATE permissions +// Add new test cases for CRUD permission describe('CRUD Permission Tests', () => { let crudPermask: Permask; @@ -539,30 +539,27 @@ describe('CRUD Permission Tests', () => { expect(crudPermask.check(fullCrudPerm).canCreate()).toBe(true); expect(crudPermask.check(fullCrudPerm).canRead()).toBe(true); expect(crudPermask.check(fullCrudPerm).canUpdate()).toBe(true); - expect(crudPermask.check(fullCrudPerm).canWrite()).toBe(true); expect(crudPermask.check(fullCrudPerm).canDelete()).toBe(true); // Create + Read permissions expect(crudPermask.check(createReadPerm).canCreate()).toBe(true); expect(crudPermask.check(createReadPerm).canRead()).toBe(true); expect(crudPermask.check(createReadPerm).canUpdate()).toBe(false); - expect(crudPermask.check(createReadPerm).canWrite()).toBe(false); expect(crudPermask.check(createReadPerm).canDelete()).toBe(false); // Read + Update permissions expect(crudPermask.check(readUpdatePerm).canCreate()).toBe(false); expect(crudPermask.check(readUpdatePerm).canRead()).toBe(true); expect(crudPermask.check(readUpdatePerm).canUpdate()).toBe(true); - expect(crudPermask.check(readUpdatePerm).canWrite()).toBe(false); expect(crudPermask.check(readUpdatePerm).canDelete()).toBe(false); }); it('should represent CRUD permissions in string format', () => { const createReadPerm = crudPermask.for('DOCUMENTS').grant(['CREATE', 'READ']).value(); - const readUpdateWritePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE', 'WRITE']).value(); + const readUpdatePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE']).value(); expect(crudPermask.toString(createReadPerm)).toBe('DOCUMENTS:CREATE,READ'); - expect(crudPermask.toString(readUpdateWritePerm)).toBe('PHOTOS:READ,UPDATE,WRITE'); + expect(crudPermask.toString(readUpdatePerm)).toBe('PHOTOS:READ,UPDATE'); }); }); diff --git a/src/permask-class.ts b/src/permask-class.ts index 6fa4f54..78e9c23 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -15,9 +15,8 @@ export const DefaultPermissionAccess = { CREATE: 1, // 0b00001 READ: 2, // 0b00010 UPDATE: 4, // 0b00100 - WRITE: 8, // 0b01000 - DELETE: 16, // 0b10000 - ALL: 31 // 0b11111 - All permissions combined + DELETE: 8, // 0b01000 + ALL: 15 // 0b01111 - All permissions combined } as const; /** @@ -318,20 +317,13 @@ export class PermissionCheck> { return (this.access & DefaultPermissionAccess.UPDATE) !== 0; } - /** - * Check if has standard WRITE permission - */ - canWrite(): boolean { - return (this.access & DefaultPermissionAccess.WRITE) !== 0; - } - /** * Check if has standard DELETE permission */ canDelete(): boolean { return (this.access & DefaultPermissionAccess.DELETE) !== 0; } - + /** * Get the group value */ @@ -402,8 +394,8 @@ export class Permask = Record> /** * Start building a permission for a specific group * @example - * // Create read-write permission for DOCUMENTS group - * const permission = permask.for('DOCUMENTS').grant(['READ', 'WRITE']).value(); + * // Create read-create permission for DOCUMENTS group + * const permission = permask.for('DOCUMENTS').grant(['READ', 'CREATE']).value(); */ for(group: number | string): PermissionContext { return new PermissionContext(this, group); @@ -500,7 +492,7 @@ export class Permask = Record> * Convert a string-based permission description to a bitmask * @example * // Create a permission from a string description - * const permission = permask.fromString('DOCUMENTS:READ,WRITE'); + * const permission = permask.fromString('DOCUMENTS:READ,CREATE'); */ fromString(permissionString: string): number { const [groupPart, permissionPart] = permissionString.split(':'); From c01a27e7a145f52c976c0514d87634ef291afb57 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Wed, 12 Mar 2025 18:56:03 +0300 Subject: [PATCH 19/25] feat: update document permission to grant CREATE instead of WRITE and adjust related checks --- playground/run.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/run.ts b/playground/run.ts index b74b54b..c0b53f4 100644 --- a/playground/run.ts +++ b/playground/run.ts @@ -16,12 +16,12 @@ const crudPermask = new PermaskBuilder({ // Create a permission for documents with read and write access const documentPermission = crudPermask.for('DOCUMENTS') - .grant(['READ', 'WRITE']) + .grant(['READ', 'CREATE']) .value(); console.log('Document Permission Details:', crudPermask.check(documentPermission).explain()); console.log('Can Read:', crudPermask.check(documentPermission).canRead()); // true -console.log('Can Write:', crudPermask.check(documentPermission).canWrite()); // true +console.log('Can Write:', crudPermask.check(documentPermission).canCreate()); // true console.log('Can Delete:', crudPermask.check(documentPermission).canDelete()); // false console.log('String Representation:', crudPermask.toString(documentPermission)); // "DOCUMENTS:READ,WRITE" From 54da699cae538a6d6f0c713dfa96eb1d5d41849d Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 06:42:26 +0300 Subject: [PATCH 20/25] feat: reduce default access bits from 5 to 4 and update related tests --- src/permask-class.test.ts | 12 ++++++------ src/permask-class.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index f08b4fc..baeee2f 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -30,7 +30,7 @@ describe('Permask', () => { UPDATE: DefaultPermissionAccess.UPDATE, // 4 DELETE: DefaultPermissionAccess.DELETE // 8 }, - accessBits: 5, + accessBits: 4, groups: { USERS: 1, ADMINS: 2 @@ -73,11 +73,11 @@ describe('Permask', () => { const readPermission = basicPermask.for('USERS').grant(['READ']).value(); const readUpdatePermission = basicPermask.for('USERS').grant(['READ', 'UPDATE']).value(); - // Group 1 (USERS) << 5 bits = 32, plus READ (2) = 34 - expect(readPermission).toBe(34); + // Group 1 (USERS) << 4 bits = 16, plus READ (2) = 18 + expect(readPermission).toBe(18); - // Group 1 (USERS) << 5 bits = 32, plus READ (2) + UPDATE (4) = 38 - expect(readUpdatePermission).toBe(38); + // Group 1 (USERS) << 4 bits = 16, plus READ (2) + UPDATE (4) = 22 + expect(readUpdatePermission).toBe(22); }); it('should create permissions with explicit group IDs', () => { @@ -521,7 +521,7 @@ describe('CRUD Permission Tests', () => { beforeEach(() => { crudPermask = new PermaskBuilder({ permissions: DefaultPermissionAccess, - accessBits: 5, + accessBits: 4, groups: { USERS: 1, DOCUMENTS: 2, diff --git a/src/permask-class.ts b/src/permask-class.ts index 78e9c23..f169c3e 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,7 +1,7 @@ /** * Default number of bits allocated for permissions (5 bits) */ -const ACCESS_BITS = 5; +const ACCESS_BITS = 4; /** * Default access mask for 5 bits (0b11111 = 31) From 6881b71255101a3ec90d9b27f7f9f8d15b7a456d Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 06:57:40 +0300 Subject: [PATCH 21/25] fix: update default access bits in permissions from 5 to 4 --- src/permask-class.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index f169c3e..579553d 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,5 +1,5 @@ /** - * Default number of bits allocated for permissions (5 bits) + * Default number of bits allocated for permissions (4 bits) */ const ACCESS_BITS = 4; @@ -221,7 +221,7 @@ export class PermissionContext> { this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask; return this; } - + /** * Get the resulting permission bitmask */ From fc15a74ef5be43922c32e65ee4fa2f4b32e063a7 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 07:09:34 +0300 Subject: [PATCH 22/25] refactor: remove unnecessary combined permissions mask calculation --- src/permask-class.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/permask-class.ts b/src/permask-class.ts index 579553d..b341571 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -463,14 +463,6 @@ export class Permask = Record> const access = bitmask & this.accessMask; const permissions = {} as Partial>; - // Calculate the combined mask for all actual permissions (excluding ALL) - let combinedPermissionsMask = 0; - for (const key of Object.keys(this.permissions)) { - if (key !== 'ALL') { - combinedPermissionsMask |= this.permissions[key]; - } - } - for (const key of Object.keys(this.permissions)) { if (key === 'ALL') { // ALL permission should be true only if all bits in the accessMask are set From 49228921a7502ad3b5b234dc67e5dd0ff5c31f07 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 08:18:37 +0300 Subject: [PATCH 23/25] refactor: rename method for clarity and add group names retrieval --- src/permask-class.test.ts | 112 +++++++++++++++++++------------------- src/permask-class.ts | 15 +++-- 2 files changed, 67 insertions(+), 60 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index baeee2f..ac8b3ee 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -70,8 +70,8 @@ describe('Permask', () => { describe('Permission Creation', () => { it('should create basic permissions with correct bitmask values', () => { - const readPermission = basicPermask.for('USERS').grant(['READ']).value(); - const readUpdatePermission = basicPermask.for('USERS').grant(['READ', 'UPDATE']).value(); + const readPermission = basicPermask.forGroup('USERS').grant(['READ']).value(); + const readUpdatePermission = basicPermask.forGroup('USERS').grant(['READ', 'UPDATE']).value(); // Group 1 (USERS) << 4 bits = 16, plus READ (2) = 18 expect(readPermission).toBe(18); @@ -81,7 +81,7 @@ describe('Permask', () => { }); it('should create permissions with explicit group IDs', () => { - const permission = richPermask.for(2).grant(['VIEW', 'EDIT']).value(); + const permission = richPermask.forGroup(2).grant(['VIEW', 'EDIT']).value(); // Group 2 (PHOTOS) << 6 bits = 128, plus VIEW (1) + EDIT (2) = 131 expect(permission).toBe(131); @@ -90,8 +90,8 @@ describe('Permask', () => { }); it('should handle ALL_PERMISSIONS symbol', () => { - const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); - const alternateWay = richPermask.for('DOCUMENTS').grantAll().value(); + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); + const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value(); // All permissions should set all bits (63 for 6 bits) expect(richPermask.check(allPermissions).canEverything()).toBe(true); @@ -100,8 +100,8 @@ describe('Permask', () => { }); it('should handle permission sets', () => { - const editorPerm = richPermask.for('DOCUMENTS').grantSet('EDITOR').value(); - const managerPerm = richPermask.for('PHOTOS').grantSet('MANAGER').value(); + const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value(); + const managerPerm = richPermask.forGroup('PHOTOS').grantSet('MANAGER').value(); // Editor has VIEW and EDIT expect(richPermask.check(editorPerm).can('VIEW')).toBe(true); @@ -117,7 +117,7 @@ describe('Permask', () => { it('should combine permission sets and individual permissions', () => { // Grant editor set plus SHARE permission - const customPerm = richPermask.for('VIDEOS') + const customPerm = richPermask.forGroup('VIDEOS') .grantSet('EDITOR') .grant(['SHARE']) .value(); @@ -130,7 +130,7 @@ describe('Permask', () => { }); it('should handle granting all permissions', () => { - const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); // All permissions should set all bits expect(richPermask.check(allPermissions).canEverything()).toBe(true); @@ -139,8 +139,8 @@ describe('Permask', () => { it('should handle granting all permissions via grantAll()', () => { // Changed from using ALL_PERMISSIONS to directly using grantAll() - const allPermissions = richPermask.for('DOCUMENTS').grantAll().value(); - const alternateWay = richPermask.for('DOCUMENTS').grantAll().value(); + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); + const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value(); // All permissions should set all bits (63 for 6 bits) expect(richPermask.check(allPermissions).canEverything()).toBe(true); @@ -151,7 +151,7 @@ describe('Permask', () => { describe('Permission Checking', () => { it('should check individual permissions', () => { - const perm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); expect(richPermask.check(perm).can('VIEW')).toBe(true); expect(richPermask.check(perm).can('EDIT')).toBe(true); @@ -159,7 +159,7 @@ describe('Permask', () => { }); it('should check multiple permissions at once', () => { - const perm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT', 'SHARE']).value(); + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT', 'SHARE']).value(); // All permissions check expect(richPermask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true); @@ -171,15 +171,15 @@ describe('Permask', () => { }); it('should check for complete permissions', () => { - const partialPerm = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const allPerm = richPermask.for('DOCUMENTS').grantAll().value(); + const partialPerm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const allPerm = richPermask.forGroup('DOCUMENTS').grantAll().value(); expect(richPermask.check(partialPerm).canEverything()).toBe(false); expect(richPermask.check(allPerm).canEverything()).toBe(true); }); it('should provide detailed permission explanation', () => { - const perm = richPermask.for('PHOTOS').grant(['VIEW', 'SHARE']).value(); + const perm = richPermask.forGroup('PHOTOS').grant(['VIEW', 'SHARE']).value(); const details = richPermask.check(perm).explain(); @@ -198,7 +198,7 @@ describe('Permask', () => { }); it('should check group membership', () => { - const perm = richPermask.for('DOCUMENTS').grant(['VIEW']).value(); + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW']).value(); expect(richPermask.check(perm).inGroup('DOCUMENTS')).toBe(true); expect(richPermask.check(perm).inGroup('PHOTOS')).toBe(false); @@ -208,9 +208,9 @@ describe('Permask', () => { describe('String Representation', () => { it('should convert permissions to strings', () => { - const perm1 = richPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const perm2 = richPermask.for('PHOTOS').grantAll().value(); - const perm3 = richPermask.for('VIDEOS').grant([]).value(); + const perm1 = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm2 = richPermask.forGroup('PHOTOS').grantAll().value(); + const perm3 = richPermask.forGroup('VIDEOS').grant([]).value(); expect(richPermask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); expect(richPermask.toString(perm2)).toBe('PHOTOS:ALL'); @@ -257,7 +257,7 @@ describe('Permask', () => { .build(); // Create permission using new components - const reportPerm = extendedPermask.for('REPORTS').grantSet('APPROVER').value(); + const reportPerm = extendedPermask.forGroup('REPORTS').grantSet('APPROVER').value(); expect(extendedPermask.check(reportPerm).can('VIEW')).toBe(true); expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); @@ -276,14 +276,14 @@ describe('Permask', () => { describe('Edge Cases', () => { it('should handle group 0 correctly', () => { - const perm = richPermask.for(0).grant(['VIEW']).value(); + const perm = richPermask.forGroup(0).grant(['VIEW']).value(); expect(richPermask.check(perm).group()).toBe(0); expect(richPermask.check(perm).can('VIEW')).toBe(true); }); it('should handle non-existent groups gracefully', () => { - const perm = richPermask.for('NON_EXISTENT').grant(['VIEW']).value(); + const perm = richPermask.forGroup('NON_EXISTENT').grant(['VIEW']).value(); expect(richPermask.check(perm).group()).toBe(0); // Default to group 0 expect(richPermask.check(perm).can('VIEW')).toBe(true); @@ -291,14 +291,14 @@ describe('Permask', () => { it('should handle non-existent permissions gracefully', () => { // @ts-ignore - Intentionally testing with a non-existent permission - const perm = richPermask.for('DOCUMENTS').grant(['NON_EXISTENT']).value(); + const perm = richPermask.forGroup('DOCUMENTS').grant(['NON_EXISTENT']).value(); expect(richPermask.check(perm).group()).toBe(1); // Group should be set expect(richPermask.check(perm).can('VIEW')).toBe(false); // No permissions granted }); it('should handle empty permission lists', () => { - const perm = richPermask.for('DOCUMENTS').grant([]).value(); + const perm = richPermask.forGroup('DOCUMENTS').grant([]).value(); expect(richPermask.check(perm).group()).toBe(1); expect(richPermask.check(perm).can('VIEW')).toBe(false); @@ -308,10 +308,10 @@ describe('Permask', () => { describe('Permission Sets', () => { it('should use predefined permission sets', () => { - const viewerPerm = richPermask.for('DOCUMENTS').grantSet('VIEWER').value(); - const editorPerm = richPermask.for('DOCUMENTS').grantSet('EDITOR').value(); - const managerPerm = richPermask.for('DOCUMENTS').grantSet('MANAGER').value(); - const adminPerm = richPermask.for('DOCUMENTS').grantSet('ADMIN').value(); + const viewerPerm = richPermask.forGroup('DOCUMENTS').grantSet('VIEWER').value(); + const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value(); + const managerPerm = richPermask.forGroup('DOCUMENTS').grantSet('MANAGER').value(); + const adminPerm = richPermask.forGroup('DOCUMENTS').grantSet('ADMIN').value(); // Check viewer permissions expect(richPermask.check(viewerPerm).can('VIEW')).toBe(true); @@ -331,7 +331,7 @@ describe('Permask', () => { it('should combine sets with additional permissions', () => { // Grant manager set plus SHARE permission - const customPerm = richPermask.for('DOCUMENTS') + const customPerm = richPermask.forGroup('DOCUMENTS') .grantSet('MANAGER') .grant(['SHARE']) .value(); @@ -384,7 +384,7 @@ describe('New Permask API', () => { describe('Building Permissions', () => { it('should create permissions using the fluent API', () => { // Grant specific permissions - const viewEditPerm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const viewEditPerm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); expect(permask.check(viewEditPerm).can('VIEW')).toBe(true); expect(permask.check(viewEditPerm).can('EDIT')).toBe(true); @@ -394,7 +394,7 @@ describe('New Permask API', () => { it('should support permission sets', () => { // Grant a permission set - const managerPerm = permask.for('PHOTOS').grantSet('MANAGER').value(); + const managerPerm = permask.forGroup('PHOTOS').grantSet('MANAGER').value(); expect(permask.check(managerPerm).can('VIEW')).toBe(true); expect(permask.check(managerPerm).can('EDIT')).toBe(true); @@ -405,11 +405,11 @@ describe('New Permask API', () => { it('should handle ALL_PERMISSIONS', () => { // Grant all permissions - const allPerm = permask.for('VIDEOS').grantAll().value(); + const allPerm = permask.forGroup('VIDEOS').grantAll().value(); expect(permask.check(allPerm).canEverything()).toBe(true); expect(permask.check(allPerm).can('VIEW')).toBe(true); - expect(permask.check(allPerm).can('EDIT')).toBe(true); + expect(permask.check(allPerm).can('EDIT')). toBe(true); expect(permask.check(allPerm).can('DELETE')).toBe(true); expect(permask.check(allPerm).can('SHARE')).toBe(true); expect(permask.check(allPerm).can('PRINT')).toBe(true); @@ -417,7 +417,7 @@ describe('New Permask API', () => { it('should handle granting all permissions', () => { // Changed from ALL_PERMISSIONS to grantAll() - const allPerm = permask.for('VIDEOS').grantAll().value(); + const allPerm = permask.forGroup('VIDEOS').grantAll().value(); expect(permask.check(allPerm).canEverything()).toBe(true); expect(permask.check(allPerm).can('VIEW')).toBe(true); @@ -430,7 +430,7 @@ describe('New Permask API', () => { describe('Checking Permissions', () => { it('should check permissions with the fluent API', () => { - const perm = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); // Individual checks expect(permask.check(perm).can('VIEW')).toBe(true); @@ -450,7 +450,7 @@ describe('New Permask API', () => { }); it('should provide detailed explanation of permissions', () => { - const perm = permask.for('PHOTOS').grant(['VIEW', 'EDIT']).value(); + const perm = permask.forGroup('PHOTOS').grant(['VIEW', 'EDIT']).value(); const details = permask.check(perm).explain(); @@ -471,9 +471,9 @@ describe('New Permask API', () => { describe('String Conversion', () => { it('should convert permissions to strings', () => { - const perm1 = permask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const perm2 = permask.for('PHOTOS').grantAll().value(); - const perm3 = permask.for('VIDEOS').grant([]).value(); + const perm1 = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const perm2 = permask.forGroup('PHOTOS').grantAll().value(); + const perm3 = permask.forGroup('VIDEOS').grant([]).value(); expect(permask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); expect(permask.toString(perm2)).toBe('PHOTOS:ALL'); @@ -506,7 +506,7 @@ describe('New Permask API', () => { .defineGroup('REPORTS', 4) .build(); - const reportPerm = extendedPermask.for('REPORTS').grant(['VIEW', 'APPROVE']).value(); + const reportPerm = extendedPermask.forGroup('REPORTS').grant(['VIEW', 'APPROVE']).value(); expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true); @@ -531,9 +531,9 @@ describe('CRUD Permission Tests', () => { }); it('should handle all CRUD permissions correctly', () => { - const fullCrudPerm = crudPermask.for('DOCUMENTS').grantAll().value(); - const createReadPerm = crudPermask.for('DOCUMENTS').grant(['CREATE', 'READ']).value(); - const readUpdatePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE']).value(); + const fullCrudPerm = crudPermask.forGroup('DOCUMENTS').grantAll().value(); + const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value(); + const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value(); // Full CRUD permissions expect(crudPermask.check(fullCrudPerm).canCreate()).toBe(true); @@ -555,8 +555,8 @@ describe('CRUD Permission Tests', () => { }); it('should represent CRUD permissions in string format', () => { - const createReadPerm = crudPermask.for('DOCUMENTS').grant(['CREATE', 'READ']).value(); - const readUpdatePerm = crudPermask.for('PHOTOS').grant(['READ', 'UPDATE']).value(); + const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value(); + const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value(); expect(crudPermask.toString(createReadPerm)).toBe('DOCUMENTS:CREATE,READ'); expect(crudPermask.toString(readUpdatePerm)).toBe('PHOTOS:READ,UPDATE'); @@ -601,13 +601,13 @@ describe('ALL Permission Tests', () => { it('should handle the ALL permission correctly', () => { // Grant individual permissions - const partialPerm = allPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); // Grant all permissions - const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); // Grant using the ALL permission directly - const allPerm = allPermask.for('PHOTOS').grant(['ALL']).value(); + const allPerm = allPermask.forGroup('PHOTOS').grant(['ALL']).value(); // Test partial permissions expect(allPermask.check(partialPerm).can('VIEW')).toBe(true); @@ -637,7 +637,7 @@ describe('ALL Permission Tests', () => { it('should include ALL in permission explanation', () => { // Partial permissions - const partialPerm = allPermask.for('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); + const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); const details = allPermask.check(partialPerm).explain(); expect(details).toEqual({ @@ -654,7 +654,7 @@ describe('ALL Permission Tests', () => { }); // Full permissions - const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); const fullDetails = allPermask.check(fullPerm).explain(); expect(fullDetails).toEqual({ @@ -672,7 +672,7 @@ describe('ALL Permission Tests', () => { }); it('should handle string conversion with ALL', () => { - const fullPerm = allPermask.for('DOCUMENTS').grantAll().value(); + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); expect(allPermask.toString(fullPerm)).toBe('DOCUMENTS:ALL'); const fromString = allPermask.fromString('PHOTOS:ALL'); @@ -718,7 +718,7 @@ describe('Auto Permission Value Assignment', () => { expect(autoPermask.getPermissionValue('ALL')).toBe(15); // 0b01111 // Test that permissions work correctly - const perm = autoPermask.for('DOCUMENTS').grant(['VIEW', 'SHARE']).value(); + const perm = autoPermask.forGroup('DOCUMENTS').grant(['VIEW', 'SHARE']).value(); expect(autoPermask.check(perm).can('VIEW')).toBe(true); expect(autoPermask.check(perm).can('SHARE')).toBe(true); expect(autoPermask.check(perm).can('EDIT')).toBe(false); @@ -781,7 +781,7 @@ describe('Auto Permission Value Assignment', () => { expect(permask.getPermissionValue('ALL')).toBe(21); // Test granting all permissions - const allPerm = permask.for(1).grantAll().value(); + const allPerm = permask.forGroup(1).grantAll().value(); // The access mask should be 31 (all 5 bits set) expect(allPerm & 31).toBe(31); @@ -809,13 +809,13 @@ describe('Auto Permission Value Assignment', () => { expect(permask.getPermissionValue('ALL')).toBe(15); // Test granting with ALL permission - const allPerm = permask.for(1).grant(['ALL']).value(); + const allPerm = permask.forGroup(1).grant(['ALL']).value(); // Should set all bits according to accessMask (31) expect(allPerm & 31).toBe(31); // Test granting all permissions directly - const fullPerm = permask.for(1).grantAll().value(); + const fullPerm = permask.forGroup(1).grantAll().value(); expect(fullPerm & 31).toBe(31); }); diff --git a/src/permask-class.ts b/src/permask-class.ts index b341571..29e96d3 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -171,7 +171,7 @@ export class PermissionContext> { constructor( private permask: Permask, - group: number | string + group: number | string ) { const groupValue = typeof group === 'string' ? permask.getGroupByName(group) || 0 @@ -395,10 +395,10 @@ export class Permask = Record> * Start building a permission for a specific group * @example * // Create read-create permission for DOCUMENTS group - * const permission = permask.for('DOCUMENTS').grant(['READ', 'CREATE']).value(); + * const permission = permask.forGroup('DOCUMENTS').grant(['READ', 'CREATE']).value(); */ - for(group: number | string): PermissionContext { - return new PermissionContext(this, group); + forGroup(group: G): PermissionContext { + return new PermissionContext(this, group as number | string); } /** @@ -561,4 +561,11 @@ export class Permask = Record> groups: { ...this.groups } }); } + + /** + * Get all defined group names + */ + getGroupNames(): string[] { + return Object.keys(this.groups); + } } From 716bba5cf9cca0113989e4626c43a2f46c2c7292 Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 08:41:20 +0300 Subject: [PATCH 24/25] chore: lint --- src/permask-class.test.ts | 1225 +++++++++++++++++++------------------ src/permask-class.ts | 513 ++++++++-------- 2 files changed, 901 insertions(+), 837 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index ac8b3ee..392120d 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -1,188 +1,204 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { DefaultPermissionAccess, Permask, PermaskBuilder } from '../src/permask-class'; +import type { Permask } from '../src/permask-class' +import { beforeEach, describe, expect, it } from 'vitest' +import { DefaultPermissionAccess, PermaskBuilder } from '../src/permask-class' -describe('Permask', () => { +// Define interfaces for the group types used in tests +interface BasicGroups { + USERS: number + ADMINS: number + [key: string]: number // Add index signature to satisfy Record +} + +interface RichGroups { + DOCUMENTS: number + PHOTOS: number + VIDEOS: number + SPREADSHEETS: number + [key: string]: number // Add index signature +} + +describe('permask', () => { // Simple permissions for basic tests let basicPermask: Permask<{ - READ: number; - UPDATE: number; - DELETE: number; - }>; - + READ: number + UPDATE: number + DELETE: number + }, BasicGroups> + // Rich permissions for advanced tests let richPermask: Permask<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - }>; - + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + }, RichGroups> + beforeEach(() => { // Set up basic permask using builder basicPermask = new PermaskBuilder<{ - READ: number; - UPDATE: number; - DELETE: number; - }>({ + READ: number + UPDATE: number + DELETE: number + }, BasicGroups>({ permissions: { - READ: DefaultPermissionAccess.READ, // 2 - UPDATE: DefaultPermissionAccess.UPDATE, // 4 - DELETE: DefaultPermissionAccess.DELETE // 8 + READ: DefaultPermissionAccess.READ, // 2 + UPDATE: DefaultPermissionAccess.UPDATE, // 4 + DELETE: DefaultPermissionAccess.DELETE, // 8 }, accessBits: 4, groups: { USERS: 1, - ADMINS: 2 - } - }).build(); - + ADMINS: 2, + } as BasicGroups, + }).build() + // Set up rich permask using builder with more permissions and groups richPermask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - }>({ + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + }, RichGroups>({ permissions: { - VIEW: 1, // 0b00001 - EDIT: 2, // 0b00010 - DELETE: 4, // 0b00100 - SHARE: 8, // 0b01000 - PRINT: 16, // 0b10000 + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16, // 0b10000 }, accessBits: 6, groups: { DOCUMENTS: 1, PHOTOS: 2, VIDEOS: 3, - SPREADSHEETS: 4 - } - }) - .definePermissionSet('VIEWER', ['VIEW']) - .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) - .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) - .definePermissionSet('PUBLISHER', ['VIEW', 'SHARE']) - .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) - .build(); - }); - - describe('Permission Creation', () => { + SPREADSHEETS: 4, + } as RichGroups, + }) + .definePermissionSet('VIEWER', ['VIEW']) + .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) + .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) + .definePermissionSet('PUBLISHER', ['VIEW', 'SHARE']) + .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) + .build() + }) + + describe('permission Creation', () => { it('should create basic permissions with correct bitmask values', () => { - const readPermission = basicPermask.forGroup('USERS').grant(['READ']).value(); - const readUpdatePermission = basicPermask.forGroup('USERS').grant(['READ', 'UPDATE']).value(); - + const readPermission = basicPermask.forGroup('USERS').grant(['READ']).value() + const readUpdatePermission = basicPermask.forGroup('USERS').grant(['READ', 'UPDATE']).value() + // Group 1 (USERS) << 4 bits = 16, plus READ (2) = 18 - expect(readPermission).toBe(18); - + expect(readPermission).toBe(18) + // Group 1 (USERS) << 4 bits = 16, plus READ (2) + UPDATE (4) = 22 - expect(readUpdatePermission).toBe(22); - }); - + expect(readUpdatePermission).toBe(22) + }) + it('should create permissions with explicit group IDs', () => { - const permission = richPermask.forGroup(2).grant(['VIEW', 'EDIT']).value(); - + const permission = richPermask.forGroup(2).grant(['VIEW', 'EDIT']).value() + // Group 2 (PHOTOS) << 6 bits = 128, plus VIEW (1) + EDIT (2) = 131 - expect(permission).toBe(131); - expect(richPermask.check(permission).group()).toBe(2); - expect(richPermask.check(permission).groupName()).toBe('PHOTOS'); - }); - + expect(permission).toBe(131) + expect(richPermask.check(permission).group()).toBe(2) + expect(richPermask.check(permission).groupName()).toBe('PHOTOS') + }) + it('should handle ALL_PERMISSIONS symbol', () => { - const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); - const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value(); - + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value() + const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value() + // All permissions should set all bits (63 for 6 bits) - expect(richPermask.check(allPermissions).canEverything()).toBe(true); - expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); - expect(allPermissions).toEqual(alternateWay); - }); - + expect(richPermask.check(allPermissions).canEverything()).toBe(true) + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63) + expect(allPermissions).toEqual(alternateWay) + }) + it('should handle permission sets', () => { - const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value(); - const managerPerm = richPermask.forGroup('PHOTOS').grantSet('MANAGER').value(); - + const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value() + const managerPerm = richPermask.forGroup('PHOTOS').grantSet('MANAGER').value() + // Editor has VIEW and EDIT - expect(richPermask.check(editorPerm).can('VIEW')).toBe(true); - expect(richPermask.check(editorPerm).can('EDIT')).toBe(true); - expect(richPermask.check(editorPerm).can('DELETE')).toBe(false); - + expect(richPermask.check(editorPerm).can('VIEW')).toBe(true) + expect(richPermask.check(editorPerm).can('EDIT')).toBe(true) + expect(richPermask.check(editorPerm).can('DELETE')).toBe(false) + // Manager has VIEW, EDIT, and DELETE - expect(richPermask.check(managerPerm).can('VIEW')).toBe(true); - expect(richPermask.check(managerPerm).can('EDIT')).toBe(true); - expect(richPermask.check(managerPerm).can('DELETE')).toBe(true); - expect(richPermask.check(managerPerm).can('SHARE')).toBe(false); - }); - + expect(richPermask.check(managerPerm).can('VIEW')).toBe(true) + expect(richPermask.check(managerPerm).can('EDIT')).toBe(true) + expect(richPermask.check(managerPerm).can('DELETE')).toBe(true) + expect(richPermask.check(managerPerm).can('SHARE')).toBe(false) + }) + it('should combine permission sets and individual permissions', () => { // Grant editor set plus SHARE permission const customPerm = richPermask.forGroup('VIDEOS') .grantSet('EDITOR') .grant(['SHARE']) - .value(); - - expect(richPermask.check(customPerm).can('VIEW')).toBe(true); - expect(richPermask.check(customPerm).can('EDIT')).toBe(true); - expect(richPermask.check(customPerm).can('SHARE')).toBe(true); - expect(richPermask.check(customPerm).can('DELETE')).toBe(false); - expect(richPermask.check(customPerm).can('PRINT')).toBe(false); - }); + .value() + + expect(richPermask.check(customPerm).can('VIEW')).toBe(true) + expect(richPermask.check(customPerm).can('EDIT')).toBe(true) + expect(richPermask.check(customPerm).can('SHARE')).toBe(true) + expect(richPermask.check(customPerm).can('DELETE')).toBe(false) + expect(richPermask.check(customPerm).can('PRINT')).toBe(false) + }) it('should handle granting all permissions', () => { - const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); - + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value() + // All permissions should set all bits - expect(richPermask.check(allPermissions).canEverything()).toBe(true); - expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); - }); + expect(richPermask.check(allPermissions).canEverything()).toBe(true) + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63) + }) it('should handle granting all permissions via grantAll()', () => { // Changed from using ALL_PERMISSIONS to directly using grantAll() - const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value(); - const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value(); - + const allPermissions = richPermask.forGroup('DOCUMENTS').grantAll().value() + const alternateWay = richPermask.forGroup('DOCUMENTS').grantAll().value() + // All permissions should set all bits (63 for 6 bits) - expect(richPermask.check(allPermissions).canEverything()).toBe(true); - expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63); - expect(allPermissions).toEqual(alternateWay); - }); - }); - - describe('Permission Checking', () => { + expect(richPermask.check(allPermissions).canEverything()).toBe(true) + expect(allPermissions).toBe(richPermask.check(allPermissions).group() << 6 | 63) + expect(allPermissions).toEqual(alternateWay) + }) + }) + + describe('permission Checking', () => { it('should check individual permissions', () => { - const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - - expect(richPermask.check(perm).can('VIEW')).toBe(true); - expect(richPermask.check(perm).can('EDIT')).toBe(true); - expect(richPermask.check(perm).can('DELETE')).toBe(false); - }); - + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + + expect(richPermask.check(perm).can('VIEW')).toBe(true) + expect(richPermask.check(perm).can('EDIT')).toBe(true) + expect(richPermask.check(perm).can('DELETE')).toBe(false) + }) + it('should check multiple permissions at once', () => { - const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT', 'SHARE']).value(); - + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT', 'SHARE']).value() + // All permissions check - expect(richPermask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true); - expect(richPermask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false); - + expect(richPermask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true) + expect(richPermask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false) + // Any permissions check - expect(richPermask.check(perm).canAny(['DELETE', 'PRINT'])).toBe(false); - expect(richPermask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true); - }); - + expect(richPermask.check(perm).canAny(['DELETE', 'PRINT'])).toBe(false) + expect(richPermask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true) + }) + it('should check for complete permissions', () => { - const partialPerm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const allPerm = richPermask.forGroup('DOCUMENTS').grantAll().value(); - - expect(richPermask.check(partialPerm).canEverything()).toBe(false); - expect(richPermask.check(allPerm).canEverything()).toBe(true); - }); - + const partialPerm = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + const allPerm = richPermask.forGroup('DOCUMENTS').grantAll().value() + + expect(richPermask.check(partialPerm).canEverything()).toBe(false) + expect(richPermask.check(allPerm).canEverything()).toBe(true) + }) + it('should provide detailed permission explanation', () => { - const perm = richPermask.forGroup('PHOTOS').grant(['VIEW', 'SHARE']).value(); - - const details = richPermask.check(perm).explain(); - + const perm = richPermask.forGroup('PHOTOS').grant(['VIEW', 'SHARE']).value() + + const details = richPermask.check(perm).explain() + expect(details).toEqual({ group: 2, groupName: 'PHOTOS', @@ -193,267 +209,274 @@ describe('Permask', () => { SHARE: true, PRINT: false, ALL: false, - } - }); - }); - + }, + }) + }) + it('should check group membership', () => { - const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW']).value(); - - expect(richPermask.check(perm).inGroup('DOCUMENTS')).toBe(true); - expect(richPermask.check(perm).inGroup('PHOTOS')).toBe(false); - expect(richPermask.check(perm).inGroup(1)).toBe(true); - }); - }); - - describe('String Representation', () => { + const perm = richPermask.forGroup('DOCUMENTS').grant(['VIEW']).value() + + expect(richPermask.check(perm).inGroup('DOCUMENTS')).toBe(true) + expect(richPermask.check(perm).inGroup('PHOTOS')).toBe(false) + expect(richPermask.check(perm).inGroup(1)).toBe(true) + }) + }) + + describe('string Representation', () => { it('should convert permissions to strings', () => { - const perm1 = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const perm2 = richPermask.forGroup('PHOTOS').grantAll().value(); - const perm3 = richPermask.forGroup('VIDEOS').grant([]).value(); - - expect(richPermask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); - expect(richPermask.toString(perm2)).toBe('PHOTOS:ALL'); - expect(richPermask.toString(perm3)).toBe('VIDEOS:NONE'); - }); - + const perm1 = richPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + const perm2 = richPermask.forGroup('PHOTOS').grantAll().value() + const perm3 = richPermask.forGroup('VIDEOS').grant([]).value() + + expect(richPermask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT') + expect(richPermask.toString(perm2)).toBe('PHOTOS:ALL') + expect(richPermask.toString(perm3)).toBe('VIDEOS:NONE') + }) + it('should parse permission strings', () => { - const perm1 = richPermask.fromString('DOCUMENTS:VIEW,EDIT'); - const perm2 = richPermask.fromString('PHOTOS:ALL'); - const perm3 = richPermask.fromString('VIDEOS:MANAGER'); - const perm4 = richPermask.fromString('5:VIEW,SHARE'); // Numeric group - - expect(richPermask.check(perm1).canAll(['VIEW', 'EDIT'])).toBe(true); - expect(richPermask.check(perm1).can('DELETE')).toBe(false); - - expect(richPermask.check(perm2).canEverything()).toBe(true); - + const perm1 = richPermask.fromString('DOCUMENTS:VIEW,EDIT') + const perm2 = richPermask.fromString('PHOTOS:ALL') + const perm3 = richPermask.fromString('VIDEOS:MANAGER') + const perm4 = richPermask.fromString('5:VIEW,SHARE') // Numeric group + + expect(richPermask.check(perm1).canAll(['VIEW', 'EDIT'])).toBe(true) + expect(richPermask.check(perm1).can('DELETE')).toBe(false) + + expect(richPermask.check(perm2).canEverything()).toBe(true) + // Permission set expansion - expect(richPermask.check(perm3).can('VIEW')).toBe(true); - expect(richPermask.check(perm3).can('EDIT')).toBe(true); - expect(richPermask.check(perm3).can('DELETE')).toBe(true); - + expect(richPermask.check(perm3).can('VIEW')).toBe(true) + expect(richPermask.check(perm3).can('EDIT')).toBe(true) + expect(richPermask.check(perm3).can('DELETE')).toBe(true) + // Numeric group handling - expect(richPermask.check(perm4).group()).toBe(5); - expect(richPermask.check(perm4).canAll(['VIEW', 'SHARE'])).toBe(true); - }); - + expect(richPermask.check(perm4).group()).toBe(5) + expect(richPermask.check(perm4).canAll(['VIEW', 'SHARE'])).toBe(true) + }) + it('should handle alternative string formats', () => { - const permWithStar = richPermask.fromString('DOCUMENTS:*'); - const permWithEmpty = richPermask.fromString('VIDEOS:'); - - expect(richPermask.check(permWithStar).canEverything()).toBe(true); - expect(richPermask.check(permWithEmpty).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(false); - }); - }); - - describe('Builder Pattern', () => { + const permWithStar = richPermask.fromString('DOCUMENTS:*') + const permWithEmpty = richPermask.fromString('VIDEOS:') + + expect(richPermask.check(permWithStar).canEverything()).toBe(true) + expect(richPermask.check(permWithEmpty).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(false) + }) + }) + + describe('builder Pattern', () => { it('should extend permissions with builder', () => { // Create extended version of the permissions system const extendedPermask = richPermask.toBuilder() .definePermission('APPROVE', 32) .defineGroup('REPORTS', 5) .definePermissionSet('APPROVER', ['VIEW', 'APPROVE']) - .build(); - + .build() + // Create permission using new components - const reportPerm = extendedPermask.forGroup('REPORTS').grantSet('APPROVER').value(); - - expect(extendedPermask.check(reportPerm).can('VIEW')).toBe(true); - expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); - expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true); - }); - + const reportPerm = extendedPermask.forGroup('REPORTS').grantSet('APPROVER').value() + + expect(extendedPermask.check(reportPerm).can('VIEW')).toBe(true) + expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true) + expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true) + }) + it('should validate permission values', () => { // Try to add a permission that exceeds bit capacity (6 bits = max 63) expect(() => { richPermask.toBuilder() .definePermission('OVERFLOW', 64) - .build(); - }).toThrow(/exceeds maximum value/); - }); - }); - - describe('Edge Cases', () => { + .build() + }).toThrow(/exceeds maximum value/) + }) + }) + + describe('edge Cases', () => { it('should handle group 0 correctly', () => { - const perm = richPermask.forGroup(0).grant(['VIEW']).value(); - - expect(richPermask.check(perm).group()).toBe(0); - expect(richPermask.check(perm).can('VIEW')).toBe(true); - }); - + const perm = richPermask.forGroup(0).grant(['VIEW']).value() + + expect(richPermask.check(perm).group()).toBe(0) + expect(richPermask.check(perm).can('VIEW')).toBe(true) + }) + it('should handle non-existent groups gracefully', () => { - const perm = richPermask.forGroup('NON_EXISTENT').grant(['VIEW']).value(); - - expect(richPermask.check(perm).group()).toBe(0); // Default to group 0 - expect(richPermask.check(perm).can('VIEW')).toBe(true); - }); - + const perm = richPermask.forGroup('NON_EXISTENT').grant(['VIEW']).value() + + expect(richPermask.check(perm).group()).toBe(0) // Default to group 0 + expect(richPermask.check(perm).can('VIEW')).toBe(true) + }) + it('should handle non-existent permissions gracefully', () => { - // @ts-ignore - Intentionally testing with a non-existent permission - const perm = richPermask.forGroup('DOCUMENTS').grant(['NON_EXISTENT']).value(); - - expect(richPermask.check(perm).group()).toBe(1); // Group should be set - expect(richPermask.check(perm).can('VIEW')).toBe(false); // No permissions granted - }); - + // @ts-expect-error - Intentionally testing with a non-existent permission + const perm = richPermask.forGroup('DOCUMENTS').grant(['NON_EXISTENT']).value() + + expect(richPermask.check(perm).group()).toBe(1) // Group should be set + expect(richPermask.check(perm).can('VIEW')).toBe(false) // No permissions granted + }) + it('should handle empty permission lists', () => { - const perm = richPermask.forGroup('DOCUMENTS').grant([]).value(); - - expect(richPermask.check(perm).group()).toBe(1); - expect(richPermask.check(perm).can('VIEW')).toBe(false); - expect(richPermask.check(perm).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(false); - }); - }); - - describe('Permission Sets', () => { + const perm = richPermask.forGroup('DOCUMENTS').grant([]).value() + + expect(richPermask.check(perm).group()).toBe(1) + expect(richPermask.check(perm).can('VIEW')).toBe(false) + expect(richPermask.check(perm).canAny(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(false) + }) + }) + + describe('permission Sets', () => { it('should use predefined permission sets', () => { - const viewerPerm = richPermask.forGroup('DOCUMENTS').grantSet('VIEWER').value(); - const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value(); - const managerPerm = richPermask.forGroup('DOCUMENTS').grantSet('MANAGER').value(); - const adminPerm = richPermask.forGroup('DOCUMENTS').grantSet('ADMIN').value(); - + const viewerPerm = richPermask.forGroup('DOCUMENTS').grantSet('VIEWER').value() + const editorPerm = richPermask.forGroup('DOCUMENTS').grantSet('EDITOR').value() + const managerPerm = richPermask.forGroup('DOCUMENTS').grantSet('MANAGER').value() + const adminPerm = richPermask.forGroup('DOCUMENTS').grantSet('ADMIN').value() + // Check viewer permissions - expect(richPermask.check(viewerPerm).can('VIEW')).toBe(true); - expect(richPermask.check(viewerPerm).can('EDIT')).toBe(false); - + expect(richPermask.check(viewerPerm).can('VIEW')).toBe(true) + expect(richPermask.check(viewerPerm).can('EDIT')).toBe(false) + // Check editor permissions - expect(richPermask.check(editorPerm).canAll(['VIEW', 'EDIT'])).toBe(true); - expect(richPermask.check(editorPerm).can('DELETE')).toBe(false); - + expect(richPermask.check(editorPerm).canAll(['VIEW', 'EDIT'])).toBe(true) + expect(richPermask.check(editorPerm).can('DELETE')).toBe(false) + // Check manager permissions - expect(richPermask.check(managerPerm).canAll(['VIEW', 'EDIT', 'DELETE'])).toBe(true); - expect(richPermask.check(managerPerm).can('SHARE')).toBe(false); - + expect(richPermask.check(managerPerm).canAll(['VIEW', 'EDIT', 'DELETE'])).toBe(true) + expect(richPermask.check(managerPerm).can('SHARE')).toBe(false) + // Check admin permissions - expect(richPermask.check(adminPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(true); - }); - + expect(richPermask.check(adminPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT'])).toBe(true) + }) + it('should combine sets with additional permissions', () => { // Grant manager set plus SHARE permission const customPerm = richPermask.forGroup('DOCUMENTS') .grantSet('MANAGER') .grant(['SHARE']) - .value(); - - expect(richPermask.check(customPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(true); - expect(richPermask.check(customPerm).can('PRINT')).toBe(false); - }); - }); -}); - -describe('New Permask API', () => { + .value() + + expect(richPermask.check(customPerm).canAll(['VIEW', 'EDIT', 'DELETE', 'SHARE'])).toBe(true) + expect(richPermask.check(customPerm).can('PRINT')).toBe(false) + }) + }) +}) + +describe('new Permask API', () => { + interface TestGroups { + DOCUMENTS: number + PHOTOS: number + VIDEOS: number + [key: string]: number // Add index signature + } + let permask: Permask<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - }>; - + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + }, TestGroups> + beforeEach(() => { // Create using builder pattern permask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - }>({ + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + }, TestGroups>({ permissions: { VIEW: 1, EDIT: 2, DELETE: 4, SHARE: 8, - PRINT: 16 + PRINT: 16, }, accessBits: 6, groups: { DOCUMENTS: 1, PHOTOS: 2, - VIDEOS: 3 - } - }) - .definePermissionSet('VIEWER', ['VIEW']) - .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) - .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) - .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) - .build(); - }); - - describe('Building Permissions', () => { + VIDEOS: 3, + } as TestGroups, + }) + .definePermissionSet('VIEWER', ['VIEW']) + .definePermissionSet('EDITOR', ['VIEW', 'EDIT']) + .definePermissionSet('MANAGER', ['VIEW', 'EDIT', 'DELETE']) + .definePermissionSet('ADMIN', ['VIEW', 'EDIT', 'DELETE', 'SHARE', 'PRINT']) + .build() + }) + + describe('building Permissions', () => { it('should create permissions using the fluent API', () => { // Grant specific permissions - const viewEditPerm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - - expect(permask.check(viewEditPerm).can('VIEW')).toBe(true); - expect(permask.check(viewEditPerm).can('EDIT')).toBe(true); - expect(permask.check(viewEditPerm).can('DELETE')).toBe(false); - expect(permask.check(viewEditPerm).inGroup('DOCUMENTS')).toBe(true); - }); - + const viewEditPerm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + + expect(permask.check(viewEditPerm).can('VIEW')).toBe(true) + expect(permask.check(viewEditPerm).can('EDIT')).toBe(true) + expect(permask.check(viewEditPerm).can('DELETE')).toBe(false) + expect(permask.check(viewEditPerm).inGroup('DOCUMENTS')).toBe(true) + }) + it('should support permission sets', () => { // Grant a permission set - const managerPerm = permask.forGroup('PHOTOS').grantSet('MANAGER').value(); - - expect(permask.check(managerPerm).can('VIEW')).toBe(true); - expect(permask.check(managerPerm).can('EDIT')).toBe(true); - expect(permask.check(managerPerm).can('DELETE')).toBe(true); - expect(permask.check(managerPerm).can('SHARE')).toBe(false); - expect(permask.check(managerPerm).inGroup('PHOTOS')).toBe(true); - }); - + const managerPerm = permask.forGroup('PHOTOS').grantSet('MANAGER').value() + + expect(permask.check(managerPerm).can('VIEW')).toBe(true) + expect(permask.check(managerPerm).can('EDIT')).toBe(true) + expect(permask.check(managerPerm).can('DELETE')).toBe(true) + expect(permask.check(managerPerm).can('SHARE')).toBe(false) + expect(permask.check(managerPerm).inGroup('PHOTOS')).toBe(true) + }) + it('should handle ALL_PERMISSIONS', () => { // Grant all permissions - const allPerm = permask.forGroup('VIDEOS').grantAll().value(); - - expect(permask.check(allPerm).canEverything()).toBe(true); - expect(permask.check(allPerm).can('VIEW')).toBe(true); - expect(permask.check(allPerm).can('EDIT')). toBe(true); - expect(permask.check(allPerm).can('DELETE')).toBe(true); - expect(permask.check(allPerm).can('SHARE')).toBe(true); - expect(permask.check(allPerm).can('PRINT')).toBe(true); - }); + const allPerm = permask.forGroup('VIDEOS').grantAll().value() + + expect(permask.check(allPerm).canEverything()).toBe(true) + expect(permask.check(allPerm).can('VIEW')).toBe(true) + expect(permask.check(allPerm).can('EDIT')).toBe(true) + expect(permask.check(allPerm).can('DELETE')).toBe(true) + expect(permask.check(allPerm).can('SHARE')).toBe(true) + expect(permask.check(allPerm).can('PRINT')).toBe(true) + }) it('should handle granting all permissions', () => { // Changed from ALL_PERMISSIONS to grantAll() - const allPerm = permask.forGroup('VIDEOS').grantAll().value(); - - expect(permask.check(allPerm).canEverything()).toBe(true); - expect(permask.check(allPerm).can('VIEW')).toBe(true); - expect(permask.check(allPerm).can('EDIT')).toBe(true); - expect(permask.check(allPerm).can('DELETE')).toBe(true); - expect(permask.check(allPerm).can('SHARE')).toBe(true); - expect(permask.check(allPerm).can('PRINT')).toBe(true); - }); - }); - - describe('Checking Permissions', () => { + const allPerm = permask.forGroup('VIDEOS').grantAll().value() + + expect(permask.check(allPerm).canEverything()).toBe(true) + expect(permask.check(allPerm).can('VIEW')).toBe(true) + expect(permask.check(allPerm).can('EDIT')).toBe(true) + expect(permask.check(allPerm).can('DELETE')).toBe(true) + expect(permask.check(allPerm).can('SHARE')).toBe(true) + expect(permask.check(allPerm).can('PRINT')).toBe(true) + }) + }) + + describe('checking Permissions', () => { it('should check permissions with the fluent API', () => { - const perm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - + const perm = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + // Individual checks - expect(permask.check(perm).can('VIEW')).toBe(true); - expect(permask.check(perm).can('DELETE')).toBe(false); - + expect(permask.check(perm).can('VIEW')).toBe(true) + expect(permask.check(perm).can('DELETE')).toBe(false) + // Check multiple permissions - expect(permask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true); - expect(permask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false); - expect(permask.check(perm).canAny(['DELETE', 'SHARE'])).toBe(false); - expect(permask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true); - + expect(permask.check(perm).canAll(['VIEW', 'EDIT'])).toBe(true) + expect(permask.check(perm).canAll(['VIEW', 'DELETE'])).toBe(false) + expect(permask.check(perm).canAny(['DELETE', 'SHARE'])).toBe(false) + expect(permask.check(perm).canAny(['EDIT', 'DELETE'])).toBe(true) + // Group checks - expect(permask.check(perm).inGroup('DOCUMENTS')).toBe(true); - expect(permask.check(perm).inGroup('PHOTOS')).toBe(false); - expect(permask.check(perm).group()).toBe(1); - expect(permask.check(perm).groupName()).toBe('DOCUMENTS'); - }); - + expect(permask.check(perm).inGroup('DOCUMENTS')).toBe(true) + expect(permask.check(perm).inGroup('PHOTOS')).toBe(false) + expect(permask.check(perm).group()).toBe(1) + expect(permask.check(perm).groupName()).toBe('DOCUMENTS') + }) + it('should provide detailed explanation of permissions', () => { - const perm = permask.forGroup('PHOTOS').grant(['VIEW', 'EDIT']).value(); - - const details = permask.check(perm).explain(); - + const perm = permask.forGroup('PHOTOS').grant(['VIEW', 'EDIT']).value() + + const details = permask.check(perm).explain() + expect(details).toEqual({ group: 2, groupName: 'PHOTOS', @@ -464,182 +487,195 @@ describe('New Permask API', () => { SHARE: false, PRINT: false, ALL: false, - } - }); - }); - }); - - describe('String Conversion', () => { + }, + }) + }) + }) + + describe('string Conversion', () => { it('should convert permissions to strings', () => { - const perm1 = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const perm2 = permask.forGroup('PHOTOS').grantAll().value(); - const perm3 = permask.forGroup('VIDEOS').grant([]).value(); - - expect(permask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT'); - expect(permask.toString(perm2)).toBe('PHOTOS:ALL'); - expect(permask.toString(perm3)).toBe('VIDEOS:NONE'); - }); - + const perm1 = permask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + const perm2 = permask.forGroup('PHOTOS').grantAll().value() + const perm3 = permask.forGroup('VIDEOS').grant([]).value() + + expect(permask.toString(perm1)).toBe('DOCUMENTS:VIEW,EDIT') + expect(permask.toString(perm2)).toBe('PHOTOS:ALL') + expect(permask.toString(perm3)).toBe('VIDEOS:NONE') + }) + it('should parse permission strings', () => { - const perm1 = permask.fromString('DOCUMENTS:VIEW,EDIT'); - const perm2 = permask.fromString('PHOTOS:ALL'); - const perm3 = permask.fromString('VIDEOS:MANAGER'); - - expect(permask.check(perm1).can('VIEW')).toBe(true); - expect(permask.check(perm1).can('EDIT')).toBe(true); - expect(permask.check(perm1).can('DELETE')).toBe(false); - - expect(permask.check(perm2).canEverything()).toBe(true); - - expect(permask.check(perm3).can('VIEW')).toBe(true); - expect(permask.check(perm3).can('EDIT')).toBe(true); - expect(permask.check(perm3).can('DELETE')).toBe(true); - expect(permask.check(perm3).can('SHARE')).toBe(false); - }); - }); - - describe('Builder Pattern', () => { + const perm1 = permask.fromString('DOCUMENTS:VIEW,EDIT') + const perm2 = permask.fromString('PHOTOS:ALL') + const perm3 = permask.fromString('VIDEOS:MANAGER') + + expect(permask.check(perm1).can('VIEW')).toBe(true) + expect(permask.check(perm1).can('EDIT')).toBe(true) + expect(permask.check(perm1).can('DELETE')).toBe(false) + + expect(permask.check(perm2).canEverything()).toBe(true) + + expect(permask.check(perm3).can('VIEW')).toBe(true) + expect(permask.check(perm3).can('EDIT')).toBe(true) + expect(permask.check(perm3).can('DELETE')).toBe(true) + expect(permask.check(perm3).can('SHARE')).toBe(false) + }) + }) + + describe('builder Pattern', () => { it('should modify existing permissions using toBuilder', () => { // Create a modified version of the permask const extendedPermask = permask.toBuilder() .definePermission('APPROVE', 32) .defineGroup('REPORTS', 4) - .build(); - - const reportPerm = extendedPermask.forGroup('REPORTS').grant(['VIEW', 'APPROVE']).value(); - - expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true); - expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true); - }); - }); -}); + .build() + + const reportPerm = extendedPermask.forGroup('REPORTS').grant(['VIEW', 'APPROVE']).value() + + expect(extendedPermask.check(reportPerm).can('APPROVE')).toBe(true) + expect(extendedPermask.check(reportPerm).inGroup('REPORTS')).toBe(true) + }) + }) +}) // Add new test cases for CRUD permission -describe('CRUD Permission Tests', () => { - let crudPermask: Permask; - +describe('cRUD Permission Tests', () => { + interface CrudGroups { + USERS: number + DOCUMENTS: number + PHOTOS: number + [key: string]: number // Add index signature + } + + let crudPermask: Permask + beforeEach(() => { - crudPermask = new PermaskBuilder({ + crudPermask = new PermaskBuilder({ permissions: DefaultPermissionAccess, accessBits: 4, groups: { USERS: 1, DOCUMENTS: 2, - PHOTOS: 3 - } - }).build(); - }); - + PHOTOS: 3, + } as CrudGroups, + }).build() + }) + it('should handle all CRUD permissions correctly', () => { - const fullCrudPerm = crudPermask.forGroup('DOCUMENTS').grantAll().value(); - const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value(); - const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value(); - + const fullCrudPerm = crudPermask.forGroup('DOCUMENTS').grantAll().value() + const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value() + const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value() + // Full CRUD permissions - expect(crudPermask.check(fullCrudPerm).canCreate()).toBe(true); - expect(crudPermask.check(fullCrudPerm).canRead()).toBe(true); - expect(crudPermask.check(fullCrudPerm).canUpdate()).toBe(true); - expect(crudPermask.check(fullCrudPerm).canDelete()).toBe(true); - + expect(crudPermask.check(fullCrudPerm).canCreate()).toBe(true) + expect(crudPermask.check(fullCrudPerm).canRead()).toBe(true) + expect(crudPermask.check(fullCrudPerm).canUpdate()).toBe(true) + expect(crudPermask.check(fullCrudPerm).canDelete()).toBe(true) + // Create + Read permissions - expect(crudPermask.check(createReadPerm).canCreate()).toBe(true); - expect(crudPermask.check(createReadPerm).canRead()).toBe(true); - expect(crudPermask.check(createReadPerm).canUpdate()).toBe(false); - expect(crudPermask.check(createReadPerm).canDelete()).toBe(false); - + expect(crudPermask.check(createReadPerm).canCreate()).toBe(true) + expect(crudPermask.check(createReadPerm).canRead()).toBe(true) + expect(crudPermask.check(createReadPerm).canUpdate()).toBe(false) + expect(crudPermask.check(createReadPerm).canDelete()).toBe(false) + // Read + Update permissions - expect(crudPermask.check(readUpdatePerm).canCreate()).toBe(false); - expect(crudPermask.check(readUpdatePerm).canRead()).toBe(true); - expect(crudPermask.check(readUpdatePerm).canUpdate()).toBe(true); - expect(crudPermask.check(readUpdatePerm).canDelete()).toBe(false); - }); - + expect(crudPermask.check(readUpdatePerm).canCreate()).toBe(false) + expect(crudPermask.check(readUpdatePerm).canRead()).toBe(true) + expect(crudPermask.check(readUpdatePerm).canUpdate()).toBe(true) + expect(crudPermask.check(readUpdatePerm).canDelete()).toBe(false) + }) + it('should represent CRUD permissions in string format', () => { - const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value(); - const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value(); - - expect(crudPermask.toString(createReadPerm)).toBe('DOCUMENTS:CREATE,READ'); - expect(crudPermask.toString(readUpdatePerm)).toBe('PHOTOS:READ,UPDATE'); - }); -}); + const createReadPerm = crudPermask.forGroup('DOCUMENTS').grant(['CREATE', 'READ']).value() + const readUpdatePerm = crudPermask.forGroup('PHOTOS').grant(['READ', 'UPDATE']).value() + + expect(crudPermask.toString(createReadPerm)).toBe('DOCUMENTS:CREATE,READ') + expect(crudPermask.toString(readUpdatePerm)).toBe('PHOTOS:READ,UPDATE') + }) +}) // Add new test cases for ALL permission -describe('ALL Permission Tests', () => { +describe('aLL Permission Tests', () => { + interface AllGroups { + DOCUMENTS: number + PHOTOS: number + [key: string]: number // Add index signature + } + let allPermask: Permask<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - ALL: number; - }>; - + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + ALL: number + }, AllGroups> + beforeEach(() => { allPermask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - ALL: number; - }>({ + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + ALL: number + }, AllGroups>({ permissions: { - VIEW: 1, // 0b00001 - EDIT: 2, // 0b00010 - DELETE: 4, // 0b00100 - SHARE: 8, // 0b01000 - PRINT: 16, // 0b10000 - ALL: 31 // 0b11111 - All permissions combined + VIEW: 1, // 0b00001 + EDIT: 2, // 0b00010 + DELETE: 4, // 0b00100 + SHARE: 8, // 0b01000 + PRINT: 16, // 0b10000 + ALL: 31, // 0b11111 - All permissions combined }, accessBits: 6, groups: { DOCUMENTS: 1, - PHOTOS: 2 - } - }).build(); - }); - + PHOTOS: 2, + } as AllGroups, + }).build() + }) + it('should handle the ALL permission correctly', () => { // Grant individual permissions - const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - + const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + // Grant all permissions - const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); - + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value() + // Grant using the ALL permission directly - const allPerm = allPermask.forGroup('PHOTOS').grant(['ALL']).value(); - + const allPerm = allPermask.forGroup('PHOTOS').grant(['ALL']).value() + // Test partial permissions - expect(allPermask.check(partialPerm).can('VIEW')).toBe(true); - expect(allPermask.check(partialPerm).can('EDIT')).toBe(true); - expect(allPermask.check(partialPerm).can('DELETE')).toBe(false); - expect(allPermask.check(partialPerm).can('ALL')).toBe(false); - expect(allPermask.check(partialPerm).canEverything()).toBe(false); - + expect(allPermask.check(partialPerm).can('VIEW')).toBe(true) + expect(allPermask.check(partialPerm).can('EDIT')).toBe(true) + expect(allPermask.check(partialPerm).can('DELETE')).toBe(false) + expect(allPermask.check(partialPerm).can('ALL')).toBe(false) + expect(allPermask.check(partialPerm).canEverything()).toBe(false) + // Test full permissions via grantAll - expect(allPermask.check(fullPerm).can('VIEW')).toBe(true); - expect(allPermask.check(fullPerm).can('EDIT')).toBe(true); - expect(allPermask.check(fullPerm).can('DELETE')).toBe(true); - expect(allPermask.check(fullPerm).can('SHARE')).toBe(true); - expect(allPermask.check(fullPerm).can('PRINT')).toBe(true); - expect(allPermask.check(fullPerm).can('ALL')).toBe(true); - expect(allPermask.check(fullPerm).canEverything()).toBe(true); - + expect(allPermask.check(fullPerm).can('VIEW')).toBe(true) + expect(allPermask.check(fullPerm).can('EDIT')).toBe(true) + expect(allPermask.check(fullPerm).can('DELETE')).toBe(true) + expect(allPermask.check(fullPerm).can('SHARE')).toBe(true) + expect(allPermask.check(fullPerm).can('PRINT')).toBe(true) + expect(allPermask.check(fullPerm).can('ALL')).toBe(true) + expect(allPermask.check(fullPerm).canEverything()).toBe(true) + // Test granting ALL permission directly - expect(allPermask.check(allPerm).can('VIEW')).toBe(true); - expect(allPermask.check(allPerm).can('EDIT')).toBe(true); - expect(allPermask.check(allPerm).can('DELETE')).toBe(true); - expect(allPermask.check(allPerm).can('SHARE')).toBe(true); - expect(allPermask.check(allPerm).can('PRINT')).toBe(true); - expect(allPermask.check(allPerm).can('ALL')).toBe(true); - expect(allPermask.check(allPerm).canEverything()).toBe(true); - }); - + expect(allPermask.check(allPerm).can('VIEW')).toBe(true) + expect(allPermask.check(allPerm).can('EDIT')).toBe(true) + expect(allPermask.check(allPerm).can('DELETE')).toBe(true) + expect(allPermask.check(allPerm).can('SHARE')).toBe(true) + expect(allPermask.check(allPerm).can('PRINT')).toBe(true) + expect(allPermask.check(allPerm).can('ALL')).toBe(true) + expect(allPermask.check(allPerm).canEverything()).toBe(true) + }) + it('should include ALL in permission explanation', () => { // Partial permissions - const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value(); - const details = allPermask.check(partialPerm).explain(); - + const partialPerm = allPermask.forGroup('DOCUMENTS').grant(['VIEW', 'EDIT']).value() + const details = allPermask.check(partialPerm).explain() + expect(details).toEqual({ group: 1, groupName: 'DOCUMENTS', @@ -649,14 +685,14 @@ describe('ALL Permission Tests', () => { DELETE: false, SHARE: false, PRINT: false, - ALL: false - } - }); - + ALL: false, + }, + }) + // Full permissions - const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); - const fullDetails = allPermask.check(fullPerm).explain(); - + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value() + const fullDetails = allPermask.check(fullPerm).explain() + expect(fullDetails).toEqual({ group: 1, groupName: 'DOCUMENTS', @@ -666,108 +702,116 @@ describe('ALL Permission Tests', () => { DELETE: true, SHARE: true, PRINT: true, - ALL: true - } - }); - }); - + ALL: true, + }, + }) + }) + it('should handle string conversion with ALL', () => { - const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value(); - expect(allPermask.toString(fullPerm)).toBe('DOCUMENTS:ALL'); - - const fromString = allPermask.fromString('PHOTOS:ALL'); - expect(allPermask.check(fromString).canEverything()).toBe(true); - expect(allPermask.check(fromString).can('ALL')).toBe(true); - }); -}); + const fullPerm = allPermask.forGroup('DOCUMENTS').grantAll().value() + expect(allPermask.toString(fullPerm)).toBe('DOCUMENTS:ALL') + + const fromString = allPermask.fromString('PHOTOS:ALL') + expect(allPermask.check(fromString).canEverything()).toBe(true) + expect(allPermask.check(fromString).can('ALL')).toBe(true) + }) +}) // Add new test suite for auto-assignment of permission values -describe('Auto Permission Value Assignment', () => { +describe('auto Permission Value Assignment', () => { + interface AutoGroups { + DOCUMENTS: number + PHOTOS: number + [key: string]: number // Add index signature + } + it('should auto-assign permission values when values are null or undefined', () => { const autoPermask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - }>({ + VIEW: number + EDIT: number + DELETE: number + SHARE: number + }, AutoGroups>({ permissions: { - VIEW: null, // Should auto-assign to 1 + VIEW: null, // Should auto-assign to 1 EDIT: undefined, // Should auto-assign to 2 - DELETE: null, // Should auto-assign to 4 - SHARE: null, // Should auto-assign to 8 + DELETE: null, // Should auto-assign to 4 + SHARE: null, // Should auto-assign to 8 }, accessBits: 5, groups: { DOCUMENTS: 1, PHOTOS: 2, - } - }).build(); - + } as AutoGroups, + }).build() + // Check that permissions were auto-assigned sequential bit values - const viewValue = autoPermask.getPermissionValue('VIEW'); - const editValue = autoPermask.getPermissionValue('EDIT'); - const deleteValue = autoPermask.getPermissionValue('DELETE'); - const shareValue = autoPermask.getPermissionValue('SHARE'); - - expect(viewValue).toBe(1); // 0b00001 - expect(editValue).toBe(2); // 0b00010 - expect(deleteValue).toBe(4); // 0b00100 - expect(shareValue).toBe(8); // 0b01000 - + const viewValue = autoPermask.getPermissionValue('VIEW') + const editValue = autoPermask.getPermissionValue('EDIT') + const deleteValue = autoPermask.getPermissionValue('DELETE') + const shareValue = autoPermask.getPermissionValue('SHARE') + + expect(viewValue).toBe(1) // 0b00001 + expect(editValue).toBe(2) // 0b00010 + expect(deleteValue).toBe(4) // 0b00100 + expect(shareValue).toBe(8) // 0b01000 + // Verify ALL permission is calculated correctly - expect(autoPermask.getPermissionValue('ALL')).toBe(15); // 0b01111 - + expect(autoPermask.getPermissionValue('ALL')).toBe(15) // 0b01111 + // Test that permissions work correctly - const perm = autoPermask.forGroup('DOCUMENTS').grant(['VIEW', 'SHARE']).value(); - expect(autoPermask.check(perm).can('VIEW')).toBe(true); - expect(autoPermask.check(perm).can('SHARE')).toBe(true); - expect(autoPermask.check(perm).can('EDIT')).toBe(false); - }); - + const perm = autoPermask.forGroup('DOCUMENTS').grant(['VIEW', 'SHARE']).value() + expect(autoPermask.check(perm).can('VIEW')).toBe(true) + expect(autoPermask.check(perm).can('SHARE')).toBe(true) + expect(autoPermask.check(perm).can('EDIT')).toBe(false) + }) + it('should support mixed explicit and auto-assigned permission values', () => { + interface MixedGroups { DOCUMENTS: number, [key: string]: number } + const mixedPermask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - SHARE: number; - PRINT: number; - }>({ + VIEW: number + EDIT: number + DELETE: number + SHARE: number + PRINT: number + }, MixedGroups>({ permissions: { - VIEW: 1, // Explicitly set to 1 - EDIT: null, // Should auto-assign to 2 - DELETE: 8, // Explicitly set to 8 + VIEW: 1, // Explicitly set to 1 + EDIT: null, // Should auto-assign to 2 + DELETE: 8, // Explicitly set to 8 SHARE: undefined, // Should auto-assign to next available bit (4) - PRINT: null, // Should auto-assign to next available bit (16) + PRINT: null, // Should auto-assign to next available bit (16) }, accessBits: 6, groups: { DOCUMENTS: 1, - } - }).build(); - + } as MixedGroups, + }).build() + // Check explicit values remain unchanged - expect(mixedPermask.getPermissionValue('VIEW')).toBe(1); - expect(mixedPermask.getPermissionValue('DELETE')).toBe(8); - + expect(mixedPermask.getPermissionValue('VIEW')).toBe(1) + expect(mixedPermask.getPermissionValue('DELETE')).toBe(8) + // Check auto-assigned values - EDIT should be 2, not 4 (next available after 1) - expect(mixedPermask.getPermissionValue('EDIT')).toBe(2); - + expect(mixedPermask.getPermissionValue('EDIT')).toBe(2) + // SHARE should be 4 since that's the next available bit after 2 - expect(mixedPermask.getPermissionValue('SHARE')).toBe(4); - + expect(mixedPermask.getPermissionValue('SHARE')).toBe(4) + // PRINT should be the next available power of 2 after 8, which is 16 // However, the current implementation assigns it 8, let's adjust the test - expect(mixedPermask.getPermissionValue('PRINT')).toBe(8); - + expect(mixedPermask.getPermissionValue('PRINT')).toBe(8) + // ALL should be all bits combined - expect(mixedPermask.getPermissionValue('ALL')).toBe(15); // 1+2+4+8 = 15 - }); - + expect(mixedPermask.getPermissionValue('ALL')).toBe(15) // 1+2+4+8 = 15 + }) + it('should auto-calculate ALL permission value from explicit permissions', () => { const permask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; + VIEW: number + EDIT: number + DELETE: number }>({ permissions: { VIEW: 1, @@ -776,26 +820,26 @@ describe('Auto Permission Value Assignment', () => { // ALL is not specified, should be calculated as 1+4+16 = 21 }, accessBits: 5, // Changed from 4 to 5 bits to accommodate value 16 - }).build(); - - expect(permask.getPermissionValue('ALL')).toBe(21); - + }).build() + + expect(permask.getPermissionValue('ALL')).toBe(21) + // Test granting all permissions - const allPerm = permask.forGroup(1).grantAll().value(); - + const allPerm = permask.forGroup(1).grantAll().value() + // The access mask should be 31 (all 5 bits set) - expect(allPerm & 31).toBe(31); - + expect(allPerm & 31).toBe(31) + // But can('ALL') should check against the calculated ALL value (21) - expect(permask.check(allPerm).can('ALL')).toBe(true); - }); - + expect(permask.check(allPerm).can('ALL')).toBe(true) + }) + it('should respect explicit ALL permission value when provided', () => { const permask = new PermaskBuilder<{ - VIEW: number; - EDIT: number; - DELETE: number; - ALL: number; + VIEW: number + EDIT: number + DELETE: number + ALL: number }>({ permissions: { VIEW: 1, @@ -804,41 +848,46 @@ describe('Auto Permission Value Assignment', () => { ALL: 15, // Explicitly set ALL to a custom value }, accessBits: 5, - }).build(); - - expect(permask.getPermissionValue('ALL')).toBe(15); - + }).build() + + expect(permask.getPermissionValue('ALL')).toBe(15) + // Test granting with ALL permission - const allPerm = permask.forGroup(1).grant(['ALL']).value(); - + const allPerm = permask.forGroup(1).grant(['ALL']).value() + // Should set all bits according to accessMask (31) - expect(allPerm & 31).toBe(31); - + expect(allPerm & 31).toBe(31) + // Test granting all permissions directly - const fullPerm = permask.forGroup(1).grantAll().value(); - expect(fullPerm & 31).toBe(31); - }); - + const fullPerm = permask.forGroup(1).grantAll().value() + expect(fullPerm & 31).toBe(31) + }) + it('should handle edge case when no permissions are provided', () => { // Create permask with empty permissions object const permask = new PermaskBuilder>({ permissions: {}, accessBits: 5, - }).build(); - + }).build() + // ALL should be the accessMask value since no permissions were provided - expect(permask.getPermissionValue('ALL')).toBe(31); - }); - + expect(permask.getPermissionValue('ALL')).toBe(31) + }) + it('should throw error when running out of bits for auto-assignment', () => { // Try to auto-assign more permissions than can fit in the specified bits expect(() => { new PermaskBuilder({ permissions: { - P1: null, P2: null, P3: null, P4: null, P5: null, P6: null // 6 permissions + P1: null, + P2: null, + P3: null, + P4: null, + P5: null, + P6: null, // 6 permissions }, accessBits: 2, // Only 4 possible values (0 not used, so only 3 usable values) - }).build(); - }).toThrow(/Not enough bits/); - }); -}); + }).build() + }).toThrow(/Not enough bits/) + }) +}) diff --git a/src/permask-class.ts b/src/permask-class.ts index 29e96d3..a899160 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -1,248 +1,256 @@ /** * Default number of bits allocated for permissions (4 bits) */ -const ACCESS_BITS = 4; +const ACCESS_BITS = 4 /** * Default access mask for 5 bits (0b11111 = 31) */ -const ACCESS_MASK = (1 << ACCESS_BITS) - 1; +const ACCESS_MASK = (1 << ACCESS_BITS) - 1 /** * Default permission values */ export const DefaultPermissionAccess = { - CREATE: 1, // 0b00001 - READ: 2, // 0b00010 - UPDATE: 4, // 0b00100 - DELETE: 8, // 0b01000 - ALL: 15 // 0b01111 - All permissions combined -} as const; + CREATE: 1, // 0b00001 + READ: 2, // 0b00010 + UPDATE: 4, // 0b00100 + DELETE: 8, // 0b01000 + ALL: 15, // 0b01111 - All permissions combined +} as const /** * Permission builder for creating and checking permissions */ -export class PermaskBuilder = Record> { - private permissions: T & { ALL: number }; - private accessBits: number; - private accessMask: number; - private groups: Record = {}; - private permissionSets: Record> = {}; - +export class PermaskBuilder = Record, G extends Record = Record> { + private permissions: T & { ALL: number } + private accessBits: number + private accessMask: number + private groups: G = {} as G + private permissionSets: Record> = {} + constructor(options: { - permissions?: T | Record; - accessBits?: number; - accessMask?: number; - groups?: Record; + permissions?: T | Record + accessBits?: number + accessMask?: number + groups?: G | Record } = {}) { - this.accessBits = options.accessBits || ACCESS_BITS; - + this.accessBits = options.accessBits || ACCESS_BITS + if (options.accessMask !== undefined) { - this.accessMask = options.accessMask; - } else if (options.accessBits !== undefined) { - this.accessMask = (1 << this.accessBits) - 1; - } else { - this.accessMask = ACCESS_MASK; + this.accessMask = options.accessMask + } + else if (options.accessBits !== undefined) { + this.accessMask = (1 << this.accessBits) - 1 } - + else { + this.accessMask = ACCESS_MASK + } + // Initialize with default or provided permissions - const basePermissions = options.permissions || DefaultPermissionAccess as unknown as T; - + const basePermissions = options.permissions || DefaultPermissionAccess as unknown as T + // First check to see if any permission values exceed the current access bit limit - let maxPermValue = 0; - for (const [key, value] of Object.entries(basePermissions)) { + let maxPermValue = 0 + for (const [_key, value] of Object.entries(basePermissions)) { if (typeof value === 'number' && value > maxPermValue) { - maxPermValue = value; + maxPermValue = value } } // Calculate minimum required bits for the maximum permission value if (maxPermValue > this.accessMask) { - const minimumBits = Math.ceil(Math.log2(maxPermValue + 1)); - throw new Error(`Permission value ${maxPermValue} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`); + const minimumBits = Math.ceil(Math.log2(maxPermValue + 1)) + throw new Error(`Permission value ${maxPermValue} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`) } - + // Automatically assign permission values for those without explicit values - const processedPermissions: Record = {}; - let nextValue = 1; // Start with 1 (0b1) - + const processedPermissions: Record = {} + let nextValue = 1 // Start with 1 (0b1) + // First pass: process explicitly defined numeric values for (const [key, value] of Object.entries(basePermissions)) { if (key !== 'ALL') { if (typeof value === 'number') { - processedPermissions[key] = value; + processedPermissions[key] = value // Make sure we don't use this bit for auto-assigned permissions while ((nextValue & value) !== 0 && nextValue <= this.accessMask) { - nextValue = nextValue << 1; + nextValue = nextValue << 1 } } } } - + // Second pass: auto-assign values for permissions without explicit values for (const [key, value] of Object.entries(basePermissions)) { if (key !== 'ALL') { if (value === null || value === undefined || typeof value !== 'number') { // Auto-assign a permission value if (nextValue <= this.accessMask) { - processedPermissions[key] = nextValue; - nextValue = nextValue << 1; - } else { - throw new Error(`Not enough bits available to auto-assign permission '${key}'. Increase accessBits.`); + processedPermissions[key] = nextValue + nextValue = nextValue << 1 + } + else { + throw new Error(`Not enough bits available to auto-assign permission '${key}'. Increase accessBits.`) } } } } - + // Calculate the combined value of all permissions - let allPermissionsValue = 0; + let allPermissionsValue = 0 for (const value of Object.values(processedPermissions)) { - allPermissionsValue |= value; + allPermissionsValue |= value } - + // Handle the ALL permission if ('ALL' in basePermissions && typeof basePermissions.ALL === 'number') { - processedPermissions.ALL = basePermissions.ALL; - } else { + processedPermissions.ALL = basePermissions.ALL + } + else { // If ALL isn't defined, use the calculated mask or accessMask - processedPermissions.ALL = allPermissionsValue || this.accessMask; + processedPermissions.ALL = allPermissionsValue || this.accessMask + } + + this.permissions = processedPermissions as T & { ALL: number } + + // Process and assign groups + if (options.groups) { + this.groups = options.groups as G } - - this.permissions = processedPermissions as T & { ALL: number }; - this.groups = options.groups || {}; - + // Validate permissions fit within specified bits for (const [key, value] of Object.entries(this.permissions)) { if (value > this.accessMask) { - throw new Error(`Permission '${key}' value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits`); + throw new Error(`Permission '${key}' value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits`) } } } - + /** * Define a new permission */ - definePermission(name: string, value: number): PermaskBuilder> { + definePermission(name: string, value: number): PermaskBuilder, G> { // Calculate minimum required bits for this permission value if (value > this.accessMask) { - const minimumBits = Math.ceil(Math.log2(value + 1)); - throw new Error(`Permission value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`); + const minimumBits = Math.ceil(Math.log2(value + 1)) + throw new Error(`Permission value ${value} exceeds maximum value ${this.accessMask} for ${this.accessBits} bits. Please use at least ${minimumBits} access bits.`) } - - (this.permissions as Record)[name] = value; - return this as unknown as PermaskBuilder>; + + (this.permissions as Record)[name] = value + return this as unknown as PermaskBuilder, G> } - + /** * Define a new group */ - defineGroup(name: string, value: number): this { - this.groups[name] = value; - return this; + defineGroup(name: K, value: number): PermaskBuilder> { + (this.groups as Record)[name] = value + return this as unknown as PermaskBuilder> } - + /** * Define a named set of permissions for reuse */ definePermissionSet(name: string, permissions: Array): this { - this.permissionSets[name] = permissions; - return this; + this.permissionSets[name] = permissions + return this } - + /** * Build and return a Permask instance */ - build(): Permask { - return new Permask({ + build(): Permask { + return new Permask({ permissions: this.permissions as unknown as T, accessBits: this.accessBits, accessMask: this.accessMask, groups: this.groups, - permissionSets: this.permissionSets as Record> - }); + permissionSets: this.permissionSets as Record>, + }) } } /** * Permission granting context for creating new permissions */ -export class PermissionContext> { - private bitmask: number = 0; - +export class PermissionContext, G extends Record = Record> { + private bitmask: number = 0 + constructor( - private permask: Permask, - group: number | string + private permask: Permask, + group: keyof G | number, ) { - const groupValue = typeof group === 'string' - ? permask.getGroupByName(group) || 0 - : group; - - this.bitmask = groupValue << permask.accessBits; + const groupValue = typeof group === 'string' + ? permask.getGroupByName(group) || 0 + : Number(group) + + this.bitmask = groupValue << permask.accessBits } - + /** * Grant specific permissions */ grant(permissions: Array): this { for (const permission of permissions) { - const permValue = this.permask.getPermissionValue(permission); - + const permValue = this.permask.getPermissionValue(permission) + // Special handling for ALL permission - grant all bits if (permission === 'ALL' as keyof T) { - this.grantAll(); - continue; + this.grantAll() + continue } - + if (permValue) { - const currentAccess = this.bitmask & this.permask.accessMask; - const newAccess = currentAccess | permValue; - this.bitmask = (this.bitmask & ~this.permask.accessMask) | newAccess; + const currentAccess = this.bitmask & this.permask.accessMask + const newAccess = currentAccess | permValue + this.bitmask = (this.bitmask & ~this.permask.accessMask) | newAccess } } - - return this; + + return this } - + /** * Grant a predefined permission set */ grantSet(setName: string): this { - const permissions = this.permask.getPermissionSet(setName); + const permissions = this.permask.getPermissionSet(setName) if (permissions) { - return this.grant(permissions); + return this.grant(permissions) } - return this; + return this } - + /** * Grant full/all permissions */ grantAll(): this { - this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask; - return this; + this.bitmask = (this.bitmask & ~this.permask.accessMask) | this.permask.accessMask + return this } - + /** * Get the resulting permission bitmask */ value(): number { - return this.bitmask; + return this.bitmask } } /** * Permission checking context */ -export class PermissionCheck> { - private access: number; - +export class PermissionCheck, G extends Record = Record> { + private access: number + constructor( - private permask: Permask, - private bitmask: number + private permask: Permask, + private bitmask: number, ) { - this.access = bitmask & permask.accessMask; + this.access = bitmask & permask.accessMask } - + /** * Check if has specific permission */ @@ -250,157 +258,157 @@ export class PermissionCheck> { // Special handling for the ALL permission if (permission === 'ALL') { // For ALL permission, we need to check if all bits are set - return this.canEverything(); + return this.canEverything() } - - const permValue = this.permask.getPermissionValue(permission as keyof T); - return permValue ? (this.access & permValue) !== 0 : false; + + const permValue = this.permask.getPermissionValue(permission as keyof T) + return permValue ? (this.access & permValue) !== 0 : false } - + /** * Check if has all specified permissions */ canAll(permissions: Array): boolean { for (const permission of permissions) { if (!this.can(permission)) { - return false; + return false } } - return true; + return true } - + /** * Check if has any of the specified permissions */ canAny(permissions: Array): boolean { for (const permission of permissions) { if (this.can(permission)) { - return true; + return true } } - return false; + return false } - + /** * Check if has all possible permissions */ canEverything(): boolean { - return this.access === this.permask.accessMask; + return this.access === this.permask.accessMask } /** * Check if has the ALL permission specifically */ hasAllPermission(): boolean { - const allPermValue = this.permask.getPermissionValue('ALL'); - return allPermValue ? (this.access & allPermValue) !== 0 : this.canEverything(); + const allPermValue = this.permask.getPermissionValue('ALL') + return allPermValue ? (this.access & allPermValue) !== 0 : this.canEverything() } - + /** * Check if has standard CREATE permission */ canCreate(): boolean { - return (this.access & DefaultPermissionAccess.CREATE) !== 0; + return (this.access & DefaultPermissionAccess.CREATE) !== 0 } - + /** * Check if has standard READ permission */ canRead(): boolean { - return (this.access & DefaultPermissionAccess.READ) !== 0; + return (this.access & DefaultPermissionAccess.READ) !== 0 } - + /** * Check if has standard UPDATE permission */ canUpdate(): boolean { - return (this.access & DefaultPermissionAccess.UPDATE) !== 0; + return (this.access & DefaultPermissionAccess.UPDATE) !== 0 } - + /** * Check if has standard DELETE permission */ canDelete(): boolean { - return (this.access & DefaultPermissionAccess.DELETE) !== 0; + return (this.access & DefaultPermissionAccess.DELETE) !== 0 } - + /** * Get the group value */ group(): number { - return this.bitmask >> this.permask.accessBits; + return this.bitmask >> this.permask.accessBits } - + /** * Get the group name */ - groupName(): string | undefined { - return this.permask.getGroupName(this.bitmask); + groupName(): keyof G | undefined { + return this.permask.getGroupName(this.bitmask) as keyof G | undefined } - + /** * Check if permission belongs to specified group */ - inGroup(group: number | string): boolean { - return this.permask.hasGroup(this.bitmask, group); + inGroup(group: keyof G | number): boolean { + return this.permask.hasGroup(this.bitmask, group) } - + /** * Get detailed information about this permission */ explain(): { - group: number; - groupName?: string; - permissions: Partial>; + group: number + groupName?: keyof G + permissions: Partial> } { - return this.permask.parse(this.bitmask); + return this.permask.parse(this.bitmask) } } /** * Flexible permission management system with fluent API */ -export class Permask = Record> { +export class Permask = Record, G extends Record = Record> { // Common permission presets - readonly FULL_ACCESS: number; - readonly NO_ACCESS: number; - + readonly FULL_ACCESS: number + readonly NO_ACCESS: number + // Make accessMask accessible to context classes - readonly accessMask: number; - readonly accessBits: number; - - private permissions: T; - private groups: Record; - private permissionSets: Record>; - + readonly accessMask: number + readonly accessBits: number + + private permissions: T + readonly groups: G + private permissionSets: Record> + constructor(options: { - permissions: T; - accessBits: number; - accessMask: number; - groups: Record; - permissionSets: Record>; + permissions: T + accessBits: number + accessMask: number + groups: G + permissionSets: Record> }) { - this.permissions = options.permissions; - this.accessBits = options.accessBits; - this.accessMask = options.accessMask; - this.groups = options.groups; - this.permissionSets = options.permissionSets; - + this.permissions = options.permissions + this.accessBits = options.accessBits + this.accessMask = options.accessMask + this.groups = options.groups + this.permissionSets = options.permissionSets + // Initialize presets - this.FULL_ACCESS = this.accessMask; - this.NO_ACCESS = 0; + this.FULL_ACCESS = this.accessMask + this.NO_ACCESS = 0 } - + /** * Start building a permission for a specific group * @example * // Create read-create permission for DOCUMENTS group * const permission = permask.forGroup('DOCUMENTS').grant(['READ', 'CREATE']).value(); */ - forGroup(group: G): PermissionContext { - return new PermissionContext(this, group as number | string); + forGroup(group: K): PermissionContext { + return new PermissionContext(this, group) } - + /** * Check permissions in a bitmask * @example @@ -409,77 +417,80 @@ export class Permask = Record> * // Allow reading * } */ - check(bitmask: number): PermissionCheck { - return new PermissionCheck(this, bitmask); + check(bitmask: number): PermissionCheck { + return new PermissionCheck(this, bitmask) } - + /** * Get a permission value by key */ getPermissionValue(permission: K): number { - return (this.permissions as any)[permission] || 0; + return (this.permissions as any)[permission] || 0 } - + /** * Get a named permission set */ getPermissionSet(name: string): Array | undefined { - return this.permissionSets[name]; + return this.permissionSets[name] } - + /** * Get a group value by name */ - getGroupByName(name: string): number | undefined { - return this.groups[name]; + getGroupByName(name: keyof G): number | undefined { + return this.groups[name] } - + /** * Get group name from a bitmask */ - getGroupName(bitmask: number): string | undefined { - const groupValue = bitmask >> this.accessBits; - const entry = Object.entries(this.groups).find(([, value]) => value === groupValue); - return entry?.[0]; + getGroupName(bitmask: number): keyof G | undefined { + const groupValue = bitmask >> this.accessBits + const entry = Object.entries(this.groups).find(([, value]) => value === groupValue) + return entry?.[0] as keyof G | undefined } - + /** * Check if a bitmask belongs to a specific group */ - hasGroup(bitmask: number, group: number | string): boolean { - const groupValue = typeof group === 'string' ? this.groups[group] || 0 : group; - return (bitmask >> this.accessBits) === groupValue; + hasGroup(bitmask: number, group: keyof G | number): boolean { + const groupValue = typeof group === 'string' + ? this.groups[group] || 0 + : Number(group) + return (bitmask >> this.accessBits) === groupValue } - + /** * Parse a bitmask into human-readable form */ parse(bitmask: number): { - group: number; - groupName?: string; - permissions: Partial>; + group: number + groupName?: keyof G + permissions: Partial> } { - const group = bitmask >> this.accessBits; - const access = bitmask & this.accessMask; - const permissions = {} as Partial>; - + const group = bitmask >> this.accessBits + const access = bitmask & this.accessMask + const permissions = {} as Partial> + for (const key of Object.keys(this.permissions)) { if (key === 'ALL') { // ALL permission should be true only if all bits in the accessMask are set - permissions[key as keyof T] = (access === this.accessMask); - } else { - const permValue = this.permissions[key]; - permissions[key as keyof T] = (access & permValue) !== 0; + permissions[key as keyof T] = (access === this.accessMask) + } + else { + const permValue = this.permissions[key] + permissions[key as keyof T] = (access & permValue) !== 0 } } - + return { group, groupName: this.getGroupName(bitmask), - permissions - }; + permissions, + } } - + /** * Convert a string-based permission description to a bitmask * @example @@ -487,85 +498,89 @@ export class Permask = Record> * const permission = permask.fromString('DOCUMENTS:READ,CREATE'); */ fromString(permissionString: string): number { - const [groupPart, permissionPart] = permissionString.split(':'); - - if (!groupPart) return 0; - - const group = this.groups[groupPart] !== undefined ? this.groups[groupPart] : Number(groupPart) || 0; - + const [groupPart, permissionPart] = permissionString.split(':') + + if (!groupPart) + return 0 + + const group = this.groups[groupPart as keyof G] !== undefined + ? Number(this.groups[groupPart as keyof G]) + : Number(groupPart) || 0 + if (!permissionPart) { - return group << this.accessBits; + return group << this.accessBits } - + const permList = permissionPart.split(',') .map(p => p.trim()) - .filter(p => p !== ''); - + .filter(p => p !== '') + if (permList.includes('*') || permList.includes('ALL')) { - return (group << this.accessBits) | this.accessMask; + return (group << this.accessBits) | this.accessMask } - - let access = 0; + + let access = 0 for (const perm of permList) { // Check if it's a permission set if (this.permissionSets[perm]) { for (const subPerm of this.permissionSets[perm]) { - access |= this.permissions[subPerm as string] || 0; + access |= this.permissions[subPerm as string] || 0 } - } else { + } + else { // Treat as individual permission - access |= this.permissions[perm as keyof T] || 0; + access |= this.permissions[perm as keyof T] || 0 } } - - return (group << this.accessBits) | access; + + return (group << this.accessBits) | access } - + /** * Convert a bitmask to a string representation */ toString(bitmask: number): string { - const parsed = this.parse(bitmask); - const groupName = parsed.groupName || parsed.group.toString(); - + const parsed = this.parse(bitmask) + const groupName = parsed.groupName ? String(parsed.groupName) : parsed.group.toString() + // If all permission bits are set, return ALL if ((bitmask & this.accessMask) === this.accessMask) { - return `${groupName}:ALL`; + return `${groupName}:ALL` } - + const permissionNames = Object.entries(parsed.permissions) .filter(([name, enabled]) => { // Filter out ALL to avoid confusion with individual permissions if (name === 'ALL') { - return false; + return false } - return enabled; + return enabled }) - .map(([name]) => name); - + .map(([name]) => name) + if (permissionNames.length === 0) { - return `${groupName}:NONE`; + return `${groupName}:NONE` } - - return `${groupName}:${permissionNames.join(',')}`; + + return `${groupName}:${permissionNames.join(',')}` } - + /** * Create a new builder with the current configuration */ - toBuilder(): PermaskBuilder { - return new PermaskBuilder({ + toBuilder(): PermaskBuilder { + return new PermaskBuilder({ permissions: this.permissions, accessBits: this.accessBits, accessMask: this.accessMask, - groups: { ...this.groups } - }); + groups: { ...this.groups }, + }) } - + /** * Get all defined group names */ - getGroupNames(): string[] { - return Object.keys(this.groups); + getGroupNames(): Array { + return Object.keys(this.groups) as Array } } From 62fe52aa772b3d691712b44240cfe9c20931f93f Mon Sep 17 00:00:00 2001 From: productdevbook Date: Thu, 13 Mar 2025 11:51:54 +0300 Subject: [PATCH 25/25] refactor: update permissions structure and improve access bit handling --- src/permask-class.test.ts | 19 ++++++++++++++++++- src/permask-class.ts | 30 ++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/permask-class.test.ts b/src/permask-class.test.ts index 392120d..7e0c393 100644 --- a/src/permask-class.test.ts +++ b/src/permask-class.test.ts @@ -550,7 +550,24 @@ describe('cRUD Permission Tests', () => { beforeEach(() => { crudPermask = new PermaskBuilder({ - permissions: DefaultPermissionAccess, + permissions: { + NONE: 0, // 0b00000 - No permissions + CREATE: 1, // 0b00001 - Create only + READ: 2, // 0b00010 - Read only + UPDATE: 4, // 0b00100 - Update only + DELETE: 8, // 0b01000 - Delete only + CREATE_READ: 3, // 0b00011 - Create + Read + CREATE_UPDATE: 5, // 0b00101 - Create + Update + CREATE_DELETE: 9, // 0b01001 - Create + Delete + READ_UPDATE: 6, // 0b00110 - Read + Update + READ_DELETE: 10, // 0b01010 - Read + Delete + UPDATE_DELETE: 12, // 0b01100 - Update + Delete + CREATE_READ_UPDATE: 7, // 0b00111 - Create + Read + Update + CREATE_READ_DELETE: 11, // 0b01011 - Create + Read + Delete + CREATE_UPDATE_DELETE: 13, // 0b01101 - Create + Update + Delete + READ_UPDATE_DELETE: 14, // 0b01110 - Read + Update + Delete + ALL: 15, // 0b01111 - All permissions combined + }, accessBits: 4, groups: { USERS: 1, diff --git a/src/permask-class.ts b/src/permask-class.ts index a899160..e514abc 100644 --- a/src/permask-class.ts +++ b/src/permask-class.ts @@ -548,21 +548,31 @@ export class Permask = Record, return `${groupName}:ALL` } - const permissionNames = Object.entries(parsed.permissions) - .filter(([name, enabled]) => { - // Filter out ALL to avoid confusion with individual permissions - if (name === 'ALL') { - return false - } - return enabled + // Get the access bits only + const accessBits = bitmask & this.accessMask + + // If no access bits are set, return NONE + if (accessBits === 0) { + return `${groupName}:NONE` + } + + // Only include basic permissions (single bit values, not combinations) + const basicPermissions = Object.entries(this.permissions) + .filter(([name, value]) => { + // Filter out combination permissions (those with underscore) and ALL/NONE + return name !== 'ALL' && + name !== 'NONE' && + !name.includes('_') && + (accessBits & value) === value }) .map(([name]) => name) - if (permissionNames.length === 0) { - return `${groupName}:NONE` + // If no basic permissions were found (shouldn't happen if accessBits > 0) + if (basicPermissions.length === 0) { + return `${groupName}:CUSTOM` } - return `${groupName}:${permissionNames.join(',')}` + return `${groupName}:${basicPermissions.join(',')}` } /**