@@ -23,7 +23,7 @@ import {
23
23
PolicyOperationKind ,
24
24
PrismaWriteActionType ,
25
25
} from '../../types' ;
26
- import { getVersion } from '../../version' ;
26
+ import { getPrismaVersion , getVersion } from '../../version' ;
27
27
import { getFields , resolveField } from '../model-meta' ;
28
28
import { NestedWriteVisitor , type NestedWriteVisitorContext } from '../nested-write-vistor' ;
29
29
import type { ModelMeta , PolicyDef , PolicyFunc , ZodSchemas } from '../types' ;
@@ -36,6 +36,7 @@ import {
36
36
prismaClientUnknownRequestError ,
37
37
} from '../utils' ;
38
38
import { Logger } from './logger' ;
39
+ import semver from 'semver' ;
39
40
40
41
/**
41
42
* Access policy enforcement utilities
@@ -45,6 +46,8 @@ export class PolicyUtil {
45
46
// @ts -ignore
46
47
private readonly logger : Logger ;
47
48
49
+ private supportNestedToOneFilter = false ;
50
+
48
51
constructor (
49
52
private readonly db : DbClientContract ,
50
53
private readonly modelMeta : ModelMeta ,
@@ -54,6 +57,10 @@ export class PolicyUtil {
54
57
private readonly logPrismaQuery ?: boolean
55
58
) {
56
59
this . logger = new Logger ( db ) ;
60
+
61
+ // use Prisma version to detect if we can filter when nested-fetching to-one relation
62
+ const prismaVersion = getPrismaVersion ( ) ;
63
+ this . supportNestedToOneFilter = prismaVersion ? semver . gte ( prismaVersion , '4.8.0' ) : false ;
57
64
}
58
65
59
66
/**
@@ -334,20 +341,29 @@ export class PolicyUtil {
334
341
}
335
342
336
343
const idFields = this . getIdFields ( model ) ;
344
+
337
345
for ( const field of getModelFields ( injectTarget ) ) {
338
346
const fieldInfo = resolveField ( this . modelMeta , model , field ) ;
339
347
if ( ! fieldInfo || ! fieldInfo . isDataModel ) {
340
348
// only care about relation fields
341
349
continue ;
342
350
}
343
351
344
- if ( fieldInfo . isArray ) {
352
+ if (
353
+ fieldInfo . isArray ||
354
+ // if Prisma version is high enough to support filtering directly when
355
+ // fetching a nullable to-one relation, let's do it that way
356
+ // https://github.com/prisma/prisma/discussions/20350
357
+ ( this . supportNestedToOneFilter && fieldInfo . isOptional )
358
+ ) {
345
359
if ( typeof injectTarget [ field ] !== 'object' ) {
346
360
injectTarget [ field ] = { } ;
347
361
}
348
- // inject extra condition for to-many relation
349
-
362
+ // inject extra condition for to-many or nullable to-one relation
350
363
await this . injectAuthGuard ( injectTarget [ field ] , fieldInfo . type , 'read' ) ;
364
+
365
+ // recurse
366
+ await this . injectNestedReadConditions ( fieldInfo . type , injectTarget [ field ] ) ;
351
367
} else {
352
368
// there's no way of injecting condition for to-one relation, so if there's
353
369
// "select" clause we make sure 'id' fields are selected and check them against
@@ -361,9 +377,6 @@ export class PolicyUtil {
361
377
}
362
378
}
363
379
}
364
-
365
- // recurse
366
- await this . injectNestedReadConditions ( fieldInfo . type , injectTarget [ field ] ) ;
367
380
}
368
381
}
369
382
@@ -373,69 +386,79 @@ export class PolicyUtil {
373
386
* omitted.
374
387
*/
375
388
async postProcessForRead ( data : any , model : string , args : any , operation : PolicyOperationKind ) {
376
- for ( const entityData of enumerate ( data ) ) {
377
- if ( typeof entityData !== 'object' || ! entityData ) {
378
- continue ;
379
- }
389
+ await Promise . all (
390
+ enumerate ( data ) . map ( ( entityData ) => this . postProcessSingleEntityForRead ( entityData , model , args , operation ) )
391
+ ) ;
392
+ }
380
393
381
- // strip auxiliary fields
382
- for ( const auxField of AUXILIARY_FIELDS ) {
383
- if ( auxField in entityData ) {
384
- delete entityData [ auxField ] ;
385
- }
394
+ private async postProcessSingleEntityForRead ( data : any , model : string , args : any , operation : PolicyOperationKind ) {
395
+ if ( typeof data !== 'object' || ! data ) {
396
+ return ;
397
+ }
398
+
399
+ // strip auxiliary fields
400
+ for ( const auxField of AUXILIARY_FIELDS ) {
401
+ if ( auxField in data ) {
402
+ delete data [ auxField ] ;
386
403
}
404
+ }
405
+
406
+ const injectTarget = args . select ?? args . include ;
407
+ if ( ! injectTarget ) {
408
+ return ;
409
+ }
387
410
388
- const injectTarget = args . select ?? args . include ;
389
- if ( ! injectTarget ) {
411
+ // recurse into nested entities
412
+ for ( const field of Object . keys ( injectTarget ) ) {
413
+ const fieldData = data [ field ] ;
414
+ if ( typeof fieldData !== 'object' || ! fieldData ) {
390
415
continue ;
391
416
}
392
417
393
- // recurse into nested entities
394
- for ( const field of Object . keys ( injectTarget ) ) {
395
- const fieldData = entityData [ field ] ;
396
- if ( typeof fieldData !== 'object' || ! fieldData ) {
397
- continue ;
398
- }
418
+ const fieldInfo = resolveField ( this . modelMeta , model , field ) ;
419
+ if ( fieldInfo ) {
420
+ if (
421
+ fieldInfo . isDataModel &&
422
+ ! fieldInfo . isArray &&
423
+ // if Prisma version supports filtering nullable to-one relation, no need to further check
424
+ ! ( this . supportNestedToOneFilter && fieldInfo . isOptional )
425
+ ) {
426
+ // to-one relation data cannot be trimmed by injected guards, we have to
427
+ // post-check them
428
+ const ids = this . getEntityIds ( fieldInfo . type , fieldData ) ;
429
+
430
+ if ( Object . keys ( ids ) . length !== 0 ) {
431
+ if ( this . logger . enabled ( 'info' ) ) {
432
+ this . logger . info (
433
+ `Validating read of to-one relation: ${ fieldInfo . type } #${ formatObject ( ids ) } `
434
+ ) ;
435
+ }
399
436
400
- const fieldInfo = resolveField ( this . modelMeta , model , field ) ;
401
- if ( fieldInfo ) {
402
- if ( fieldInfo . isDataModel && ! fieldInfo . isArray ) {
403
- // to-one relation data cannot be trimmed by injected guards, we have to
404
- // post-check them
405
- const ids = this . getEntityIds ( fieldInfo . type , fieldData ) ;
406
-
407
- if ( Object . keys ( ids ) . length !== 0 ) {
408
- // if (this.logger.enabled('info')) {
409
- // this.logger.info(
410
- // `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`
411
- // );
412
- // }
413
- try {
414
- await this . checkPolicyForFilter ( fieldInfo . type , ids , operation , this . db ) ;
415
- } catch ( err ) {
416
- if (
417
- isPrismaClientKnownRequestError ( err ) &&
418
- err . code === PrismaErrorCode . CONSTRAINED_FAILED
419
- ) {
420
- // denied by policy
421
- if ( fieldInfo . isOptional ) {
422
- // if the relation is optional, just nullify it
423
- entityData [ field ] = null ;
424
- } else {
425
- // otherwise reject
426
- throw err ;
427
- }
437
+ try {
438
+ await this . checkPolicyForFilter ( fieldInfo . type , ids , operation , this . db ) ;
439
+ } catch ( err ) {
440
+ if (
441
+ isPrismaClientKnownRequestError ( err ) &&
442
+ err . code === PrismaErrorCode . CONSTRAINED_FAILED
443
+ ) {
444
+ // denied by policy
445
+ if ( fieldInfo . isOptional ) {
446
+ // if the relation is optional, just nullify it
447
+ data [ field ] = null ;
428
448
} else {
429
- // unknown error
449
+ // otherwise reject
430
450
throw err ;
431
451
}
452
+ } else {
453
+ // unknown error
454
+ throw err ;
432
455
}
433
456
}
434
457
}
435
-
436
- // recurse
437
- await this . postProcessForRead ( fieldData , fieldInfo . type , injectTarget [ field ] , operation ) ;
438
458
}
459
+
460
+ // recurse
461
+ await this . postProcessForRead ( fieldData , fieldInfo . type , injectTarget [ field ] , operation ) ;
439
462
}
440
463
}
441
464
}
0 commit comments