@@ -17,6 +17,8 @@ import {
1717 getTableNames ,
1818 hasValidPrimaryKey ,
1919 lcFirst ,
20+ toPascalCase ,
21+ ucFirst ,
2022} from '../utils' ;
2123
2224export interface GeneratedModelFile {
@@ -165,6 +167,7 @@ export function generateModelFile(
165167 table : Table ,
166168 _useSharedTypes : boolean ,
167169 options ?: { condition ?: boolean } ,
170+ allTables ?: Table [ ] ,
168171) : GeneratedModelFile {
169172 const conditionEnabled = options ?. condition !== false ;
170173 const { typeName, singularName, pluralName } = getTableNames ( table ) ;
@@ -196,16 +199,23 @@ export function generateModelFile(
196199 const statements : t . Statement [ ] = [ ] ;
197200
198201 statements . push ( createImportDeclaration ( '../client' , [ 'OrmClient' ] ) ) ;
202+ const m2nRels = table . relations . manyToMany . filter (
203+ ( r ) => r . junctionLeftKeyFields ?. length && r . junctionRightKeyFields ?. length ,
204+ ) ;
205+ const hasM2n = m2nRels . length > 0 ;
206+
207+ const queryBuilderImports = [
208+ 'QueryBuilder' ,
209+ 'buildFindManyDocument' ,
210+ 'buildFindFirstDocument' ,
211+ 'buildFindOneDocument' ,
212+ 'buildCreateDocument' ,
213+ 'buildUpdateByPkDocument' ,
214+ 'buildDeleteByPkDocument' ,
215+ ...( hasM2n ? [ 'buildJunctionRemoveDocument' ] : [ ] ) ,
216+ ] ;
199217 statements . push (
200- createImportDeclaration ( '../query-builder' , [
201- 'QueryBuilder' ,
202- 'buildFindManyDocument' ,
203- 'buildFindFirstDocument' ,
204- 'buildFindOneDocument' ,
205- 'buildCreateDocument' ,
206- 'buildUpdateByPkDocument' ,
207- 'buildDeleteByPkDocument' ,
208- ] ) ,
218+ createImportDeclaration ( '../query-builder' , queryBuilderImports ) ,
209219 ) ;
210220 statements . push (
211221 createImportDeclaration (
@@ -982,6 +992,166 @@ export function generateModelFile(
982992 ) ;
983993 }
984994
995+ // ── M:N add/remove methods ────────────────────────────────────────────
996+ for ( const rel of m2nRels ) {
997+ if ( ! rel . fieldName ) continue ;
998+
999+ const junctionTable = allTables ?. find ( ( tb ) => tb . name === rel . junctionTable ) ;
1000+ if ( ! junctionTable ) continue ;
1001+
1002+ const junctionNames = getTableNames ( junctionTable ) ;
1003+ const junctionCreateMutation = junctionTable . query ?. create ?? `create${ junctionNames . typeName } ` ;
1004+ const junctionCreateInputType = `Create${ junctionNames . typeName } Input` ;
1005+ const junctionDeleteMutation = junctionTable . query ?. delete ;
1006+ const junctionDeleteInputType = `Delete${ junctionNames . typeName } Input` ;
1007+ const junctionSingular = junctionNames . singularName ;
1008+
1009+ // Derive a friendly singular name from the fieldName (e.g., "tags" → "Tag")
1010+ const relSingular = ucFirst ( rel . fieldName . replace ( / s $ / , '' ) ) ;
1011+
1012+ const leftKeys = rel . junctionLeftKeyFields ! ;
1013+ const rightKeys = rel . junctionRightKeyFields ! ;
1014+ const leftPkFields = rel . leftKeyFields ?? [ 'id' ] ;
1015+ const rightPkFields = rel . rightKeyFields ?? [ 'id' ] ;
1016+
1017+ // ── add<Relation> ───────────────────────────────────────────────
1018+ {
1019+ // Parameters: one param per left PK + one param per right PK
1020+ const params : t . Identifier [ ] = [ ] ;
1021+ for ( const lk of leftPkFields ) {
1022+ const p = t . identifier ( lk ) ;
1023+ p . typeAnnotation = t . tsTypeAnnotation ( t . tsStringKeyword ( ) ) ;
1024+ params . push ( p ) ;
1025+ }
1026+ for ( const rk of rightPkFields ) {
1027+ const p = t . identifier ( rk === leftPkFields [ 0 ] ? `right${ ucFirst ( rk ) } ` : rk ) ;
1028+ p . typeAnnotation = t . tsTypeAnnotation ( t . tsStringKeyword ( ) ) ;
1029+ params . push ( p ) ;
1030+ }
1031+
1032+ // Build the junction row data object: { junctionLeftKey: leftPk, junctionRightKey: rightPk }
1033+ const dataProps : t . ObjectProperty [ ] = [ ] ;
1034+ for ( let i = 0 ; i < leftKeys . length ; i ++ ) {
1035+ dataProps . push (
1036+ t . objectProperty ( t . identifier ( leftKeys [ i ] ) , t . identifier ( params [ i ] . name ) ) ,
1037+ ) ;
1038+ }
1039+ for ( let i = 0 ; i < rightKeys . length ; i ++ ) {
1040+ dataProps . push (
1041+ t . objectProperty (
1042+ t . identifier ( rightKeys [ i ] ) ,
1043+ t . identifier ( params [ leftPkFields . length + i ] . name ) ,
1044+ ) ,
1045+ ) ;
1046+ }
1047+
1048+ const body : t . Statement [ ] = [
1049+ t . variableDeclaration ( 'const' , [
1050+ t . variableDeclarator (
1051+ t . objectPattern ( [
1052+ t . objectProperty ( t . identifier ( 'document' ) , t . identifier ( 'document' ) , false , true ) ,
1053+ t . objectProperty ( t . identifier ( 'variables' ) , t . identifier ( 'variables' ) , false , true ) ,
1054+ ] ) ,
1055+ t . callExpression ( t . identifier ( 'buildCreateDocument' ) , [
1056+ t . stringLiteral ( junctionNames . typeName ) ,
1057+ t . stringLiteral ( junctionCreateMutation ) ,
1058+ t . stringLiteral ( junctionSingular ) ,
1059+ t . objectExpression ( [ t . objectProperty ( t . identifier ( 'id' ) , t . booleanLiteral ( true ) ) ] ) ,
1060+ t . objectExpression ( dataProps ) ,
1061+ t . stringLiteral ( junctionCreateInputType ) ,
1062+ ] ) ,
1063+ ) ,
1064+ ] ) ,
1065+ t . returnStatement (
1066+ t . newExpression ( t . identifier ( 'QueryBuilder' ) , [
1067+ t . objectExpression ( [
1068+ t . objectProperty (
1069+ t . identifier ( 'client' ) ,
1070+ t . memberExpression ( t . thisExpression ( ) , t . identifier ( 'client' ) ) ,
1071+ ) ,
1072+ t . objectProperty ( t . identifier ( 'operation' ) , t . stringLiteral ( 'mutation' ) ) ,
1073+ t . objectProperty ( t . identifier ( 'operationName' ) , t . stringLiteral ( junctionNames . typeName ) ) ,
1074+ t . objectProperty ( t . identifier ( 'fieldName' ) , t . stringLiteral ( junctionCreateMutation ) ) ,
1075+ t . objectProperty ( t . identifier ( 'document' ) , t . identifier ( 'document' ) , false , true ) ,
1076+ t . objectProperty ( t . identifier ( 'variables' ) , t . identifier ( 'variables' ) , false , true ) ,
1077+ ] ) ,
1078+ ] ) ,
1079+ ) ,
1080+ ] ;
1081+
1082+ const method = t . classMethod ( 'method' , t . identifier ( `add${ relSingular } ` ) , params , t . blockStatement ( body ) ) ;
1083+ method . async = true ;
1084+ classBody . push ( method ) ;
1085+ }
1086+
1087+ // ── remove<Relation> ────────────────────────────────────────────
1088+ if ( junctionDeleteMutation ) {
1089+ const params : t . Identifier [ ] = [ ] ;
1090+ for ( const lk of leftPkFields ) {
1091+ const p = t . identifier ( lk ) ;
1092+ p . typeAnnotation = t . tsTypeAnnotation ( t . tsStringKeyword ( ) ) ;
1093+ params . push ( p ) ;
1094+ }
1095+ for ( const rk of rightPkFields ) {
1096+ const p = t . identifier ( rk === leftPkFields [ 0 ] ? `right${ ucFirst ( rk ) } ` : rk ) ;
1097+ p . typeAnnotation = t . tsTypeAnnotation ( t . tsStringKeyword ( ) ) ;
1098+ params . push ( p ) ;
1099+ }
1100+
1101+ // Build the keys object for junction delete
1102+ const keysProps : t . ObjectProperty [ ] = [ ] ;
1103+ for ( let i = 0 ; i < leftKeys . length ; i ++ ) {
1104+ keysProps . push (
1105+ t . objectProperty ( t . identifier ( leftKeys [ i ] ) , t . identifier ( params [ i ] . name ) ) ,
1106+ ) ;
1107+ }
1108+ for ( let i = 0 ; i < rightKeys . length ; i ++ ) {
1109+ keysProps . push (
1110+ t . objectProperty (
1111+ t . identifier ( rightKeys [ i ] ) ,
1112+ t . identifier ( params [ leftPkFields . length + i ] . name ) ,
1113+ ) ,
1114+ ) ;
1115+ }
1116+
1117+ const body : t . Statement [ ] = [
1118+ t . variableDeclaration ( 'const' , [
1119+ t . variableDeclarator (
1120+ t . objectPattern ( [
1121+ t . objectProperty ( t . identifier ( 'document' ) , t . identifier ( 'document' ) , false , true ) ,
1122+ t . objectProperty ( t . identifier ( 'variables' ) , t . identifier ( 'variables' ) , false , true ) ,
1123+ ] ) ,
1124+ t . callExpression ( t . identifier ( 'buildJunctionRemoveDocument' ) , [
1125+ t . stringLiteral ( junctionNames . typeName ) ,
1126+ t . stringLiteral ( junctionDeleteMutation ) ,
1127+ t . objectExpression ( keysProps ) ,
1128+ t . stringLiteral ( junctionDeleteInputType ) ,
1129+ ] ) ,
1130+ ) ,
1131+ ] ) ,
1132+ t . returnStatement (
1133+ t . newExpression ( t . identifier ( 'QueryBuilder' ) , [
1134+ t . objectExpression ( [
1135+ t . objectProperty (
1136+ t . identifier ( 'client' ) ,
1137+ t . memberExpression ( t . thisExpression ( ) , t . identifier ( 'client' ) ) ,
1138+ ) ,
1139+ t . objectProperty ( t . identifier ( 'operation' ) , t . stringLiteral ( 'mutation' ) ) ,
1140+ t . objectProperty ( t . identifier ( 'operationName' ) , t . stringLiteral ( junctionNames . typeName ) ) ,
1141+ t . objectProperty ( t . identifier ( 'fieldName' ) , t . stringLiteral ( junctionDeleteMutation ) ) ,
1142+ t . objectProperty ( t . identifier ( 'document' ) , t . identifier ( 'document' ) , false , true ) ,
1143+ t . objectProperty ( t . identifier ( 'variables' ) , t . identifier ( 'variables' ) , false , true ) ,
1144+ ] ) ,
1145+ ] ) ,
1146+ ) ,
1147+ ] ;
1148+
1149+ const method = t . classMethod ( 'method' , t . identifier ( `remove${ relSingular } ` ) , params , t . blockStatement ( body ) ) ;
1150+ method . async = true ;
1151+ classBody . push ( method ) ;
1152+ }
1153+ }
1154+
9851155 const classDecl = t . classDeclaration (
9861156 t . identifier ( modelName ) ,
9871157 null ,
@@ -1005,5 +1175,5 @@ export function generateAllModelFiles(
10051175 useSharedTypes : boolean ,
10061176 options ?: { condition ?: boolean } ,
10071177) : GeneratedModelFile [ ] {
1008- return tables . map ( ( table ) => generateModelFile ( table , useSharedTypes , options ) ) ;
1178+ return tables . map ( ( table ) => generateModelFile ( table , useSharedTypes , options , tables ) ) ;
10091179}
0 commit comments