@@ -19,15 +19,17 @@ import {
19
19
StringLiteral ,
20
20
UnaryExpr ,
21
21
} from '@zenstackhq/language/ast' ;
22
- import { DELEGATE_AUX_RELATION_PREFIX } from '@zenstackhq/runtime' ;
22
+ import { DELEGATE_AUX_RELATION_PREFIX , PolicyOperationKind } from '@zenstackhq/runtime' ;
23
23
import {
24
24
ExpressionContext ,
25
25
getFunctionExpressionContext ,
26
26
getIdFields ,
27
27
getLiteral ,
28
+ getQueryGuardFunctionName ,
28
29
isAuthInvocation ,
29
30
isDataModelFieldReference ,
30
31
isDelegateModel ,
32
+ isFromStdlib ,
31
33
isFutureExpr ,
32
34
PluginError ,
33
35
TypeScriptExpressionTransformer ,
@@ -37,6 +39,7 @@ import { lowerCaseFirst } from 'lower-case-first';
37
39
import invariant from 'tiny-invariant' ;
38
40
import { CodeBlockWriter } from 'ts-morph' ;
39
41
import { name } from '..' ;
42
+ import { isCheckInvocation } from '../../../utils/ast-utils' ;
40
43
41
44
type ComparisonOperator = '==' | '!=' | '>' | '>=' | '<' | '<=' ;
42
45
@@ -60,6 +63,11 @@ type FilterOperators =
60
63
export const TRUE = '{ AND: [] }' ;
61
64
export const FALSE = '{ OR: [] }' ;
62
65
66
+ export type ExpressionWriterOptions = {
67
+ isPostGuard ?: boolean ;
68
+ operationContext : PolicyOperationKind ;
69
+ } ;
70
+
63
71
/**
64
72
* Utility for writing ZModel expression as Prisma query argument objects into a ts-morph writer
65
73
*/
@@ -68,15 +76,14 @@ export class ExpressionWriter {
68
76
69
77
/**
70
78
* Constructs a new ExpressionWriter
71
- *
72
- * @param isPostGuard indicates if we're writing for post-update conditions
73
79
*/
74
- constructor ( private readonly writer : CodeBlockWriter , private readonly isPostGuard = false ) {
80
+ constructor ( private readonly writer : CodeBlockWriter , private readonly options : ExpressionWriterOptions ) {
75
81
this . plainExprBuilder = new TypeScriptExpressionTransformer ( {
76
82
context : ExpressionContext . AccessPolicy ,
77
- isPostGuard : this . isPostGuard ,
83
+ isPostGuard : this . options . isPostGuard ,
78
84
// in post-guard context, `this` references pre-update value
79
- thisExprContext : this . isPostGuard ? 'context.preValue' : undefined ,
85
+ thisExprContext : this . options . isPostGuard ? 'context.preValue' : undefined ,
86
+ operationContext : this . options . operationContext ,
80
87
} ) ;
81
88
}
82
89
@@ -269,17 +276,20 @@ export class ExpressionWriter {
269
276
// expression rooted to `auth()` is always compiled to plain expression
270
277
! this . isAuthOrAuthMemberAccess ( expr . left ) &&
271
278
// `future()` in post-update context
272
- ( ( this . isPostGuard && this . isFutureMemberAccess ( expr . left ) ) ||
279
+ ( ( this . options . isPostGuard && this . isFutureMemberAccess ( expr . left ) ) ||
273
280
// non-`future()` in pre-update context
274
- ( ! this . isPostGuard && ! this . isFutureMemberAccess ( expr . left ) ) ) ;
281
+ ( ! this . options . isPostGuard && ! this . isFutureMemberAccess ( expr . left ) ) ) ;
275
282
276
283
if ( compileToRelationQuery ) {
277
284
this . block ( ( ) => {
278
285
this . writeFieldCondition (
279
286
expr . left ,
280
287
( ) => {
281
288
// inner scope of collection expression is always compiled as non-post-guard
282
- const innerWriter = new ExpressionWriter ( this . writer , false ) ;
289
+ const innerWriter = new ExpressionWriter ( this . writer , {
290
+ isPostGuard : false ,
291
+ operationContext : this . options . operationContext ,
292
+ } ) ;
283
293
innerWriter . write ( expr . right ) ;
284
294
} ,
285
295
operator === '?' ? 'some' : operator === '!' ? 'every' : 'none'
@@ -297,14 +307,14 @@ export class ExpressionWriter {
297
307
}
298
308
299
309
if ( isMemberAccessExpr ( expr ) ) {
300
- if ( isFutureExpr ( expr . operand ) && this . isPostGuard ) {
310
+ if ( isFutureExpr ( expr . operand ) && this . options . isPostGuard ) {
301
311
// when writing for post-update, future().field.x is a field access
302
312
return true ;
303
313
} else {
304
314
return this . isFieldAccess ( expr . operand ) ;
305
315
}
306
316
}
307
- if ( isDataModelFieldReference ( expr ) && ! this . isPostGuard ) {
317
+ if ( isDataModelFieldReference ( expr ) && ! this . options . isPostGuard ) {
308
318
return true ;
309
319
}
310
320
return false ;
@@ -437,7 +447,7 @@ export class ExpressionWriter {
437
447
this . writer . write ( operator === '!=' ? TRUE : FALSE ) ;
438
448
} else {
439
449
this . writeOperator ( operator , fieldAccess , ( ) => {
440
- if ( isDataModelFieldReference ( operand ) && ! this . isPostGuard ) {
450
+ if ( isDataModelFieldReference ( operand ) && ! this . options . isPostGuard ) {
441
451
// if operand is a field reference and we're not generating for post-update guard,
442
452
// we should generate a field reference (comparing fields in the same model)
443
453
this . writeFieldReference ( operand ) ;
@@ -735,6 +745,11 @@ export class ExpressionWriter {
735
745
functionAllowedContext . includes ( ExpressionContext . AccessPolicy ) ||
736
746
functionAllowedContext . includes ( ExpressionContext . ValidationRule )
737
747
) {
748
+ if ( isCheckInvocation ( expr ) ) {
749
+ this . writeRelationCheck ( expr ) ;
750
+ return ;
751
+ }
752
+
738
753
if ( ! expr . args . some ( ( arg ) => this . isFieldAccess ( arg . value ) ) ) {
739
754
// filter functions without referencing fields
740
755
this . guard ( ( ) => this . plain ( expr ) ) ;
@@ -744,13 +759,13 @@ export class ExpressionWriter {
744
759
let valueArg = expr . args [ 1 ] ?. value ;
745
760
746
761
// isEmpty function is zero arity, it's mapped to a boolean literal
747
- if ( funcDecl . name === 'isEmpty' ) {
762
+ if ( isFromStdlib ( funcDecl ) && funcDecl . name === 'isEmpty' ) {
748
763
valueArg = { $type : BooleanLiteral , value : true } as LiteralExpr ;
749
764
}
750
765
751
766
// contains function has a 3rd argument that indicates whether the comparison should be case-insensitive
752
767
let extraArgs : Record < string , Expression > | undefined = undefined ;
753
- if ( funcDecl . name === 'contains' ) {
768
+ if ( isFromStdlib ( funcDecl ) && funcDecl . name === 'contains' ) {
754
769
if ( getLiteral < boolean > ( expr . args [ 2 ] ?. value ) === true ) {
755
770
extraArgs = { mode : { $type : StringLiteral , value : 'insensitive' } as LiteralExpr } ;
756
771
}
@@ -770,4 +785,38 @@ export class ExpressionWriter {
770
785
throw new PluginError ( name , `Unsupported function ${ funcDecl . name } ` ) ;
771
786
}
772
787
}
788
+
789
+ private writeRelationCheck ( expr : InvocationExpr ) {
790
+ if ( ! isDataModelFieldReference ( expr . args [ 0 ] . value ) ) {
791
+ throw new PluginError ( name , `First argument of check() must be a field` ) ;
792
+ }
793
+ if ( ! isDataModel ( expr . args [ 0 ] . value . $resolvedType ?. decl ) ) {
794
+ throw new PluginError ( name , `First argument of check() must be a relation field` ) ;
795
+ }
796
+
797
+ const fieldRef = expr . args [ 0 ] . value ;
798
+ const targetModel = fieldRef . $resolvedType ?. decl as DataModel ;
799
+
800
+ let operation : string ;
801
+ if ( expr . args [ 1 ] ) {
802
+ const literal = getLiteral < string > ( expr . args [ 1 ] . value ) ;
803
+ if ( ! literal ) {
804
+ throw new TypeScriptExpressionTransformerError ( `Second argument of check() must be a string literal` ) ;
805
+ }
806
+ if ( ! [ 'read' , 'create' , 'update' , 'delete' ] . includes ( literal ) ) {
807
+ throw new TypeScriptExpressionTransformerError ( `Invalid check() operation "${ literal } "` ) ;
808
+ }
809
+ operation = literal ;
810
+ } else {
811
+ if ( ! this . options . operationContext ) {
812
+ throw new TypeScriptExpressionTransformerError ( 'Unable to determine CRUD operation from context' ) ;
813
+ }
814
+ operation = this . options . operationContext ;
815
+ }
816
+
817
+ this . block ( ( ) => {
818
+ const targetGuardFunc = getQueryGuardFunctionName ( targetModel , undefined , false , operation ) ;
819
+ this . writer . write ( `${ fieldRef . target . $refText } : ${ targetGuardFunc } (context, db)` ) ;
820
+ } ) ;
821
+ }
773
822
}
0 commit comments