@@ -30,6 +30,7 @@ import { Logger } from '../logger';
3030import { QueryUtils } from '../query-utils' ;
3131import type { EntityChecker , ModelPolicyDef , PermissionCheckerFunc , PolicyDef , PolicyFunc , ZodSchemas } from '../types' ;
3232import { formatObject , prismaClientKnownRequestError } from '../utils' ;
33+ import { isPlainObject } from 'is-plain-object' ;
3334
3435/**
3536 * Access policy enforcement utilities
@@ -107,23 +108,63 @@ export class PolicyUtil extends QueryUtils {
107108 // Static True/False conditions
108109 // https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
109110
110- public isTrue ( condition : object ) {
111- if ( condition === null || condition === undefined ) {
111+ private singleKey ( obj : object | null | undefined , key : string ) : obj is { [ key : string ] : unknown } {
112+ if ( ! obj ) {
112113 return false ;
113114 } else {
114- return (
115- ( typeof condition === 'object' && Object . keys ( condition ) . length === 0 ) ||
116- ( 'AND' in condition && Array . isArray ( condition . AND ) && condition . AND . length === 0 )
117- ) ;
115+ return Object . keys ( obj ) . length === 1 && Object . keys ( obj ) [ 0 ] === key ;
118116 }
119117 }
120118
121- public isFalse ( condition : object ) {
122- if ( condition === null || condition === undefined ) {
119+ public isTrue ( condition : object | null | undefined ) {
120+ if ( condition === null || condition === undefined || ! isPlainObject ( condition ) ) {
123121 return false ;
124- } else {
125- return 'OR' in condition && Array . isArray ( condition . OR ) && condition . OR . length === 0 ;
126122 }
123+
124+ // {} is true
125+ if ( Object . keys ( condition ) . length === 0 ) {
126+ return true ;
127+ }
128+
129+ // { OR: TRUE } is true
130+ if ( this . singleKey ( condition , 'OR' ) && typeof condition . OR === 'object' && this . isTrue ( condition . OR ) ) {
131+ return true ;
132+ }
133+
134+ // { NOT: FALSE } is true
135+ if ( this . singleKey ( condition , 'NOT' ) && typeof condition . NOT === 'object' && this . isFalse ( condition . NOT ) ) {
136+ return true ;
137+ }
138+
139+ // { AND: [] } is true
140+ if ( this . singleKey ( condition , 'AND' ) && Array . isArray ( condition . AND ) && condition . AND . length === 0 ) {
141+ return true ;
142+ }
143+
144+ return false ;
145+ }
146+
147+ public isFalse ( condition : object | null | undefined ) {
148+ if ( condition === null || condition === undefined || ! isPlainObject ( condition ) ) {
149+ return false ;
150+ }
151+
152+ // { AND: FALSE } is false
153+ if ( this . singleKey ( condition , 'AND' ) && typeof condition . AND === 'object' && this . isFalse ( condition . AND ) ) {
154+ return true ;
155+ }
156+
157+ // { NOT: TRUE } is false
158+ if ( this . singleKey ( condition , 'NOT' ) && typeof condition . NOT === 'object' && this . isTrue ( condition . NOT ) ) {
159+ return true ;
160+ }
161+
162+ // { OR: [] } is false
163+ if ( this . singleKey ( condition , 'OR' ) && Array . isArray ( condition . OR ) && condition . OR . length === 0 ) {
164+ return true ;
165+ }
166+
167+ return false ;
127168 }
128169
129170 private makeTrue ( ) {
@@ -149,11 +190,6 @@ export class PolicyUtil extends QueryUtils {
149190
150191 const result : any = { } ;
151192 for ( const [ key , value ] of Object . entries < any > ( condition ) ) {
152- if ( this . isFalse ( result ) ) {
153- // already false, no need to continue
154- break ;
155- }
156-
157193 if ( value === null || value === undefined ) {
158194 result [ key ] = value ;
159195 continue ;
@@ -165,14 +201,13 @@ export class PolicyUtil extends QueryUtils {
165201 . map ( ( c : any ) => this . reduce ( c ) )
166202 . filter ( ( c ) => c !== undefined && ! this . isTrue ( c ) ) ;
167203 if ( children . length === 0 ) {
168- result [ key ] = [ ] ; // true
204+ // { ..., AND: [] }
205+ result [ key ] = [ ] ;
169206 } else if ( children . some ( ( c ) => this . isFalse ( c ) ) ) {
170- result [ 'OR' ] = [ ] ; // false
207+ // { ..., AND: { OR: [] } }
208+ result [ key ] = this . makeFalse ( ) ;
171209 } else {
172- if ( ! this . isTrue ( { AND : result [ key ] } ) ) {
173- // use AND only if it's not already true
174- result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
175- }
210+ result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
176211 }
177212 break ;
178213 }
@@ -182,54 +217,43 @@ export class PolicyUtil extends QueryUtils {
182217 . map ( ( c : any ) => this . reduce ( c ) )
183218 . filter ( ( c ) => c !== undefined && ! this . isFalse ( c ) ) ;
184219 if ( children . length === 0 ) {
185- result [ key ] = [ ] ; // false
220+ // { ..., OR: [] }
221+ result [ key ] = [ ] ;
186222 } else if ( children . some ( ( c ) => this . isTrue ( c ) ) ) {
187- result [ 'AND' ] = [ ] ; // true
223+ // { ..., OR: { AND: [] } }
224+ result [ key ] = this . makeTrue ( ) ;
188225 } else {
189- if ( ! this . isFalse ( { OR : result [ key ] } ) ) {
190- // use OR only if it's not already false
191- result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
192- }
226+ result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
193227 }
194228 break ;
195229 }
196230
197231 case 'NOT' : {
198- const children = enumerate ( value )
199- . map ( ( c : any ) => this . reduce ( c ) )
200- . filter ( ( c ) => c !== undefined && ! this . isFalse ( c ) ) ;
201- if ( children . length === 0 ) {
202- // all clauses are false, result is a constant true,
203- // thus eliminated (not adding into result)
204- } else if ( children . some ( ( c ) => this . isTrue ( c ) ) ) {
205- // some clauses are true, result is a constant false,
206- // eliminate all other keys and set entire condition to false
207- Object . keys ( result ) . forEach ( ( k ) => delete result [ k ] ) ;
208- result [ 'OR' ] = [ ] ; // this will cause the outer loop to exit too
209- } else {
210- result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
211- }
232+ const children = enumerate ( value ) . map ( ( c : any ) => this . reduce ( c ) ) ;
233+ result [ key ] = ! Array . isArray ( value ) && children . length === 1 ? children [ 0 ] : children ;
212234 break ;
213235 }
214236
215237 default : {
216- const booleanKeys = [ 'AND' , 'OR' , 'NOT' , 'is' , 'isNot' , 'none' , 'every' , 'some' ] ;
217- if (
218- typeof value === 'object' &&
219- value &&
220- // recurse only if the value has at least one boolean key
221- Object . keys ( value ) . some ( ( k ) => booleanKeys . includes ( k ) )
222- ) {
223- result [ key ] = this . reduce ( value ) ;
224- } else {
238+ if ( ! isPlainObject ( value ) ) {
239+ // don't visit into non-plain object values - could be Date, array, etc.
225240 result [ key ] = value ;
241+ } else {
242+ result [ key ] = this . reduce ( value ) ;
226243 }
227244 break ;
228245 }
229246 }
230247 }
231248
232- return result ;
249+ // finally normalize constant true/false conditions
250+ if ( this . isTrue ( result ) ) {
251+ return this . makeTrue ( ) ;
252+ } else if ( this . isFalse ( result ) ) {
253+ return this . makeFalse ( ) ;
254+ } else {
255+ return result ;
256+ }
233257 }
234258
235259 //#endregion
0 commit comments