mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added entity metadata validation
This commit is contained in:
parent
c43c2faa43
commit
a69cb3a5ff
35
sample/sample14-errors-in-wrong-metdata/app.ts
Normal file
35
sample/sample14-errors-in-wrong-metdata/app.ts
Normal 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));
|
||||
45
sample/sample14-errors-in-wrong-metdata/entity/Post.ts
Normal file
45
sample/sample14-errors-in-wrong-metdata/entity/Post.ts
Normal 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[];
|
||||
|
||||
}
|
||||
29
sample/sample14-errors-in-wrong-metdata/entity/PostAuthor.ts
Normal file
29
sample/sample14-errors-in-wrong-metdata/entity/PostAuthor.ts
Normal 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[];
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
69
src/metadata-storage/EntityMetadataValidator.ts
Normal file
69
src/metadata-storage/EntityMetadataValidator.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
22
src/metadata-storage/error/MissingJoinColumnError.ts
Normal file
22
src/metadata-storage/error/MissingJoinColumnError.ts
Normal 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.`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
23
src/metadata-storage/error/MissingJoinTableError.ts
Normal file
23
src/metadata-storage/error/MissingJoinTableError.ts
Normal 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.`;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user