import { ColumnMetadata } from "../metadata/ColumnMetadata" import { DataSource } from "../data-source/DataSource" import { EntityMetadata } from "../metadata/EntityMetadata" import { ForeignKeyMetadata } from "../metadata/ForeignKeyMetadata" import { IndexMetadata } from "../metadata/IndexMetadata" import { JoinTableMetadataArgs } from "../metadata-args/JoinTableMetadataArgs" import { RelationMetadata } from "../metadata/RelationMetadata" import { TypeORMError } from "../error" import { DriverUtils } from "../driver/DriverUtils" /** * Creates EntityMetadata for junction tables. * Junction tables are tables generated by many-to-many relations. */ export class JunctionEntityMetadataBuilder { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(private connection: DataSource) {} // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Builds EntityMetadata for the junction of the given many-to-many relation. */ build( relation: RelationMetadata, joinTable: JoinTableMetadataArgs, ): EntityMetadata { const referencedColumns = this.collectReferencedColumns( relation, joinTable, ) const inverseReferencedColumns = this.collectInverseReferencedColumns( relation, joinTable, ) const joinTableName = joinTable.name || this.connection.namingStrategy.joinTableName( relation.entityMetadata.tableNameWithoutPrefix, relation.inverseEntityMetadata.tableNameWithoutPrefix, relation.propertyPath, relation.inverseRelation ? relation.inverseRelation.propertyName : "", ) const entityMetadata = new EntityMetadata({ connection: this.connection, args: { target: "", name: joinTableName, type: "junction", database: joinTable.database || relation.entityMetadata.database, schema: joinTable.schema || relation.entityMetadata.schema, synchronize: joinTable.synchronize, }, }) entityMetadata.build() // create original side junction columns const junctionColumns = referencedColumns.map((referencedColumn) => { const joinColumn = joinTable.joinColumns ? joinTable.joinColumns.find((joinColumnArgs) => { return ( (!joinColumnArgs.referencedColumnName || joinColumnArgs.referencedColumnName === referencedColumn.propertyName) && !!joinColumnArgs.name ) }) : undefined const columnName = joinColumn && joinColumn.name ? joinColumn.name : this.connection.namingStrategy.joinTableColumnName( relation.entityMetadata.tableNameWithoutPrefix, referencedColumn.propertyName, referencedColumn.databaseName, ) return new ColumnMetadata({ connection: this.connection, entityMetadata: entityMetadata, referencedColumn: referencedColumn, args: { target: "", mode: "virtual", propertyName: columnName, options: { name: columnName, length: !referencedColumn.length && (DriverUtils.isMySQLFamily( this.connection.driver, ) || this.connection.driver.options.type === "aurora-mysql") && // some versions of mariadb support the column type and should not try to provide the length property this.connection.driver.normalizeType( referencedColumn, ) !== "uuid" && (referencedColumn.generationStrategy === "uuid" || referencedColumn.type === "uuid") ? "36" : referencedColumn.length, // fix https://github.com/typeorm/typeorm/issues/3604 width: referencedColumn.width, type: referencedColumn.type, precision: referencedColumn.precision, scale: referencedColumn.scale, charset: referencedColumn.charset, collation: referencedColumn.collation, zerofill: referencedColumn.zerofill, unsigned: referencedColumn.zerofill ? true : referencedColumn.unsigned, enum: referencedColumn.enum, enumName: referencedColumn.enumName, foreignKeyConstraintName: joinColumn?.foreignKeyConstraintName, nullable: false, primary: true, }, }, }) }) // create inverse side junction columns const inverseJunctionColumns = inverseReferencedColumns.map( (inverseReferencedColumn) => { const joinColumn = joinTable.inverseJoinColumns ? joinTable.inverseJoinColumns.find((joinColumnArgs) => { return ( (!joinColumnArgs.referencedColumnName || joinColumnArgs.referencedColumnName === inverseReferencedColumn.propertyName) && !!joinColumnArgs.name ) }) : undefined const columnName = joinColumn && joinColumn.name ? joinColumn.name : this.connection.namingStrategy.joinTableInverseColumnName( relation.inverseEntityMetadata .tableNameWithoutPrefix, inverseReferencedColumn.propertyName, inverseReferencedColumn.databaseName, ) return new ColumnMetadata({ connection: this.connection, entityMetadata: entityMetadata, referencedColumn: inverseReferencedColumn, args: { target: "", mode: "virtual", propertyName: columnName, options: { length: !inverseReferencedColumn.length && (DriverUtils.isMySQLFamily( this.connection.driver, ) || this.connection.driver.options.type === "aurora-mysql") && // some versions of mariadb support the column type and should not try to provide the length property this.connection.driver.normalizeType( inverseReferencedColumn, ) !== "uuid" && (inverseReferencedColumn.generationStrategy === "uuid" || inverseReferencedColumn.type === "uuid") ? "36" : inverseReferencedColumn.length, // fix https://github.com/typeorm/typeorm/issues/3604 width: inverseReferencedColumn.width, // fix https://github.com/typeorm/typeorm/issues/6442 type: inverseReferencedColumn.type, precision: inverseReferencedColumn.precision, scale: inverseReferencedColumn.scale, charset: inverseReferencedColumn.charset, collation: inverseReferencedColumn.collation, zerofill: inverseReferencedColumn.zerofill, unsigned: inverseReferencedColumn.zerofill ? true : inverseReferencedColumn.unsigned, enum: inverseReferencedColumn.enum, enumName: inverseReferencedColumn.enumName, foreignKeyConstraintName: joinColumn?.foreignKeyConstraintName, name: columnName, nullable: false, primary: true, }, }, }) }, ) this.changeDuplicatedColumnNames( junctionColumns, inverseJunctionColumns, ) // set junction table columns entityMetadata.ownerColumns = junctionColumns entityMetadata.inverseColumns = inverseJunctionColumns entityMetadata.ownColumns = [ ...junctionColumns, ...inverseJunctionColumns, ] entityMetadata.ownColumns.forEach( (column) => (column.relationMetadata = relation), ) // create junction table foreign keys // Note: UPDATE CASCADE clause is not supported in Oracle. // Note: UPDATE/DELETE CASCADE clauses are not supported in Spanner. entityMetadata.foreignKeys = relation.createForeignKeyConstraints ? [ new ForeignKeyMetadata({ entityMetadata: entityMetadata, referencedEntityMetadata: relation.entityMetadata, columns: junctionColumns, referencedColumns: referencedColumns, name: junctionColumns[0]?.foreignKeyConstraintName, onDelete: this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.onDelete || "CASCADE", onUpdate: this.connection.driver.options.type === "oracle" || this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.onUpdate || "CASCADE", }), new ForeignKeyMetadata({ entityMetadata: entityMetadata, referencedEntityMetadata: relation.inverseEntityMetadata, columns: inverseJunctionColumns, referencedColumns: inverseReferencedColumns, name: inverseJunctionColumns[0]?.foreignKeyConstraintName, onDelete: this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.inverseRelation ? relation.inverseRelation.onDelete : "CASCADE", onUpdate: this.connection.driver.options.type === "oracle" || this.connection.driver.options.type === "spanner" ? "NO ACTION" : relation.inverseRelation ? relation.inverseRelation.onUpdate : "CASCADE", }), ] : [] // create junction table indices entityMetadata.ownIndices = [ new IndexMetadata({ entityMetadata: entityMetadata, columns: junctionColumns, args: { target: entityMetadata.target, synchronize: true, }, }), new IndexMetadata({ entityMetadata: entityMetadata, columns: inverseJunctionColumns, args: { target: entityMetadata.target, synchronize: true, }, }), ] // finally return entity metadata return entityMetadata } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Collects referenced columns from the given join column args. */ protected collectReferencedColumns( relation: RelationMetadata, joinTable: JoinTableMetadataArgs, ): ColumnMetadata[] { const hasAnyReferencedColumnName = joinTable.joinColumns ? joinTable.joinColumns.find( (joinColumn) => !!joinColumn.referencedColumnName, ) : false if ( !joinTable.joinColumns || (joinTable.joinColumns && !hasAnyReferencedColumnName) ) { return relation.entityMetadata.columns.filter( (column) => column.isPrimary, ) } else { return joinTable.joinColumns.map((joinColumn) => { const referencedColumn = relation.entityMetadata.columns.find( (column) => column.propertyName === joinColumn.referencedColumnName, ) if (!referencedColumn) throw new TypeORMError( `Referenced column ${joinColumn.referencedColumnName} was not found in entity ${relation.entityMetadata.name}`, ) return referencedColumn }) } } /** * Collects inverse referenced columns from the given join column args. */ protected collectInverseReferencedColumns( relation: RelationMetadata, joinTable: JoinTableMetadataArgs, ): ColumnMetadata[] { const hasInverseJoinColumns = !!joinTable.inverseJoinColumns const hasAnyInverseReferencedColumnName = hasInverseJoinColumns ? joinTable.inverseJoinColumns!.find( (joinColumn) => !!joinColumn.referencedColumnName, ) : false if ( !hasInverseJoinColumns || (hasInverseJoinColumns && !hasAnyInverseReferencedColumnName) ) { return relation.inverseEntityMetadata.primaryColumns } else { return joinTable.inverseJoinColumns!.map((joinColumn) => { const referencedColumn = relation.inverseEntityMetadata.ownColumns.find( (column) => column.propertyName === joinColumn.referencedColumnName, ) if (!referencedColumn) throw new TypeORMError( `Referenced column ${joinColumn.referencedColumnName} was not found in entity ${relation.inverseEntityMetadata.name}`, ) return referencedColumn }) } } protected changeDuplicatedColumnNames( junctionColumns: ColumnMetadata[], inverseJunctionColumns: ColumnMetadata[], ) { junctionColumns.forEach((junctionColumn) => { inverseJunctionColumns.forEach((inverseJunctionColumn) => { if ( junctionColumn.givenDatabaseName === inverseJunctionColumn.givenDatabaseName ) { const junctionColumnName = this.connection.namingStrategy.joinTableColumnDuplicationPrefix( junctionColumn.propertyName, 1, ) junctionColumn.propertyName = junctionColumnName junctionColumn.givenDatabaseName = junctionColumnName const inverseJunctionColumnName = this.connection.namingStrategy.joinTableColumnDuplicationPrefix( inverseJunctionColumn.propertyName, 2, ) inverseJunctionColumn.propertyName = inverseJunctionColumnName inverseJunctionColumn.givenDatabaseName = inverseJunctionColumnName } }) }) } }