@@ -111,6 +111,9 @@ export class PrismaSchemaGenerator {
111111 this . generateDefaultGenerator ( prisma ) ;
112112 }
113113
114+ // auto-generate missing opposite relation fields so Prisma schema is always valid
115+ this . generateMissingOppositeRelations ( prisma ) ;
116+
114117 return this . PRELUDE + prisma . toString ( ) ;
115118 }
116119
@@ -335,12 +338,15 @@ export class PrismaSchemaGenerator {
335338 return AstUtils . streamAst ( expr ) . some ( isAuthInvocation ) ;
336339 }
337340
341+ // Args that are ZenStack-only and should not appear in the generated Prisma schema
342+ private readonly NON_PRISMA_RELATION_ARGS = [ 'createOpposite' ] ;
343+
338344 private makeFieldAttribute ( attr : DataFieldAttribute ) {
339345 const attrName = attr . decl . ref ! . name ;
340- return new PrismaFieldAttribute (
341- attrName ,
342- attr . args . map ( ( arg ) => this . makeAttributeArg ( arg ) ) ,
343- ) ;
346+ const args = attr . args
347+ . filter ( ( arg ) => ! ( attrName === '@relation' && arg . name && this . NON_PRISMA_RELATION_ARGS . includes ( arg . name ) ) )
348+ . map ( ( arg ) => this . makeAttributeArg ( arg ) ) ;
349+ return new PrismaFieldAttribute ( attrName , args ) ;
344350 }
345351
346352 private makeAttributeArg ( arg : AttributeArg ) : PrismaAttributeArg {
@@ -512,6 +518,90 @@ export class PrismaSchemaGenerator {
512518 ) ;
513519 }
514520
521+ private hasCreateReverse ( field : DataField ) : boolean {
522+ const relAttr = field . attributes . find ( ( attr ) => attr . decl . ref ?. name === '@relation' ) ;
523+ if ( ! relAttr ) return false ;
524+ const createOppositeArg = relAttr . args . find ( ( arg ) => arg . name === 'createOpposite' ) ;
525+ if ( ! createOppositeArg ) return false ;
526+ return isLiteralExpr ( createOppositeArg . value ) && createOppositeArg . value . value === true ;
527+ }
528+
529+ /**
530+ * For relation fields with `createOpposite: true`, auto-generates the opposite relation
531+ * field in the Prisma schema so it stays valid.
532+ */
533+ private generateMissingOppositeRelations ( prisma : PrismaModel ) {
534+ for ( const decl of this . zmodel . declarations ) {
535+ if ( ! isDataModel ( decl ) ) continue ;
536+
537+ const allFields = getAllFields ( decl , false ) ;
538+ for ( const field of allFields ) {
539+ if ( ! isDataModel ( field . type . reference ?. ref ) ) continue ;
540+ if ( ! this . hasCreateReverse ( field ) ) continue ;
541+
542+ const oppositeModel = field . type . reference ! . ref ! as DataModel ;
543+ const oppositeFields = getAllFields ( oppositeModel , false ) . filter (
544+ ( f ) => f !== field && f . type . reference ?. ref ?. name === decl . name ,
545+ ) ;
546+
547+ if ( oppositeFields . length === 0 ) {
548+ // missing opposite relation — add it to the Prisma model
549+ const prismaOppositeModel = prisma . findModel ( oppositeModel . name ) ;
550+ if ( ! prismaOppositeModel ) continue ;
551+
552+ const fieldName = lowerCaseFirst ( decl . name ) ;
553+ // check if a field with this name already exists in the Prisma model
554+ if ( prismaOppositeModel . fields . some ( ( f ) => f . name === fieldName ) ) continue ;
555+
556+ if ( field . type . array ) {
557+ // the field is an array (e.g., posts Post[]), so the opposite should be a scalar relation
558+ const idFields = getIdFields ( prismaOppositeModel . name === decl . name ? decl : oppositeModel ) ;
559+ if ( idFields . length === 0 ) continue ;
560+
561+ const idFieldName = idFields [ 0 ] ! ;
562+ const refIdFieldName = fieldName + idFieldName . charAt ( 0 ) . toUpperCase ( ) + idFieldName . slice ( 1 ) ;
563+ // add FK field if not present
564+ if ( ! prismaOppositeModel . fields . some ( ( f ) => f . name === refIdFieldName ) ) {
565+ const idField = allFields . find ( ( f ) => f . name === idFieldName ) ;
566+ if ( idField ?. type . type ) {
567+ prismaOppositeModel . addField (
568+ refIdFieldName ,
569+ new ModelFieldType ( idField . type . type , false , false ) ,
570+ ) ;
571+ }
572+ }
573+ prismaOppositeModel . addField (
574+ fieldName ,
575+ new ModelFieldType ( decl . name , false , false ) ,
576+ [
577+ new PrismaFieldAttribute ( '@relation' , [
578+ new PrismaAttributeArg (
579+ 'fields' ,
580+ new PrismaAttributeArgValue ( 'Array' , [
581+ new PrismaAttributeArgValue ( 'FieldReference' , new PrismaFieldReference ( refIdFieldName ) ) ,
582+ ] ) ,
583+ ) ,
584+ new PrismaAttributeArg (
585+ 'references' ,
586+ new PrismaAttributeArgValue ( 'Array' , [
587+ new PrismaAttributeArgValue ( 'FieldReference' , new PrismaFieldReference ( idFieldName ) ) ,
588+ ] ) ,
589+ ) ,
590+ ] ) ,
591+ ] ,
592+ ) ;
593+ } else {
594+ // the field is a scalar relation (e.g., greeting Greeting), so the opposite should be an array
595+ prismaOppositeModel . addField (
596+ fieldName ,
597+ new ModelFieldType ( decl . name , true , false ) ,
598+ ) ;
599+ }
600+ }
601+ }
602+ }
603+ }
604+
515605 private truncate ( name : string ) {
516606 if ( name . length <= IDENTIFIER_NAME_MAX_LENGTH ) {
517607 return name ;
0 commit comments