import { EntityMetadata } from "../metadata/EntityMetadata" import { MissingPrimaryColumnError } from "../error/MissingPrimaryColumnError" import { CircularRelationsError } from "../error/CircularRelationsError" import { DepGraph } from "../util/DepGraph" import { Driver } from "../driver/Driver" import { DataTypeNotSupportedError } from "../error/DataTypeNotSupportedError" import { ColumnType } from "../driver/types/ColumnTypes" import { NoConnectionOptionError } from "../error/NoConnectionOptionError" import { InitializedRelationError } from "../error/InitializedRelationError" import { TypeORMError } from "../error" import { DriverUtils } from "../driver/DriverUtils" /// todo: add check if there are multiple tables with the same name /// todo: add checks when generated column / table names are too long for the specific driver // todo: type in function validation, inverse side function validation // todo: check on build for duplicate names, since naming checking was removed from MetadataStorage // todo: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns? // todo: check if multiple tree parent metadatas in validator // todo: tree decorators can be used only on closure table (validation) // todo: throw error if parent tree metadata was not specified in a closure table // todo: MetadataArgsStorage: type in function validation, inverse side function validation // todo: MetadataArgsStorage: check on build for duplicate names, since naming checking was removed from MetadataStorage // todo: MetadataArgsStorage: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns? // todo: MetadataArgsStorage: check for duplicate targets too since this check has been removed too // todo: check if relation decorator contains primary: true and nullable: true // todo: check column length, precision. scale // todo: MySQL index can be unique or spatial or fulltext /** * Validates built entity metadatas. */ export class EntityMetadataValidator { // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Validates all given entity metadatas. */ validateMany(entityMetadatas: EntityMetadata[], driver: Driver) { entityMetadatas.forEach((entityMetadata) => this.validate(entityMetadata, entityMetadatas, driver), ) this.validateDependencies(entityMetadatas) this.validateEagerRelations(entityMetadatas) } /** * Validates given entity metadata. */ validate( entityMetadata: EntityMetadata, allEntityMetadatas: EntityMetadata[], driver: Driver, ) { // check if table metadata has an id if (!entityMetadata.primaryColumns.length && !entityMetadata.isJunction) throw new MissingPrimaryColumnError(entityMetadata) // if entity has multiple primary keys and uses custom constraint name, // then all primary keys should have the same constraint name if (entityMetadata.primaryColumns.length > 1) { const areConstraintNamesEqual = entityMetadata.primaryColumns.every( (columnMetadata, i, columnMetadatas) => columnMetadata.primaryKeyConstraintName === columnMetadatas[0].primaryKeyConstraintName, ) if (!areConstraintNamesEqual) { throw new TypeORMError( `Entity ${entityMetadata.name} has multiple primary columns with different constraint names. Constraint names should be the equal.`, ) } } // validate if table is using inheritance it has a discriminator // also validate if discriminator values are not empty and not repeated if ( entityMetadata.inheritancePattern === "STI" || entityMetadata.tableType === "entity-child" ) { if (!entityMetadata.discriminatorColumn) throw new TypeORMError( `Entity ${entityMetadata.name} using single-table inheritance, it should also have a discriminator column. Did you forget to put discriminator column options?`, ) if (typeof entityMetadata.discriminatorValue === "undefined") throw new TypeORMError( `Entity ${entityMetadata.name} has an undefined discriminator value. Discriminator value should be defined.`, ) const sameDiscriminatorValueEntityMetadata = allEntityMetadatas.find((metadata) => { return ( metadata !== entityMetadata && (metadata.inheritancePattern === "STI" || metadata.tableType === "entity-child") && metadata.tableName === entityMetadata.tableName && metadata.discriminatorValue === entityMetadata.discriminatorValue && metadata.inheritanceTree.some( (parent) => entityMetadata.inheritanceTree.indexOf( parent, ) !== -1, ) ) }) if (sameDiscriminatorValueEntityMetadata) throw new TypeORMError( `Entities ${entityMetadata.name} and ${sameDiscriminatorValueEntityMetadata.name} have the same discriminator values. Make sure they are different while using the @ChildEntity decorator.`, ) } entityMetadata.relationCounts.forEach((relationCount) => { if ( relationCount.relation.isManyToOne || relationCount.relation.isOneToOne ) throw new TypeORMError( `Relation count can not be implemented on ManyToOne or OneToOne relations.`, ) }) if (!(driver.options.type === "mongodb")) { entityMetadata.columns .filter((column) => !column.isVirtualProperty) .forEach((column) => { const normalizedColumn = driver.normalizeType( column, ) as ColumnType if ( driver.supportedDataTypes.indexOf(normalizedColumn) === -1 ) throw new DataTypeNotSupportedError( column, normalizedColumn, driver.options.type, ) if ( column.length && driver.withLengthColumnTypes.indexOf( normalizedColumn, ) === -1 ) throw new TypeORMError( `Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`, ) if ( column.type === "enum" && !column.enum && !column.enumName ) throw new TypeORMError( `Column "${column.propertyName}" of Entity "${entityMetadata.name}" is defined as enum, but missing "enum" or "enumName" properties.`, ) }) } if ( DriverUtils.isMySQLFamily(driver) || driver.options.type === "aurora-mysql" ) { const generatedColumns = entityMetadata.columns.filter( (column) => column.isGenerated && column.generationStrategy !== "uuid", ) if (generatedColumns.length > 1) throw new TypeORMError( `Error in ${entityMetadata.name} entity. There can be only one auto-increment column in MySql table.`, ) } // for mysql we are able to not define a default selected database, instead all entities can have their database // defined in their decorators. To make everything work either all entities must have database define and we // can live without database set in the connection options, either database in the connection options must be set if (DriverUtils.isMySQLFamily(driver)) { const metadatasWithDatabase = allEntityMetadatas.filter( (metadata) => metadata.database, ) if (metadatasWithDatabase.length === 0 && !driver.database) throw new NoConnectionOptionError("database") } if (driver.options.type === "mssql") { const charsetColumns = entityMetadata.columns.filter( (column) => column.charset, ) if (charsetColumns.length > 1) throw new TypeORMError( `Character set specifying is not supported in Sql Server`, ) } // Postgres supports only STORED generated columns. if (driver.options.type === "postgres") { const virtualColumn = entityMetadata.columns.find( (column) => column.asExpression && (!column.generatedType || column.generatedType === "VIRTUAL"), ) if (virtualColumn) throw new TypeORMError( `Column "${virtualColumn.propertyName}" of Entity "${entityMetadata.name}" is defined as VIRTUAL, but Postgres supports only STORED generated columns.`, ) } // check if relations are all without initialized properties const entityInstance = entityMetadata.create(undefined, { fromDeserializer: true, }) entityMetadata.relations.forEach((relation) => { if (relation.isManyToMany || relation.isOneToMany) { // we skip relations for which persistence is disabled since initialization in them cannot harm somehow if (relation.persistenceEnabled === false) return // get entity relation value and check if its an array const relationInitializedValue = relation.getEntityValue(entityInstance) if (Array.isArray(relationInitializedValue)) throw new InitializedRelationError(relation) } }) // validate relations entityMetadata.relations.forEach((relation) => { // check OnDeleteTypes if ( driver.supportedOnDeleteTypes && relation.onDelete && !driver.supportedOnDeleteTypes.includes(relation.onDelete) ) { throw new TypeORMError( `OnDeleteType "${relation.onDelete}" is not supported for ${driver.options.type}!`, ) } // check OnUpdateTypes if ( driver.supportedOnUpdateTypes && relation.onUpdate && !driver.supportedOnUpdateTypes.includes(relation.onUpdate) ) { throw new TypeORMError( `OnUpdateType "${relation.onUpdate}" is not valid for ${driver.options.type}!`, ) } // check join tables: // using JoinTable is possible only on one side of the many-to-many relation // todo(dima): fix // if (relation.joinTable) { // if (!relation.isManyToMany) // throw new UsingJoinTableIsNotAllowedError(entityMetadata, relation); // // if there is inverse side of the relation, then check if it does not have join table too // if (relation.hasInverseSide && relation.inverseRelation.joinTable) // throw new UsingJoinTableOnlyOnOneSideAllowedError(entityMetadata, relation); // } // check join columns: // using JoinColumn is possible only on one side of the relation and on one-to-one, many-to-one relation types // first check if relation is one-to-one or many-to-one // todo(dima): fix /*if (relation.joinColumn) { // join column can be applied only on one-to-one and many-to-one relations if (!relation.isOneToOne && !relation.isManyToOne) throw new UsingJoinColumnIsNotAllowedError(entityMetadata, relation); // if there is inverse side of the relation, then check if it does not have join table too if (relation.hasInverseSide && relation.inverseRelation.joinColumn && relation.isOneToOne) throw new UsingJoinColumnOnlyOnOneSideAllowedError(entityMetadata, relation); // check if join column really has referenced column if (relation.joinColumn && !relation.joinColumn.referencedColumn) throw new TypeORMError(`Join column does not have referenced column set`); } // if its a one-to-one relation and JoinColumn is missing on both sides of the relation // or its one-side relation without JoinColumn we should give an error if (!relation.joinColumn && relation.isOneToOne && (!relation.hasInverseSide || !relation.inverseRelation.joinColumn)) throw new MissingJoinColumnError(entityMetadata, relation);*/ // if its a many-to-many relation and JoinTable is missing on both sides of the relation // or its one-side relation without JoinTable we should give an error // todo(dima): fix it // if (!relation.joinTable && relation.isManyToMany && (!relation.hasInverseSide || !relation.inverseRelation.joinTable)) // throw new MissingJoinTableError(entityMetadata, relation); // todo: validate if its one-to-one and side which does not have join column MUST have inverse side // todo: validate if its many-to-many and side which does not have join table MUST have inverse side // todo: if there is a relation, and inverse side is specified only on one side, shall we give error // todo: with message like: "Inverse side is specified only on one side of the relationship. Specify on other side too to prevent confusion". // todo: add validation if there two entities with the same target, and show error message with description of the problem (maybe file was renamed/moved but left in output directory) // todo: check if there are multiple columns on the same column applied. // todo: check column type if is missing in relational databases (throw new TypeORMError(`Column type of ${type} cannot be determined.`);) // todo: include driver-specific checks. for example in mongodb empty prefixes are not allowed // todo: if multiple columns with same name - throw exception, including cases when columns are in embeds with same prefixes or without prefix at all // todo: if multiple primary key used, at least one of them must be unique or @Index decorator must be set on entity // todo: check if entity with duplicate names, some decorators exist }) // make sure cascade remove is not set for both sides of relationships (can be set in OneToOne decorators) entityMetadata.relations.forEach((relation) => { const isCircularCascadeRemove = relation.isCascadeRemove && relation.inverseRelation && relation.inverseRelation!.isCascadeRemove if (isCircularCascadeRemove) throw new TypeORMError( `Relation ${entityMetadata.name}#${ relation.propertyName } and ${relation.inverseRelation!.entityMetadata.name}#${ relation.inverseRelation!.propertyName } both has cascade remove set. ` + `This may lead to unexpected circular removals. Please set cascade remove only from one side of relationship.`, ) }) // todo: maybe better just deny removal from one to one relation without join column? entityMetadata.eagerRelations.forEach((relation) => {}) } /** * Validates dependencies of the entity metadatas. */ protected validateDependencies(entityMetadatas: EntityMetadata[]) { const graph = new DepGraph() entityMetadatas.forEach((entityMetadata) => { graph.addNode(entityMetadata.name) }) entityMetadatas.forEach((entityMetadata) => { entityMetadata.relationsWithJoinColumns .filter((relation) => !relation.isNullable) .forEach((relation) => { graph.addDependency( entityMetadata.name, relation.inverseEntityMetadata.name, ) }) }) try { graph.overallOrder() } catch (err) { throw new CircularRelationsError( err.toString().replace("Error: Dependency Cycle Found: ", ""), ) } } /** * Validates eager relations to prevent circular dependency in them. */ protected validateEagerRelations(entityMetadatas: EntityMetadata[]) { entityMetadatas.forEach((entityMetadata) => { entityMetadata.eagerRelations.forEach((relation) => { if ( relation.inverseRelation && relation.inverseRelation.isEager ) throw new TypeORMError( `Circular eager relations are disallowed. ` + `${entityMetadata.targetName}#${relation.propertyPath} contains "eager: true", and its inverse side ` + `${relation.inverseEntityMetadata.targetName}#${relation.inverseRelation.propertyPath} contains "eager: true" as well.` + ` Remove "eager: true" from one side of the relation.`, ) }) }) } }