import { Rule, RuleOptions } from './Rule'; import { RawRuleFrom } from './RawRule'; import { Abilities, Normalize, SubjectType, AbilityParameters, AbilityTuple, ExtractSubjectType } from './types'; import { wrapArray, detectSubjectType, mergePrioritized, getOrDefault, identity, isSubjectType, DETECT_SUBJECT_TYPE_STRATEGY, filterWithLazyAllocation } from './utils'; import { LinkedItem, linkedItem, unlinkItem } from './structures/LinkedItem'; export interface RuleIndexOptions extends Partial> { detectSubjectType?( subject: Exclude[1], SubjectType> ): ExtractSubjectType[1]>; anyAction?: string; anySubjectType?: string; } export declare const ɵabilities: unique symbol; export declare const ɵconditions: unique symbol; interface WithGenerics { [ɵabilities]: any [ɵconditions]: any } export type Public = { [K in keyof T]: T[K] }; export interface Generics { abilities: T[typeof ɵabilities], conditions: T[typeof ɵconditions] } export type RuleOf = Rule['abilities'], Generics['conditions']>; export type RawRuleOf = RawRuleFrom['abilities'], Generics['conditions']>; export type RuleIndexOptionsOf = RuleIndexOptions['abilities'], Generics['conditions']>; interface AbilityEvent { target: T /** @deprecated use "target" property instead */ ability: T } export interface UpdateEvent extends AbilityEvent { rules: RawRuleOf[] } /** * @deprecated `on`/`emit` properly infer type without this type * TODO(major): delete */ export type EventHandler = (event: Event) => void; export type Events< T extends WithGenerics, K extends keyof EventsMap = keyof EventsMap > = Map[K]> | null>; interface EventsMap { update(event: UpdateEvent): void updated(event: UpdateEvent): void } type IndexTree = Map[], merged: boolean }>>; export type Unsubscribe = () => void; const defaultActionEntry = () => ({ rules: [] as unknown as Rule[], merged: false }); const defaultSubjectEntry = () => new Map>(); type AbilitySubjectTypeParameters = AbilityParameters< T, T extends AbilityTuple ? IncludeField extends true ? (action: T[0], subject: ExtractSubjectType, field?: string) => 0 : (action: T[0], subject: ExtractSubjectType) => 0 : never, (action: Extract) => 0 >; export class RuleIndex { private _hasPerFieldRules = false; private _events?: Events; private _indexedRules: IndexTree = new Map(); private _rules: RawRuleFrom[]; private readonly _ruleOptions: RuleOptions; private _detectSubjectType: this['detectSubjectType']; private readonly _anyAction: string; private readonly _anySubjectType: string; private readonly _hasCustomSubjectTypeDetection: boolean; readonly [ɵabilities]!: A; readonly [ɵconditions]!: Conditions; constructor( rules: RawRuleFrom[] = [], options: RuleIndexOptions = {} ) { this._ruleOptions = { conditionsMatcher: options.conditionsMatcher, fieldMatcher: options.fieldMatcher, resolveAction: options.resolveAction || identity, }; this._anyAction = options.anyAction || 'manage'; this._anySubjectType = options.anySubjectType || 'all'; this._rules = rules; this._hasCustomSubjectTypeDetection = !!options.detectSubjectType; this._detectSubjectType = options.detectSubjectType || (detectSubjectType as this['detectSubjectType']); this._indexAndAnalyzeRules(rules); } get rules() { return this._rules; } detectSubjectType(object?: Normalize[1]): ExtractSubjectType[1]> { if (isSubjectType(object)) return object as ExtractSubjectType[1]>; if (!object) return this._anySubjectType as ExtractSubjectType[1]>; return this._detectSubjectType(object as Exclude[1], SubjectType>); } update(rules: RawRuleFrom[]): Public { const event = { rules, ability: this, target: this } as unknown as UpdateEvent; this._emit('update', event); this._hasPerFieldRules = false; this._rules = rules; this._indexAndAnalyzeRules(rules); this._emit('updated', event); return this; } private _indexAndAnalyzeRules(rawRules: RawRuleFrom[]) { const indexedRules: IndexTree = new Map(); let typeOfSubjectType: string | undefined; for (let i = rawRules.length - 1; i >= 0; i--) { const priority = rawRules.length - i - 1; const rule = new Rule(rawRules[i], this._ruleOptions, priority); const actions = wrapArray(rule.action); const subjects = wrapArray(rule.subject || this._anySubjectType); if (!this._hasPerFieldRules && rule.fields) this._hasPerFieldRules = true; for (let k = 0; k < subjects.length; k++) { const subjectRules = getOrDefault(indexedRules, subjects[k], defaultSubjectEntry); if (typeOfSubjectType === undefined) { typeOfSubjectType = typeof subjects[k]; } if (typeof subjects[k] !== typeOfSubjectType && typeOfSubjectType !== 'mixed') { typeOfSubjectType = 'mixed'; } for (let j = 0; j < actions.length; j++) { getOrDefault(subjectRules, actions[j], defaultActionEntry).rules.push(rule); } } } this._indexedRules = indexedRules; if (typeOfSubjectType !== 'mixed' && !this._hasCustomSubjectTypeDetection) { const detectSubjectType = DETECT_SUBJECT_TYPE_STRATEGY[typeOfSubjectType as 'function' | 'string'] || DETECT_SUBJECT_TYPE_STRATEGY.string; this._detectSubjectType = detectSubjectType as this['detectSubjectType']; } } possibleRulesFor(...args: AbilitySubjectTypeParameters): readonly Rule[]; possibleRulesFor( action: string, subjectType: SubjectType = this._anySubjectType ): readonly Rule[] { if (!isSubjectType(subjectType)) { throw new Error('"possibleRulesFor" accepts only subject types (i.e., string or class) as the 2nd parameter'); } const subjectRules = this._indexedRules.get(subjectType); const actionRules = subjectRules?.get(action); if (actionRules?.merged) { return actionRules.rules; } const anyActionRules = action !== this._anyAction && !!subjectRules?.has(this._anyAction) ? Object.freeze(subjectRules.get(this._anyAction)!.rules) : undefined; let rules = mergePrioritized(actionRules?.rules, anyActionRules); if (subjectType !== this._anySubjectType) { rules = mergePrioritized(rules, (this as any).possibleRulesFor(action, this._anySubjectType)); } if (actionRules) { actionRules.rules = Object.freeze(rules) as Rule[]; actionRules.merged = true; } return rules; } rulesFor(...args: AbilitySubjectTypeParameters): readonly Rule[]; rulesFor( action: string, subjectType?: SubjectType, field?: string ): readonly Rule[] { const rules: Rule[] = (this as any).possibleRulesFor(action, subjectType); if (field && typeof field !== 'string') { throw new Error('The 3rd, `field` parameter is expected to be a string. See https://casl.js.org/v6/en/api/casl-ability#can-of-ability for details'); } if (!this._hasPerFieldRules) { return rules; } return filterWithLazyAllocation(rules, rule => rule.matchesField(field)); } actionsFor(subjectType: ExtractSubjectType[1]>): string[] { if (!isSubjectType(subjectType)) { throw new Error('"actionsFor" accepts only subject types (i.e., string or class) as a parameter'); } const actions = new Set(); const subjectRules = this._indexedRules.get(subjectType); if (subjectRules) { Array.from(subjectRules.keys()).forEach(action => actions.add(action)); } const anySubjectTypeRules = subjectType !== this._anySubjectType ? this._indexedRules.get(this._anySubjectType) : undefined; if (anySubjectTypeRules) { Array.from(anySubjectTypeRules.keys()).forEach(action => actions.add(action)); } return Array.from(actions); } on>( event: T, handler: EventsMap>[T] ): Unsubscribe { this._events = this._events || new Map(); const events = this._events; const tail = events.get(event) || null; const item = linkedItem(handler, tail); events.set(event, item); return () => { const currentTail = events.get(event); if (!item.next && !item.prev && currentTail === item) { events.delete(event); } else if (item === currentTail) { events.set(event, item.prev); } unlinkItem(item); }; } private _emit>( name: T, payload: Parameters[T]>[0] ) { if (!this._events) return; let current = this._events.get(name) || null; const handlers: ((payload: Parameters[T]>[0]) => void)[] = []; // We collect handlers in an array first to avoid issues if a handler // unsubscribes itself (or others) during emission, which would mutate the linked list. // Using an array here is faster than cloning linked list nodes. while (current !== null) { handlers.push(current.value); current = current.prev; } for (let i = 0; i < handlers.length; i++) { handlers[i](payload); } } }