added entity metadata validation

This commit is contained in:
Umed Khudoiberdiev 2016-04-29 12:36:57 +05:00
parent c43c2faa43
commit a69cb3a5ff
14 changed files with 316 additions and 28 deletions

View File

@ -0,0 +1,35 @@
import {CreateConnectionOptions, createConnection} from "../../src/typeorm";
import {Post} from "./entity/Post";
import {PostAuthor} from "./entity/PostAuthor";
const options: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true
},
entities: [Post, PostAuthor]
};
createConnection(options).then(connection => {
let author = new PostAuthor();
author.name = "Umed";
let post = new Post();
post.text = "Hello how are you?";
post.title = "hello";
post.author = author;
let postRepository = connection.getRepository(Post);
postRepository
.persist(post)
.then(post => console.log("Post has been saved"))
.catch(error => console.log("Cannot save. Error: ", error));
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,45 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {OneToOne} from "../../../src/relations";
import {PostAuthor} from "./PostAuthor";
import {JoinColumn} from "../../../src/decorator/relations/JoinColumn";
import {ManyToOne} from "../../../src/decorator/relations/ManyToOne";
import {OneToMany} from "../../../src/decorator/relations/OneToMany";
import {JoinTable} from "../../../src/decorator/relations/JoinTable";
import {ManyToMany} from "../../../src/decorator/relations/ManyToMany";
@Table("sample14_post")
export class Post {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
title: string;
@Column()
text: string;
@OneToOne(type => PostAuthor, author => author.post, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinColumn() // comment this and you'll get an error because JoinColumn must be at least on one side of the one-to-one relationship
// @JoinTable() // uncomment this and you'll get an error because JoinTable is not allowed here (only many-to-many)
author: PostAuthor;
@OneToMany(type => PostAuthor, author => author.editedPost, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
// @JoinColumn() // uncomment this and you'll get an error, because JoinColumn is not allowed here (only many-to-one/one-to-one)
// @JoinTable() // uncomment this and you'll get an error because JoinTable is not allowed here (only many-to-many)
editors: PostAuthor[];
@ManyToMany(type => PostAuthor, author => author.manyPosts)
@JoinTable() // comment this and you'll get an error because JoinTable must be at least on one side of the many-to-many relationship
manyAuthors: PostAuthor[];
}

View File

@ -0,0 +1,29 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {Post} from "./Post";
import {OneToOne} from "../../../src/relations";
import {ManyToOne} from "../../../src/decorator/relations/ManyToOne";
import {ManyToMany} from "../../../src/decorator/relations/ManyToMany";
@Table("sample14_post_author")
export class PostAuthor {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
name: string;
@OneToOne(type => Post, post => post.author)
// @JoinColumn() // uncomment this and it will case an error, because JoinColumn is allowed only on one side
post: Post;
@ManyToOne(type => Post, post => post.editors)
// @JoinColumn() // JoinColumn is optional here, so if it present or not you should not get an error
editedPost: Post;
@ManyToMany(type => Post, post => post.manyAuthors)
// @JoinTable() // uncomment this and it will case an error, because only one side of the ManyToMany relation can have a JoinTable decorator.
manyPosts: Post[];
}

View File

@ -2,6 +2,7 @@ import {PrimaryColumn, Column} from "../../../src/columns";
import {Table} from "../../../src/tables";
import {ManyToMany} from "../../../src/relations";
import {Post} from "./Post";
import {JoinTable} from "../../../src/decorator/relations/JoinTable";
@Table("sample4_post_details")
export class PostDetails {

View File

@ -1,5 +1,4 @@
import {MetadataStorage} from "./MetadataStorage";
import {PropertyMetadata} from "../metadata/PropertyMetadata";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
@ -7,8 +6,13 @@ import {ColumnOptions} from "../metadata/options/ColumnOptions";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {JunctionTableMetadata} from "../metadata/JunctionTableMetadata";
import {defaultMetadataStorage} from "../typeorm";
import {TableMetadata} from "../metadata/TableMetadata";
import {TargetMetadataCollection} from "../metadata/collection/TargetMetadataCollection";
import {UsingJoinTableIsNotAllowedError} from "./error/UsingJoinTableIsNotAllowedError";
import {UsingJoinTableOnlyOnOneSideAllowedError} from "./error/UsingJoinTableOnlyOnOneSideAllowedError";
import {UsingJoinColumnIsNotAllowedError} from "./error/UsingJoinColumnIsNotAllowedError";
import {UsingJoinColumnOnlyOnOneSideAllowedError} from "./error/UsingJoinColumnOnlyOnOneSideAllowedError";
import {MissingJoinColumnError} from "./error/MissingJoinColumnError";
import {MissingJoinTableError} from "./error/MissingJoinTableError";
import {EntityMetadataValidator} from "./EntityMetadataValidator";
/**
* Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes.
@ -23,6 +27,7 @@ export class EntityMetadataBuilder {
// todo: duplicate name checking for: table, relation, column, index, naming strategy, join tables/columns?
private metadataStorage: MetadataStorage = defaultMetadataStorage();
private entityValidator = new EntityMetadataValidator();
// -------------------------------------------------------------------------
// Constructor
@ -46,8 +51,6 @@ export class EntityMetadataBuilder {
const allTableMetadatas = allMetadataStorage.tableMetadatas.filterByClasses(entityClasses);
const tableMetadatas = allTableMetadatas.filterByClasses(entityClasses).filter(table => !table.isAbstract);
// const abstractTableMetadatas = allTableMetadatas.filterByClasses(entityClasses).filter(table => table.isAbstract);
const entityMetadatas = tableMetadatas.map(tableMetadata => {
const mergedMetadata = allMetadataStorage.mergeWithAbstract(allTableMetadatas, tableMetadata);
@ -78,15 +81,20 @@ export class EntityMetadataBuilder {
tableMetadata.namingStrategy = this.namingStrategy;
entityMetadata.columns.forEach(column => column.namingStrategy = this.namingStrategy);
entityMetadata.relations.forEach(relation => relation.namingStrategy = this.namingStrategy);
// check if table metadata has an id
if (!entityMetadata.primaryColumn)
throw new Error(`Entity "${entityMetadata.name}" (table "${tableMetadata.name}") does not have a primary column. Primary column is required to have in all your entities. Use @PrimaryColumn decorator to add a primary column to your entity.`);
return entityMetadata;
});
// set inverse side (related) entity metadatas for all relation metadatas
entityMetadatas.forEach(entityMetadata => {
entityMetadata.relations.forEach(relation => {
relation.relatedEntityMetadata = entityMetadatas.find(m => m.target === relation.type);
});
});
// check for errors in build metadata schema (we need to check after relationEntityMetadata is set)
this.entityValidator.validateMany(entityMetadatas);
// generate columns and foreign keys for tables with relations
entityMetadatas.forEach(metadata => {
const foreignKeyRelations = metadata.ownerOneToOneRelations.concat(metadata.manyToOneRelations);
@ -122,13 +130,6 @@ export class EntityMetadataBuilder {
});
});
// set inverse side (related) entity metadatas for all relation metadatas
entityMetadatas.forEach(entityMetadata => {
entityMetadata.relations.forEach(relation => {
relation.relatedEntityMetadata = entityMetadatas.find(m => m.target === relation.type);
});
});
// generate junction tables with its columns and foreign keys
const junctionEntityMetadatas: EntityMetadata[] = [];
entityMetadatas.forEach(metadata => {
@ -174,13 +175,4 @@ export class EntityMetadataBuilder {
return entityMetadatas.concat(junctionEntityMetadatas);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private filterRepeatedMetadatas<T extends PropertyMetadata>(newMetadatas: T[], existsMetadatas: T[]): T[] {
return newMetadatas.filter(fieldFromMapped => {
return !!existsMetadatas.find(fieldFromDocument => fieldFromDocument.propertyName === fieldFromMapped.propertyName);
});
}
}

View File

@ -0,0 +1,69 @@
import {UsingJoinTableIsNotAllowedError} from "./error/UsingJoinTableIsNotAllowedError";
import {UsingJoinTableOnlyOnOneSideAllowedError} from "./error/UsingJoinTableOnlyOnOneSideAllowedError";
import {UsingJoinColumnIsNotAllowedError} from "./error/UsingJoinColumnIsNotAllowedError";
import {UsingJoinColumnOnlyOnOneSideAllowedError} from "./error/UsingJoinColumnOnlyOnOneSideAllowedError";
import {MissingJoinColumnError} from "./error/MissingJoinColumnError";
import {MissingJoinTableError} from "./error/MissingJoinTableError";
import {EntityMetadata} from "../metadata/EntityMetadata";
/**
* Validates built entity metadatas.
*
* @internal
*/
export class EntityMetadataValidator {
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
validateMany(entityMetadatas: EntityMetadata[]) {
entityMetadatas.forEach(entityMetadata => this.validate(entityMetadata));
}
validate(entityMetadata: EntityMetadata) {
// check if table metadata has an id
if (!entityMetadata.primaryColumn)
throw new Error(`Entity "${entityMetadata.name}" does not have a primary column. Primary column is required to have in all your entities. Use @PrimaryColumn decorator to add a primary column to your entity.`);
entityMetadata.relations.forEach(relation => {
// check join tables:
// using JoinTable is possible only on one side of the many-to-many relation
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
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);
}
// 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.inverseRelation || !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
if (!relation.joinTable && relation.isManyToMany && (!relation.inverseRelation || !relation.inverseRelation.joinTable))
throw new MissingJoinTableError(entityMetadata, relation);
});
}
}

View File

@ -7,7 +7,8 @@ export class MetadataAlreadyExistsError extends Error {
constructor(metadataType: string, constructor: Function, propertyName?: string) {
super();
this.message = metadataType + " metadata already exists for the class constructor " + JSON.stringify(constructor) +
(propertyName ? " on property " + propertyName : "");
(propertyName ? " on property " + propertyName : ". If you previously renamed or moved entity class, make sure" +
" that compiled version of old entity class source wasn't left in the compiler output directory.");
}
}

View File

@ -0,0 +1,22 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class MissingJoinColumnError extends Error {
name = "MissingJoinColumnError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
if (relation.inverseRelation) {
this.message = `JoinColumn is missing on both sides of ${entityMetadata.name}#${relation.name} and ` +
`${relation.relatedEntityMetadata.name}#${relation.inverseRelation.name} one-to-one relationship. ` +
`You need to put JoinColumn decorator on one of the sides.`;
} else {
this.message = `JoinColumn is missing on ${entityMetadata.name}#${relation.name} one-to-one relationship. ` +
`You need to put JoinColumn decorator on it.`;
}
}
}

View File

@ -0,0 +1,23 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class MissingJoinTableError extends Error {
name = "MissingJoinTableError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
if (relation.inverseRelation) {
this.message = `JoinTable is missing on both sides of ${entityMetadata.name}#${relation.name} and ` +
`${relation.relatedEntityMetadata.name}#${relation.inverseRelation.name} many-to-many relationship. ` +
`You need to put decorator decorator on one of the sides.`;
} else {
this.message = `JoinTable is missing on ${entityMetadata.name}#${relation.name} many-to-many relationship. ` +
`You need to put JoinTable decorator on it.`;
}
}
}

View File

@ -0,0 +1,16 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class UsingJoinColumnIsNotAllowedError extends Error {
name = "UsingJoinColumnIsNotAllowedError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinColumn on ${entityMetadata.name}#${relation.name} is wrong. ` +
`You can use JoinColumn only on one-to-one and many-to-one relations.`;
}
}

