UNPKG

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
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 };