77 ReferenceExpr ,
88} from '@zenstackhq/language/ast' ;
99import { analyzePolicies , getLiteral } from '@zenstackhq/sdk' ;
10- import { ValidationAcceptor } from 'langium' ;
10+ import { AstNode , DiagnosticInfo , getDocument , ValidationAcceptor } from 'langium' ;
1111import { IssueCodes , SCALAR_TYPES } from '../constants' ;
1212import { AstValidator } from '../types' ;
1313import { getIdFields , getUniqueFields } from '../utils' ;
@@ -18,13 +18,14 @@ import { validateAttributeApplication, validateDuplicatedDeclarations } from './
1818 */
1919export default class DataModelValidator implements AstValidator < DataModel > {
2020 validate ( dm : DataModel , accept : ValidationAcceptor ) : void {
21- validateDuplicatedDeclarations ( dm . fields , accept ) ;
21+ this . validateBaseAbstractModel ( dm , accept ) ;
22+ validateDuplicatedDeclarations ( dm . $resolvedFields , accept ) ;
2223 this . validateAttributes ( dm , accept ) ;
2324 this . validateFields ( dm , accept ) ;
2425 }
2526
2627 private validateFields ( dm : DataModel , accept : ValidationAcceptor ) {
27- const idFields = dm . fields . filter ( ( f ) => f . attributes . find ( ( attr ) => attr . decl . ref ?. name === '@id' ) ) ;
28+ const idFields = dm . $resolvedFields . filter ( ( f ) => f . attributes . find ( ( attr ) => attr . decl . ref ?. name === '@id' ) ) ;
2829 const modelLevelIds = getIdFields ( dm ) ;
2930
3031 if ( idFields . length === 0 && modelLevelIds . length === 0 ) {
@@ -57,6 +58,14 @@ export default class DataModelValidator implements AstValidator<DataModel> {
5758 }
5859
5960 dm . fields . forEach ( ( field ) => this . validateField ( field , accept ) ) ;
61+
62+ if ( ! dm . isAbstract ) {
63+ dm . $resolvedFields
64+ . filter ( ( x ) => isDataModel ( x . type . reference ?. ref ) )
65+ . forEach ( ( y ) => {
66+ this . validateRelationField ( y , accept ) ;
67+ } ) ;
68+ }
6069 }
6170
6271 private validateField ( field : DataModelField , accept : ValidationAcceptor ) : void {
@@ -69,10 +78,6 @@ export default class DataModelValidator implements AstValidator<DataModel> {
6978 }
7079
7180 field . attributes . forEach ( ( attr ) => validateAttributeApplication ( attr , accept ) ) ;
72-
73- if ( isDataModel ( field . type . reference ?. ref ) ) {
74- this . validateRelationField ( field , accept ) ;
75- }
7681 }
7782
7883 private validateAttributes ( dm : DataModel , accept : ValidationAcceptor ) {
@@ -175,8 +180,9 @@ export default class DataModelValidator implements AstValidator<DataModel> {
175180 if ( relationName ) {
176181 // field's relation points to another type, and that type's opposite relation field
177182 // points back
178- const oppositeModelFields = field . type . reference ?. ref ?. fields as DataModelField [ ] ;
179- if ( oppositeModelFields ) {
183+ const oppositeModel = field . type . reference ?. ref as DataModel ;
184+ if ( oppositeModel ) {
185+ const oppositeModelFields = oppositeModel . $resolvedFields as DataModelField [ ] ;
180186 for ( const oppositeField of oppositeModelFields ) {
181187 // find the opposite relation with the matching name
182188 const relAttr = oppositeField . attributes . find ( ( a ) => a . decl . ref ?. name === '@relation' ) ;
@@ -204,34 +210,68 @@ export default class DataModelValidator implements AstValidator<DataModel> {
204210 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
205211 const oppositeModel = field . type . reference ! . ref ! as DataModel ;
206212
207- let oppositeFields = oppositeModel . fields . filter ( ( f ) => f . type . reference ?. ref === field . $container ) ;
213+ // Use name because the current document might be updated
214+ let oppositeFields = oppositeModel . $resolvedFields . filter (
215+ ( f ) => f . type . reference ?. ref ?. name === field . $container . name
216+ ) ;
208217 oppositeFields = oppositeFields . filter ( ( f ) => {
209218 const fieldRel = this . parseRelation ( f ) ;
210219 return fieldRel . valid && fieldRel . name === thisRelation . name ;
211220 } ) ;
212221
213222 if ( oppositeFields . length === 0 ) {
223+ const node = field . $isInherited ? field . $container : field ;
224+ const info : DiagnosticInfo < AstNode , string > = { node, code : IssueCodes . MissingOppositeRelation } ;
225+
226+ let relationFieldDocUri : string ;
227+ let relationDataModelName : string ;
228+
229+ if ( field . $isInherited ) {
230+ info . property = 'name' ;
231+ const container = field . $container as DataModel ;
232+ const abstractContainer = container . superTypes . find ( ( x ) =>
233+ x . ref ?. fields . find ( ( f ) => f . name === field . name )
234+ ) ?. ref as DataModel ;
235+
236+ relationFieldDocUri = getDocument ( abstractContainer ) . textDocument . uri ;
237+ relationDataModelName = abstractContainer . name ;
238+ } else {
239+ relationFieldDocUri = getDocument ( field ) . textDocument . uri ;
240+ relationDataModelName = field . $container . name ;
241+ }
242+
243+ const data : MissingOppositeRelationData = {
244+ relationFieldName : field . name ,
245+ relationDataModelName,
246+ relationFieldDocUri,
247+ dataModelName : field . $container . name ,
248+ } ;
249+
250+ info . data = data ;
251+
214252 accept (
215253 'error' ,
216254 `The relation field "${ field . name } " on model "${ field . $container . name } " is missing an opposite relation field on model "${ oppositeModel . name } "` ,
217- { node : field , code : IssueCodes . MissingOppositeRelation }
255+ info
218256 ) ;
219257 return ;
220258 } else if ( oppositeFields . length > 1 ) {
221- oppositeFields . forEach ( ( f ) => {
222- if ( this . isSelfRelation ( f ) ) {
223- // self relations are partial
224- // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
225- } else {
226- accept (
227- 'error' ,
228- `Fields ${ oppositeFields . map ( ( f ) => '"' + f . name + '"' ) . join ( ', ' ) } on model "${
229- oppositeModel . name
230- } " refer to the same relation to model "${ field . $container . name } "`,
231- { node : f }
232- ) ;
233- }
234- } ) ;
259+ oppositeFields
260+ . filter ( ( x ) => ! x . $isInherited )
261+ . forEach ( ( f ) => {
262+ if ( this . isSelfRelation ( f ) ) {
263+ // self relations are partial
264+ // https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
265+ } else {
266+ accept (
267+ 'error' ,
268+ `Fields ${ oppositeFields . map ( ( f ) => '"' + f . name + '"' ) . join ( ', ' ) } on model "${
269+ oppositeModel . name
270+ } " refer to the same relation to model "${ field . $container . name } "`,
271+ { node : f }
272+ ) ;
273+ }
274+ } ) ;
235275 return ;
236276 }
237277
@@ -317,4 +357,26 @@ export default class DataModelValidator implements AstValidator<DataModel> {
317357 } ) ;
318358 }
319359 }
360+
361+ private validateBaseAbstractModel ( model : DataModel , accept : ValidationAcceptor ) {
362+ model . superTypes . forEach ( ( superType , index ) => {
363+ if ( ! superType . ref ?. isAbstract )
364+ accept ( 'error' , `Model ${ superType . $refText } cannot be extended because it's not abstract` , {
365+ node : model ,
366+ property : 'superTypes' ,
367+ index,
368+ } ) ;
369+ } ) ;
370+ }
371+ }
372+
373+ export interface MissingOppositeRelationData {
374+ relationDataModelName : string ;
375+ relationFieldName : string ;
376+ // it might be the abstract model in the imported document
377+ relationFieldDocUri : string ;
378+
379+ // the name of DataModel that the relation field belongs to.
380+ // the document is the same with the error node.
381+ dataModelName : string ;
320382}
0 commit comments