View File

@ -0,0 +1,17 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class UsingJoinColumnOnlyOnOneSideAllowedError extends Error {
name = "UsingJoinColumnOnlyOnOneSideAllowedError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinColumn is allowed only on one side of the one-to-one relationship. ` +
`Both ${entityMetadata.name}#${relation.name} and ${relation.relatedEntityMetadata.name}#${relation.inverseRelation.name} ` +
`has JoinTable decorators. Choose one of them and left JoinTable decorator only on it.`;
}
}

View File

@ -0,0 +1,17 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class UsingJoinTableIsNotAllowedError extends Error {
name = "UsingJoinTableIsNotAllowedError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinTable on ${entityMetadata.name}#${relation.name} is wrong. ` +
`${entityMetadata.name}#${relation.name} has ${relation.relationType} relation, ` +
`however you can use JoinTable only on many-to-many relations.`;
}
}

View File

@ -0,0 +1,17 @@
import {EntityMetadata} from "../../metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata/RelationMetadata";
/**
* @internal
*/
export class UsingJoinTableOnlyOnOneSideAllowedError extends Error {
name = "UsingJoinTableOnlyOnOneSideAllowedError";
constructor(entityMetadata: EntityMetadata, relation: RelationMetadata) {
super();
this.message = `Using JoinTable is allowed only on one side of the many-to-many relationship. ` +
`Both ${entityMetadata.name}#${relation.name} and ${relation.relatedEntityMetadata.name}#${relation.inverseRelation.name} ` +
`has JoinTable decorators. Choose one of them and left JoinColumn decorator only on it.`;
}
}

View File

@ -184,6 +184,10 @@ export class RelationMetadata extends PropertyMetadata {
get isManyToMany(): boolean {
return this.relationType === RelationTypes.MANY_TO_MANY;
}
get hasInverseSide(): boolean {
return !!this.inverseRelation;
}
// ---------------------------------------------------------------------
// Private Methods