11import { CompoundCondition , Condition , buildAnd , buildOr } from '@ucast/mongo2js' ;
2- import { AnyAbility } from '../PureAbility' ;
3- import { Generics , RuleOf } from '../RuleIndex' ;
4- import { ExtractSubjectType } from '../types' ;
2+ import type { AnyAbility } from '../PureAbility' ;
3+ import type { RuleOf } from '../RuleIndex' ;
4+ import type { ExtractSubjectType } from '../types' ;
55
66export type RuleToQueryConverter < T extends AnyAbility , R = object > = ( rule : RuleOf < T > ) => R ;
77export interface AbilityQuery < T = object > {
@@ -15,39 +15,15 @@ export function rulesToQuery<T extends AnyAbility, R = object>(
1515 subjectType : ExtractSubjectType < Parameters < T [ 'rulesFor' ] > [ 1 ] > ,
1616 convert : RuleToQueryConverter < T , R >
1717) : AbilityQuery < R > | null {
18- const $and : Generics < T > [ 'conditions' ] [ ] = [ ] ;
19- const $or : Generics < T > [ 'conditions' ] [ ] = [ ] ;
20- const rules = ability . rulesFor ( action , subjectType ) ;
21-
22- for ( let i = 0 ; i < rules . length ; i ++ ) {
23- const rule = rules [ i ] ;
24- const list = rule . inverted ? $and : $or ;
25-
26- if ( ! rule . conditions ) {
27- if ( rule . inverted ) {
28- // stop if inverted rule without fields and conditions
29- // Example:
30- // can('read', 'Post', { id: 2 })
31- // cannot('read', "Post")
32- // can('read', 'Post', { id: 5 })
33- break ;
34- } else {
35- // if it allows reading all types then remove previous conditions
36- // Example:
37- // can('read', 'Post', { id: 1 })
38- // can('read', 'Post')
39- // cannot('read', 'Post', { status: 'draft' })
40- return $and . length ? { $and } : { } ;
41- }
42- } else {
43- list . push ( convert ( rule ) ) ;
18+ return mergeRules < T , R , AbilityQuery < R > > (
19+ ability . rulesFor ( action , subjectType ) ,
20+ convert ,
21+ {
22+ and : ( conditions ) => ( { $and : conditions } ) ,
23+ or : ( conditions ) => ( { $or : conditions } ) ,
24+ empty : ( ) => ( { } )
4425 }
45- }
46-
47- // if there are no regular conditions and the where no rule without condition
48- // then user is not allowed to perform this action on this subject type
49- if ( ! $or . length ) return null ;
50- return $and . length ? { $or, $and } : { $or } ;
26+ ) ;
5127}
5228
5329function ruleToAST ( rule : RuleOf < AnyAbility > ) : Condition {
@@ -63,19 +39,79 @@ export function rulesToAST<T extends AnyAbility>(
6339 action : Parameters < T [ 'rulesFor' ] > [ 0 ] ,
6440 subjectType : ExtractSubjectType < Parameters < T [ 'rulesFor' ] > [ 1 ] > ,
6541) : Condition | null {
66- const query = rulesToQuery ( ability , action , subjectType , ruleToAST ) as AbilityQuery < Condition > ;
42+ return mergeRules < T , Condition , Condition > (
43+ ability . rulesFor ( action , subjectType ) ,
44+ ruleToAST ,
45+ {
46+ and : buildAnd ,
47+ or : ( conditions ) => conditions . length === 1 ? conditions [ 0 ] : buildOr ( conditions ) ,
48+ empty : ( ) => buildAnd ( [ ] )
49+ }
50+ ) ;
51+ }
6752
68- if ( query === null ) {
69- return null ;
53+ /**
54+ * Converts CASL's sequential, switch-case priority enforcement into flat boolean logic.
55+ *
56+ * CASL evaluates rules from bottom to top (highest priority). When a record is evaluated:
57+ * - If it matches a `cannot` rule, it returns `false`.
58+ * - If it matches a `can` rule, it returns `true`.
59+ * - Thus, a `can` rule is only reached if it was not intercepted by any higher-priority `cannot` rule.
60+ *
61+ * This function flattens this logic for database queries by isolating each `can` rule ("OR" branches)
62+ * and strictly bounding it by all the preceding `cannot` conditions ("AND NOT" bounds).
63+ * Because standard `$or` logic inherently absorbs the overlap of previously matched `can` paths,
64+ * we don't mathematically need to subtract higher-priority `can` rules.
65+ *
66+ * @param rules - The sorted array of CASL rules (highest priority first).
67+ * @param convert - The transformer mapping a CASL rule to the target query/AST format.
68+ * @param hooks - The logical combination hooks for the target format.
69+ */
70+ function mergeRules < T extends AnyAbility , R , Result > (
71+ rules : RuleOf < T > [ ] ,
72+ convert : ( rule : RuleOf < T > ) => R ,
73+ hooks : {
74+ and : ( conditions : R [ ] ) => Result ,
75+ or : ( conditions : R [ ] ) => Result ,
76+ empty : ( ) => Result ,
7077 }
78+ ) : Result | null {
79+ const higherCannots : R [ ] = [ ] ;
80+ const orConditions : R [ ] = [ ] ;
81+ let hasUnconditionalCan = false ;
7182
72- if ( ! query . $and ) {
73- return query . $or ? buildOr ( query . $or ) : buildAnd ( [ ] ) ;
83+ for ( let i = 0 ; i < rules . length ; i ++ ) {
84+ const rule = rules [ i ] ;
85+
86+ if ( rule . inverted ) {
87+ if ( ! rule . conditions ) {
88+ break ; // stop evaluation on unconditional cannot
89+ }
90+ higherCannots . push ( convert ( rule ) ) ;
91+ } else {
92+ if ( ! rule . conditions ) {
93+ hasUnconditionalCan = true ;
94+ break ; // stop evaluation on unconditional can
95+ }
96+
97+ let cond = convert ( rule ) ;
98+ if ( higherCannots . length > 0 ) {
99+ cond = hooks . and ( [ cond , ...higherCannots ] ) as unknown as R ;
100+ }
101+ orConditions . push ( cond ) ;
102+ }
74103 }
75104
76- if ( query . $or ) {
77- query . $and . push ( buildOr ( query . $or ) ) ;
105+ if ( hasUnconditionalCan ) {
106+ if ( higherCannots . length === 0 ) {
107+ return hooks . empty ( ) ;
108+ }
109+ if ( orConditions . length === 0 ) {
110+ return hooks . and ( higherCannots ) ;
111+ }
112+ orConditions . push ( hooks . and ( higherCannots ) as unknown as R ) ;
78113 }
79114
80- return buildAnd ( query . $and ) ;
115+ if ( orConditions . length === 0 ) return null ;
116+ return hooks . or ( orConditions ) ;
81117}
0 commit comments