@@ -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,130 @@ export class PrismaSchemaGenerator {
512518 ) ;
513519 }
514520
521+ private hasCreateOpposite ( 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+ private getRelationName ( field : DataField ) : string | undefined {
530+ const relAttr = field . attributes . find ( ( attr ) => attr . decl . ref ?. name === '@relation' ) ;
531+ if ( ! relAttr ) return undefined ;
532+ const nameArg = relAttr . args . find ( ( arg ) => ! arg . name || arg . name === 'name' ) ;
533+ if ( ! nameArg ) return undefined ;
534+ return isStringLiteral ( nameArg . value ) ? ( nameArg . value . value as string ) : undefined ;
535+ }
536+
537+ /**
538+ * For relation fields with `createOpposite: true`, auto-generates the opposite relation
539+ * field in the Prisma schema so it stays valid.
540+ */
541+ private generateMissingOppositeRelations ( prisma : PrismaModel ) {
542+ for ( const decl of this . zmodel . declarations ) {
543+ if ( ! isDataModel ( decl ) ) continue ;
544+
545+ const allFields = getAllFields ( decl , false ) ;
546+ for ( const field of allFields ) {
547+ if ( ! isDataModel ( field . type . reference ?. ref ) ) continue ;
548+ if ( ! this . hasCreateOpposite ( field ) ) continue ;
549+
550+ const relationName = this . getRelationName ( field ) ;
551+ const oppositeModel = field . type . reference ! . ref ! as DataModel ;
552+
553+ // match opposite fields by both target model name and relation name
554+ const oppositeFields = getAllFields ( oppositeModel , false ) . filter ( ( f ) => {
555+ if ( f === field || f . type . reference ?. ref ?. name !== decl . name ) return false ;
556+ return this . getRelationName ( f ) === relationName ;
557+ } ) ;
558+
559+ if ( oppositeFields . length === 0 ) {
560+ // missing opposite relation — add it to the Prisma model
561+ const prismaOppositeModel = prisma . findModel ( oppositeModel . name ) ;
562+ if ( ! prismaOppositeModel ) continue ;
563+
564+ // use relation name to disambiguate when multiple relations target the same model
565+ const fieldName = relationName
566+ ? lowerCaseFirst ( relationName )
567+ : lowerCaseFirst ( decl . name ) ;
568+ if ( prismaOppositeModel . fields . some ( ( f ) => f . name === fieldName ) ) {
569+ throw new Error (
570+ `Cannot auto-generate opposite relation field "${ fieldName } " on model "${ oppositeModel . name } ": a field with that name already exists` ,
571+ ) ;
572+ }
573+
574+ // build @relation args for the generated field, including relation name if present
575+ const buildRelationAttr = ( extraArgs : PrismaAttributeArg [ ] ) => {
576+ const args : PrismaAttributeArg [ ] = [ ] ;
577+ if ( relationName ) {
578+ args . push (
579+ new PrismaAttributeArg (
580+ undefined ,
581+ new PrismaAttributeArgValue ( 'String' , relationName ) ,
582+ ) ,
583+ ) ;
584+ }
585+ args . push ( ...extraArgs ) ;
586+ return new PrismaFieldAttribute ( '@relation' , args ) ;
587+ } ;
588+
589+ if ( field . type . array ) {
590+ // the field is an array (e.g., posts Post[]), so the opposite should be a scalar relation
591+ const idFields = getIdFields ( decl ) ;
592+ if ( idFields . length === 0 ) continue ;
593+
594+ // create FK fields for all id fields (supports composite keys)
595+ idFields . forEach ( ( idFieldName ) => {
596+ const refIdFieldName = fieldName + idFieldName . charAt ( 0 ) . toUpperCase ( ) + idFieldName . slice ( 1 ) ;
597+ if ( ! prismaOppositeModel . fields . some ( ( f ) => f . name === refIdFieldName ) ) {
598+ const idField = allFields . find ( ( f ) => f . name === idFieldName ) ;
599+ if ( idField ?. type . type ) {
600+ prismaOppositeModel . addField (
601+ refIdFieldName ,
602+ new ModelFieldType ( idField . type . type , false , false ) ,
603+ ) ;
604+ }
605+ }
606+ } ) ;
607+
608+ prismaOppositeModel . addField (
609+ fieldName ,
610+ new ModelFieldType ( decl . name , false , false ) ,
611+ [
612+ buildRelationAttr ( [
613+ new PrismaAttributeArg (
614+ 'fields' ,
615+ new PrismaAttributeArgValue ( 'Array' , idFields . map (
616+ ( idFieldName ) => new PrismaAttributeArgValue ( 'FieldReference' ,
617+ new PrismaFieldReference ( fieldName + idFieldName . charAt ( 0 ) . toUpperCase ( ) + idFieldName . slice ( 1 ) ) ) ,
618+ ) ) ,
619+ ) ,
620+ new PrismaAttributeArg (
621+ 'references' ,
622+ new PrismaAttributeArgValue ( 'Array' , idFields . map (
623+ ( idFieldName ) => new PrismaAttributeArgValue ( 'FieldReference' , new PrismaFieldReference ( idFieldName ) ) ,
624+ ) ) ,
625+ ) ,
626+ ] ) ,
627+ ] ,
628+ ) ;
629+ } else {
630+ // the field is a scalar relation (e.g., user User), so the opposite should be an array
631+ const attrs = relationName
632+ ? [ buildRelationAttr ( [ ] ) ]
633+ : [ ] ;
634+ prismaOppositeModel . addField (
635+ fieldName ,
636+ new ModelFieldType ( decl . name , true , false ) ,
637+ attrs ,
638+ ) ;
639+ }
640+ }
641+ }
642+ }
643+ }
644+
515645 private truncate ( name : string ) {
516646 if ( name . length <= IDENTIFIER_NAME_MAX_LENGTH ) {
517647 return name ;
0 commit comments