@@ -30,6 +30,7 @@ import { Logger } from '../logger';
30
30
import { QueryUtils } from '../query-utils' ;
31
31
import type { EntityChecker , ModelPolicyDef , PermissionCheckerFunc , PolicyDef , PolicyFunc , ZodSchemas } from '../types' ;
32
32
import { formatObject , prismaClientKnownRequestError } from '../utils' ;
33
+ import { isPlainObject } from 'is-plain-object' ;
33
34
34
35
/**
35
36
* Access policy enforcement utilities
@@ -107,23 +108,63 @@ export class PolicyUtil extends QueryUtils {
107
108
// Static True/False conditions
108
109
// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
109
110
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 ) {
112
113
return false ;
113
114
} 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 ;
118
116
}
119
117
}
120
118
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 ) ) {
123
121
return false ;
124
- } else {
125
- return 'OR' in condition && Array . isArray ( condition . OR ) && condition . OR . length === 0 ;
126
122
}
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 ;
127
168
}
128
169
129
170
private makeTrue ( ) {
@@ -149,11 +190,6 @@ export class PolicyUtil extends QueryUtils {
149
190
150
191
const result : any = { } ;
151
192
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
-
157
193
if ( value === null || value === undefined ) {
158
194
result [ key ] = value ;
159
195
continue ;
@@ -165,14 +201,13 @@ export class PolicyUtil extends QueryUtils {
165
201
. map ( ( c : any ) => this . reduce ( c ) )
166
202
. filter ( ( c ) => c !== undefined && ! this . isTrue ( c ) ) ;
167
203
if ( children . length === 0 ) {
168
- result [ key ] = [ ] ; // true
204
+ // { ..., AND: [] }
205
+ result [ key ] = [ ] ;
169
206
} else if ( children . some ( ( c ) => this . isFalse ( c ) ) ) {
170
- result [ 'OR' ] = [ ] ; // false
207
+ // { ..., AND: { OR: [] } }
208
+ result [ key ] = this . makeFalse ( ) ;
171
209
} 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 ;
176
211
}
177
212
break ;
178
213
}
@@ -182,54 +217,43 @@ export class PolicyUtil extends QueryUtils {
182
217
. map ( ( c : any ) => this . reduce ( c ) )
183
218
. filter ( ( c ) => c !== undefined && ! this . isFalse ( c ) ) ;
184
219
if ( children . length === 0 ) {
185
- result [ key ] = [ ] ; // false
220
+ // { ..., OR: [] }
221
+ result [ key ] = [ ] ;
186
222
} else if ( children . some ( ( c ) => this . isTrue ( c ) ) ) {
187
- result [ 'AND' ] = [ ] ; // true
223
+ // { ..., OR: { AND: [] } }
224
+ result [ key ] = this . makeTrue ( ) ;
188
225
} 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 ;
193
227
}
194
228
break ;
195
229
}
196
230
197
231
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 ;
212
234
break ;
213
235
}
214
236
215
237
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.
225
240
result [ key ] = value ;
241
+ } else {
242
+ result [ key ] = this . reduce ( value ) ;
226
243
}
227
244
break ;
228
245
}
229
246
}
230
247
}
231
248
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
+ }
233
257
}
234
258
235
259
//#endregion
0 commit comments