Skip to content

Commit

Permalink
feat: adds possibility to auto detect subject type based on passed ru…
Browse files Browse the repository at this point in the history
…les (#882)
  • Loading branch information
stalniy authored Feb 23, 2024
1 parent 71bb1fb commit 4737fe2
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 16 deletions.
2 changes: 0 additions & 2 deletions packages/casl-ability/spec/ability.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ describe('Ability', () => {
ability = defineAbility((can) => {
can('read', Post)
can('update', Post, { authorId: 1 })
}, {
detectSubjectType: object => object.constructor
})

expect(ability).to.allow('read', Post)
Expand Down
29 changes: 21 additions & 8 deletions packages/casl-ability/src/RuleIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
AbilityTuple,
ExtractSubjectType
} from './types';
import { wrapArray, detectSubjectType, mergePrioritized, getOrDefault, identity, isSubjectType } from './utils';
import { wrapArray, detectSubjectType, mergePrioritized, getOrDefault, identity, isSubjectType, DETECT_SUBJECT_TYPE_STRATEGY } from './utils';
import { LinkedItem, linkedItem, unlinkItem, cloneLinkedItem } from './structures/LinkedItem';

export interface RuleIndexOptions<A extends Abilities, C> extends Partial<RuleOptions<C>> {
Expand Down Expand Up @@ -91,12 +91,13 @@ type AbilitySubjectTypeParameters<T extends Abilities, IncludeField extends bool
export class RuleIndex<A extends Abilities, Conditions> {
private _hasPerFieldRules: boolean = false;
private _events?: Events<this>;
private _indexedRules: IndexTree<A, Conditions>;
private _indexedRules: IndexTree<A, Conditions> = new Map();
private _rules: RawRuleFrom<A, Conditions>[];
private readonly _ruleOptions: RuleOptions<Conditions>;
private readonly _detectSubjectType: this['detectSubjectType'];
private _detectSubjectType: this['detectSubjectType'];
private readonly _anyAction: string;
private readonly _anySubjectType: string;
private readonly _hasCustomSubjectTypeDetection: boolean;
readonly [ɵabilities]!: A;
readonly [ɵconditions]!: Conditions;

Expand All @@ -111,9 +112,10 @@ export class RuleIndex<A extends Abilities, Conditions> {
};
this._anyAction = options.anyAction || 'manage';
this._anySubjectType = options.anySubjectType || 'all';
this._detectSubjectType = options.detectSubjectType || (detectSubjectType as this['detectSubjectType']);
this._rules = rules;
this._indexedRules = this._buildIndexFor(rules);
this._hasCustomSubjectTypeDetection = !!options.detectSubjectType;
this._detectSubjectType = options.detectSubjectType || (detectSubjectType as this['detectSubjectType']);
this._indexAndAnalyzeRules(rules);
}

get rules() {
Expand All @@ -135,14 +137,15 @@ export class RuleIndex<A extends Abilities, Conditions> {

this._emit('update', event);
this._rules = rules;
this._indexedRules = this._buildIndexFor(rules);
this._indexAndAnalyzeRules(rules);
this._emit('updated', event);

return this;
}

private _buildIndexFor(rawRules: RawRuleFrom<A, Conditions>[]) {
private _indexAndAnalyzeRules(rawRules: RawRuleFrom<A, Conditions>[]) {
const indexedRules: IndexTree<A, Conditions> = new Map();
let typeOfSubjectType: string | undefined;

for (let i = rawRules.length - 1; i >= 0; i--) {
const priority = rawRules.length - i - 1;
Expand All @@ -153,14 +156,24 @@ export class RuleIndex<A extends Abilities, Conditions> {

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

return indexedRules;
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<A, false>): Rule<A, Conditions>[];
Expand Down
17 changes: 11 additions & 6 deletions packages/casl-ability/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,23 @@ export const isSubjectType = (value: unknown): value is SubjectType => {
};

const getSubjectClassName = (value: SubjectClass) => value.modelName || value.name;
export const getSubjectTypeName = (value: SubjectType) => {
export function getSubjectTypeName(value: SubjectType) {
return typeof value === 'string' ? value : getSubjectClassName(value);
};
}

export function detectSubjectType(subject: Exclude<Subject, SubjectType>): string {
if (Object.hasOwn(subject, TYPE_FIELD)) {
return subject[TYPE_FIELD];
export function detectSubjectType(object: Exclude<Subject, SubjectType>): string {
if (Object.hasOwn(object, TYPE_FIELD)) {
return object[TYPE_FIELD];
}

return getSubjectClassName(subject.constructor as SubjectClass);
return getSubjectClassName(object.constructor as SubjectClass);
}

export const DETECT_SUBJECT_TYPE_STRATEGY = {
function: (object: Exclude<Subject, SubjectType>) => object.constructor as SubjectClass,
string: detectSubjectType
};

type AliasMerge = (actions: string[], action: string | string[]) => string[];
function expandActions(aliasMap: AliasesMap, rawActions: string | string[], merge: AliasMerge) {
let actions = wrapArray(rawActions);
Expand Down

0 comments on commit 4737fe2

Please sign in to comment.