11import { AstUtils , type ValidationAcceptor } from 'langium' ;
22import pluralize from 'pluralize' ;
3+ import type { BinaryExpr , DataModel , Expression } from '../ast' ;
34import {
45 ArrayExpr ,
56 Attribute ,
67 AttributeArg ,
78 AttributeParam ,
8- DataModelAttribute ,
99 DataField ,
1010 DataFieldAttribute ,
11+ DataModelAttribute ,
1112 InternalAttribute ,
1213 ReferenceExpr ,
1314 isArrayExpr ,
1415 isAttribute ,
15- isDataModel ,
1616 isDataField ,
17+ isDataModel ,
1718 isEnum ,
1819 isReferenceExpr ,
1920 isTypeDef ,
2021} from '../generated/ast' ;
2122import {
2223 getAllAttributes ,
2324 getStringLiteral ,
24- hasAttribute ,
25+ isAuthOrAuthMemberAccess ,
26+ isCollectionPredicate ,
2527 isDataFieldReference ,
2628 isDelegateModel ,
2729 isFutureExpr ,
@@ -31,7 +33,6 @@ import {
3133 typeAssignable ,
3234} from '../utils' ;
3335import type { AstValidator } from './common' ;
34- import type { DataModel } from '../ast' ;
3536
3637// a registry of function handlers marked with @check
3738const attributeCheckers = new Map < string , PropertyDescriptor > ( ) ;
@@ -153,6 +154,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
153154 }
154155 }
155156
157+ // TODO: design a way to let plugin register validation
156158 @check ( '@@allow' )
157159 @check ( '@@deny' )
158160 // @ts -expect-error
@@ -166,10 +168,75 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
166168 }
167169 this . validatePolicyKinds ( kind , [ 'create' , 'read' , 'update' , 'delete' , 'all' ] , attr , accept ) ;
168170
169- // @encrypted fields cannot be used in policy rules
170- this . rejectEncryptedFields ( attr , accept ) ;
171+ if ( ( kind === 'create' || kind === 'all' ) && attr . args [ 1 ] ?. value ) {
172+ // "create" rules cannot access non-owned relations because the entity does not exist yet, so
173+ // there can't possibly be a fk that points to it
174+ this . rejectNonOwnedRelationInExpression ( attr . args [ 1 ] . value , accept ) ;
175+ }
171176 }
172177
178+ private rejectNonOwnedRelationInExpression ( expr : Expression , accept : ValidationAcceptor ) {
179+ const contextModel = AstUtils . getContainerOfType ( expr , isDataModel ) ;
180+ if ( ! contextModel ) {
181+ return ;
182+ }
183+
184+ if (
185+ AstUtils . streamAst ( expr ) . some ( ( node ) => {
186+ if ( ! isDataFieldReference ( node ) ) {
187+ // not a field reference, skip
188+ return false ;
189+ }
190+
191+ // referenced field is not a member of the context model, skip
192+ if ( node . target . ref ?. $container !== contextModel ) {
193+ return false ;
194+ }
195+
196+ const field = node . target . ref as DataField ;
197+ if ( ! isRelationshipField ( field ) ) {
198+ // not a relation, skip
199+ return false ;
200+ }
201+
202+ if ( isAuthOrAuthMemberAccess ( node ) ) {
203+ // field reference is from auth() or access from auth(), not a relation query
204+ return false ;
205+ }
206+
207+ // check if the the node is a reference inside a collection predicate scope by auth access,
208+ // e.g., `auth().foo?[x > 0]`
209+
210+ // make sure to skip the current level if the node is already an LHS of a collection predicate,
211+ // otherwise we're just circling back to itself when visiting the parent
212+ const startNode =
213+ isCollectionPredicate ( node . $container ) && ( node . $container as BinaryExpr ) . left === node
214+ ? node . $container
215+ : node ;
216+ const collectionPredicate = AstUtils . getContainerOfType ( startNode . $container , isCollectionPredicate ) ;
217+ if ( collectionPredicate && isAuthOrAuthMemberAccess ( collectionPredicate . left ) ) {
218+ return false ;
219+ }
220+
221+ const relationAttr = field . attributes . find ( ( attr ) => attr . decl . ref ?. name === '@relation' ) ;
222+ if ( ! relationAttr ) {
223+ // no "@relation", not owner side of the relation, match
224+ return true ;
225+ }
226+
227+ if ( ! relationAttr . args . some ( ( arg ) => arg . name === 'fields' ) ) {
228+ // no "fields" argument, can't be owner side of the relation, match
229+ return true ;
230+ }
231+
232+ return false ;
233+ } )
234+ ) {
235+ accept ( 'error' , `non-owned relation fields are not allowed in "create" rules` , { node : expr } ) ;
236+ }
237+ }
238+
239+ // TODO: design a way to let plugin register validation
173240 @check ( '@allow' )
174241 @check ( '@deny' )
175242 // @ts -expect-error
@@ -199,9 +266,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
199266 ) ;
200267 }
201268 }
202-
203- // @encrypted fields cannot be used in policy rules
204- this . rejectEncryptedFields ( attr , accept ) ;
205269 }
206270
207271 @check ( '@@validate' )
@@ -261,14 +325,6 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
261325 }
262326 }
263327
264- private rejectEncryptedFields ( attr : AttributeApplication , accept : ValidationAcceptor ) {
265- AstUtils . streamAllContents ( attr ) . forEach ( ( node ) => {
266- if ( isDataFieldReference ( node ) && hasAttribute ( node . target . ref as DataField , '@encrypted' ) ) {
267- accept ( 'error' , `Encrypted fields cannot be used in policy rules` , { node } ) ;
268- }
269- } ) ;
270- }
271-
272328 private validatePolicyKinds (
273329 kind : string ,
274330 candidates : string [ ] ,
0 commit comments