permix
Version:
Permix is a lightweight, framework-agnostic, type-safe permissions management library for JavaScript applications on the client and server sides.
264 lines (257 loc) • 6.29 kB
JavaScript
function createHooks$1() {
let hooks = {};
const hook = (name, fn) => {
if (!hooks[name]) {
hooks[name] = [];
}
hooks[name].push(fn);
return () => {
const index = hooks[name].indexOf(fn);
if (index !== -1) {
hooks[name].splice(index, 1);
}
};
};
const hookOnce = (name, fn) => {
const remove = hook(name, (...args) => {
remove();
fn(...args);
});
};
const removeHook = (name, fn) => {
if (hooks[name]) {
const index = hooks[name].indexOf(fn);
if (index !== -1) {
hooks[name].splice(index, 1);
}
}
};
const callHook = (name, ...args) => {
if (hooks[name]) {
for (const fn of hooks[name]) {
fn(...args);
}
}
};
const clearHook = name => {
delete hooks[name];
};
const clearAllHooks = () => {
hooks = {};
};
return {
hook,
hookOnce,
removeHook,
callHook,
clearHook,
clearAllHooks
};
}
/**
* @example
* const permissions = {
* post: {
* create: true,
* read: false,
* },
* }
*
* isRulesValid(permissions) // true
*
* const permissions2 = {
* post: {
* create: true,
* read: 'string',
* },
* }
*
* isRulesValid(permissions2) // false
*/
function isRulesValid(value) {
if (typeof value !== 'object' || value === null) {
return false;
}
return Object.values(value).every(action => Object.values(action).every(action => typeof action === 'boolean' || typeof action === 'function'));
}
function createTemplate(rules) {
function validate(p) {
if (!isRulesValid(p)) {
throw new Error('[Permix]: Permissions in template are not valid.');
}
}
if (typeof rules === 'function') {
return param => {
const p = rules(param);
validate(p);
return p;
};
}
validate(rules);
return () => rules;
}
function createHooks() {
return createHooks$1();
}
const permixSymbol = Symbol('permix');
function checkWithRules(state, ...[entity, action, data]) {
if (!state) {
console.error('[Permix]: Rules wasn\'t provided. Please setup permissions and try again.');
return false;
}
if (!state[entity]) {
console.error(`[Permix]: Incorrect entity name "${String(entity)}". Please check the name of your validation entity.`);
return false;
}
const entityObj = state[entity];
const actions = Array.isArray(action) ? action : [action];
const actionValues = action === 'all' ? Object.values(entityObj) : actions.map(a => entityObj[a]);
return actionValues.every(action => {
if (typeof action === 'function') {
return Boolean(action(data));
}
return action ?? false;
});
}
/**
* Interface for the Permix permission manager
* @example
* ```ts
* const permix = createPermix<{
* post: {
* dataType: { id: string }
* action: 'create' | 'read'
* }
* }>()
* ```
*/
/**
* Create a Permix instance
*
* @link https://permix.letstri.dev/docs/guide/instance
*
* @example
* ```ts
* const permix = createPermix<{
* post: {
* dataType: { id: string }
* action: 'create' | 'read'
* },
* user: {
* dataType: { id: string }
* action: 'create' | 'read'
* }
* }>()
*
* permix.setup({
* post: { create: false },
* user: { read: true }
* })
*
* console.log(permix.check('post', 'create')) // false
* console.log(permix.check('user', 'read')) // true
* ```
*/
function createPermix(initial) {
let rules = null;
let isSetupCalled = false;
let isReady = false;
let resolveSetup;
const hooks = createHooks();
const setupPromise = new Promise(res => {
resolveSetup = () => res(undefined);
});
hooks.hook('ready', () => {
if (typeof window !== 'undefined') {
isReady = true;
}
});
hooks.hook('setup', r => {
rules = r;
isSetupCalled = true;
if (!isReady) {
hooks.callHook('ready');
}
resolveSetup();
});
if (initial) {
hooks.callHook('setup', initial);
}
const permix = {
check(...args) {
return checkWithRules(rules, ...args);
},
async checkAsync(...args) {
await setupPromise;
return checkWithRules(rules, ...args);
},
setup(rules) {
if (!isRulesValid(rules)) {
throw new Error('[Permix]: Permissions in setup are not valid.');
}
hooks.callHook('setup', rules);
},
hook: hooks.hook,
hookOnce: hooks.hookOnce,
template: createTemplate,
isReady: () => isReady,
isReadyAsync: async () => {
await setupPromise;
return isReady;
},
dehydrate: () => {
if (!isSetupCalled) {
throw new Error('[Permix]: To dehydrate Permix, `setup` must be called first.');
}
const processedSetup = {};
for (const entity in rules) {
processedSetup[entity] = {};
for (const action in rules[entity]) {
const value = rules[entity][action];
processedSetup[entity][action] = typeof value === 'function' ? false : value;
}
}
return processedSetup;
},
hydrate: state => {
const parsedRules = {};
for (const entity in state) {
parsedRules[entity] = {};
for (const action in state[entity]) {
const value = state[entity][action];
parsedRules[entity][action] = value;
}
}
hooks.callHook('hydrate');
const timeout = setTimeout(() => {
console.error('[Permix]: You should call `setup` immediately after hydration to fully restore Permix state. https://permix.letstri.dev/docs/guide/hydration');
}, 1000);
hooks.hook('setup', () => {
clearTimeout(timeout);
});
rules = parsedRules;
},
_: {
isSetupCalled: () => isSetupCalled,
getRules: () => {
return rules;
},
setRules: r => {
rules = r;
},
hooks,
[permixSymbol]: true
}
};
return permix;
}
function validatePermix(permix) {
if (!permix._[permixSymbol]) {
throw new Error('[Permix]: Permix instance is not valid');
}
}
function getRules(permix) {
validatePermix(permix);
return permix._.getRules();
}
export { createPermix as a, checkWithRules as b, createTemplate as c, getRules as g, isRulesValid as i, validatePermix as v };