added functional tests for one-to-many and fixed few issues related to one-to-many

This commit is contained in:
Umed Khudoiberdiev 2016-05-21 15:07:32 +05:00
parent 6177c1929b
commit 711e3a0698
9 changed files with 450 additions and 25 deletions

View File

@ -137,6 +137,18 @@ export class Gulpfile {
.pipe(mocha());
}
/**
* Runs functional tests.
*/
@Task()
functional() {
chai.should();
chai.use(require("sinon-chai"));
chai.use(require("chai-as-promised"));
return gulp.src("./build/es5/test/functional/**/*.js")
.pipe(mocha());
}
/**
* Runs unit-tests.
*/
@ -153,7 +165,7 @@ export class Gulpfile {
*/
@SequenceTask()
tests() {
return ["compile", "tslint", "unit", "integration"];
return ["compile", "tslint", "unit", "integration", "functional"];
}
}

View File

@ -229,20 +229,37 @@ export class EntityPersistOperationBuilder {
operations: UpdateByInverseSideOperation[] = []): UpdateByInverseSideOperation[] {
metadata.relations
.filter(relation => relation.isOneToMany) // todo: maybe need to check isOneToOne and not owner
.filter(relation => newEntity[relation.propertyName] instanceof Array) // todo: what to do with empty relations? need to set to NULL from inverse side?
// .filter(relation => newEntity[relation.propertyName] instanceof Array) // todo: what to do with empty relations? need to set to NULL from inverse side?
.forEach(relation => {
// to find new objects in relation go throw all objects in newEntity and check if they don't exist in dbEntity
newEntity[relation.propertyName].filter((subEntity: any) => {
if (!dbEntity /* are you sure about this? */ || !dbEntity[relation.propertyName]) // if there is no items in dbEntity - then all items in newEntity are new
return true;
return !dbEntity[relation.propertyName].find((dbSubEntity: any) => {
return relation.inverseEntityMetadata.getEntityId(subEntity) === relation.inverseEntityMetadata.getEntityId(dbSubEntity);
if (newEntity && newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].filter((newSubEntity: any) => {
if (!dbEntity /* are you sure about this? */ || !dbEntity[relation.propertyName]) // if there are no items in dbEntity - then all items in newEntity are new
return true;
return !dbEntity[relation.propertyName].find((dbSubEntity: any) => {
return relation.inverseEntityMetadata.getEntityId(newSubEntity) === relation.inverseEntityMetadata.getEntityId(dbSubEntity);
});
}).forEach((subEntity: any) => {
operations.push(new UpdateByInverseSideOperation("update", subEntity, newEntity, relation));
});
}).forEach((subEntity: any) => {
operations.push(new UpdateByInverseSideOperation(subEntity, newEntity, relation));
});
}
// we also need to find removed elements. to find them need to traverse dbEntity and find its elements missing in newEntity
if (dbEntity && dbEntity[relation.propertyName] instanceof Array) {
dbEntity[relation.propertyName].filter((dbSubEntity: any) => {
if (!newEntity /* are you sure about this? */ || !newEntity[relation.propertyName]) // if there are no items in newEntity - then all items in dbEntity are removed
return true;
return !newEntity[relation.propertyName].find((newSubEntity: any) => {
return relation.inverseEntityMetadata.getEntityId(dbSubEntity) === relation.inverseEntityMetadata.getEntityId(newSubEntity);
});
}).forEach((subEntity: any) => {
operations.push(new UpdateByInverseSideOperation("remove", subEntity, newEntity, relation));
});
}
});
return operations;
@ -371,7 +388,7 @@ export class EntityPersistOperationBuilder {
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual && !column.isUpdateDate && !column.isVersion && !column.isCreateDate)
.filter(column => newEntity[column.propertyName] !== dbEntity[column.name]);
.filter(column => newEntity[column.propertyName] !== dbEntity[column.propertyName]);
}
private diffRelations(updatesByRelations: UpdateByRelationOperation[], metadata: EntityMetadata, newEntity: any, dbEntity: any) {
@ -379,14 +396,14 @@ export class EntityPersistOperationBuilder {
.filter(relation => relation.isManyToOne || (relation.isOneToOne && relation.isOwning))
.filter(relation => !updatesByRelations.find(operation => operation.targetEntity === newEntity && operation.updatedRelation === relation)) // try to find if there is update by relation operation - we dont need to generate update relation operation for this
.filter(relation => {
if (!newEntity[relation.propertyName] && !dbEntity[relation.name])
if (!newEntity[relation.propertyName] && !dbEntity[relation.propertyName])
return false;
if (!newEntity[relation.propertyName] || !dbEntity[relation.name])
if (!newEntity[relation.propertyName] || !dbEntity[relation.propertyName])
return true;
const newEntityRelationMetadata = this.entityMetadatas.findByTarget(newEntity[relation.propertyName].constructor);
const dbEntityRelationMetadata = this.entityMetadatas.findByTarget(dbEntity[relation.name].constructor);
return newEntityRelationMetadata.getEntityId(newEntity[relation.propertyName]) !== dbEntityRelationMetadata.getEntityId(dbEntity[relation.name]);
const dbEntityRelationMetadata = this.entityMetadatas.findByTarget(dbEntity[relation.propertyName].constructor);
return newEntityRelationMetadata.getEntityId(newEntity[relation.propertyName]) !== dbEntityRelationMetadata.getEntityId(dbEntity[relation.propertyName]);
});
}

View File

@ -176,7 +176,7 @@ export class PersistOperationExecutor {
*/
private executeUpdateInverseRelationsOperations(persistOperation: PersistOperation) {
return Promise.all(persistOperation.updatesByInverseRelations.map(updateInverseOperation => {
return this.updateInverseRelation(updateInverseOperation);
return this.updateInverseRelation(updateInverseOperation, persistOperation.inserts);
}));
}
@ -312,7 +312,7 @@ export class PersistOperationExecutor {
return this.driver.update(tableName, { [relationName]: relationId }, { [idColumn]: id });
}
private updateInverseRelation(operation: UpdateByInverseSideOperation) {
private updateInverseRelation(operation: UpdateByInverseSideOperation, insertOperations: InsertOperation[]) {
/*let tableName: string, relationName: string, relationId: any, idColumn: string, id: any;
const relatedInsertOperation = insertOperations.find(o => o.entity === operation.targetEntity);
const idInInserts = relatedInsertOperation ? relatedInsertOperation.entityId : null;
@ -332,12 +332,25 @@ export class PersistOperationExecutor {
idColumn = metadata.primaryColumn.name;
id = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts;
}*/
const targetEntityMetadata = this.entityMetadatas.findByTarget(operation.targetEntity.constructor);
const fromEntityMetadata = this.entityMetadatas.findByTarget(operation.fromEntity.constructor);
const tableName = targetEntityMetadata.table.name;
const targetRelation = operation.fromRelation.inverseRelation;
const targetEntityId = operation.fromEntity[targetRelation.joinColumn.referencedColumn.name];
const idColumn = targetEntityMetadata.primaryColumn.name;
const id = targetEntityMetadata.getEntityId(operation.targetEntity);
const fromEntityInsertOperation = insertOperations.find(o => o.entity === operation.fromEntity);
let targetEntityId: any; // todo: better do it during insertion - pass UpdateByInverseSideOperation[] to insert and do it there
if (operation.operationType === "remove") {
targetEntityId = null;
} else {
if (fromEntityInsertOperation && targetRelation.joinColumn.referencedColumn === fromEntityMetadata.primaryColumn) {
targetEntityId = fromEntityInsertOperation.entityId;
} else {
targetEntityId = operation.fromEntity[targetRelation.joinColumn.referencedColumn.name];
}
}
return this.driver.update(tableName, { [targetRelation.name]: targetEntityId }, { [idColumn]: id });
}

View File

@ -4,7 +4,8 @@ import {RelationMetadata} from "../../metadata/RelationMetadata";
* @internal
*/
export class UpdateByInverseSideOperation {
constructor(public targetEntity: any,
constructor(public operationType: "update"|"remove",
public targetEntity: any,
public fromEntity: any,
public fromRelation: RelationMetadata) {
}

View File

@ -32,24 +32,24 @@ import {ConnectionOptions} from "../connection/ConnectionOptions";
* user: "categories.user",
* profile: "user.profile"
* },
* innerJoin: [
* innerJoin: {
* author: "photo.author",
* categories: "categories",
* user: "categories.user",
* profile: "user.profile"
* ],
* },
* leftJoinAndSelect: {
* author: "photo.author",
* categories: "categories",
* user: "categories.user",
* profile: "user.profile"
* },
* innerJoinAndSelect: [
* innerJoinAndSelect: {
* author: "photo.author",
* categories: "categories",
* user: "categories.user",
* profile: "user.profile"
* ]
* }
* };
*/
export interface FindOptions {

View File

@ -0,0 +1,19 @@
import {Table} from "../../../../../src/decorator/tables/Table";
import {PrimaryColumn} from "../../../../../src/decorator/columns/PrimaryColumn";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {Post} from "./Post";
import {Column} from "../../../../../src/decorator/columns/Column";
@Table()
export class Category {
@PrimaryColumn("int", { generated: true })
id: number;
@ManyToOne(type => Post, post => post.categories)
post: Post;
@Column()
name: string;
}

View File

@ -0,0 +1,19 @@
import {Category} from "./Category";
import {Table} from "../../../../../src/decorator/tables/Table";
import {PrimaryColumn} from "../../../../../src/decorator/columns/PrimaryColumn";
import {OneToMany} from "../../../../../src/decorator/relations/OneToMany";
import {Column} from "../../../../../src/decorator/columns/Column";
@Table()
export class Post {
@PrimaryColumn("int", { generated: true })
id: number;
@OneToMany(type => Category, category => category.post)
categories: Category[]|null;
@Column()
title: string;
}

View File

@ -0,0 +1,304 @@
import "reflect-metadata";
import * as chai from "chai";
import {expect} from "chai";
import {Connection} from "../../../../src/connection/Connection";
import {Repository} from "../../../../src/repository/Repository";
import {Post} from "./entity/Post";
import {Category} from "./entity/Category";
import {CreateConnectionOptions} from "../../../../src/connection-manager/CreateConnectionOptions";
import {createConnection} from "../../../../src/typeorm";
chai.should();
chai.use(require("sinon-chai"));
chai.use(require("chai-as-promised"));
describe("persistence > one-to-many", function() {
// -------------------------------------------------------------------------
// Configuration
// -------------------------------------------------------------------------
const parameters: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true
},
entities: [Post, Category]
};
// connect to db
let connection: Connection;
before(function() {
return createConnection(parameters)
.then(con => connection = con)
.catch(e => console.log("Error during connection to db: " + e));
});
after(function() {
connection.close();
});
// clean up database before each test
function reloadDatabase() {
return connection.driver
.clearDatabase()
.then(() => connection.syncSchema())
.catch(e => console.log("Error during schema re-creation: ", e));
}
let postRepository: Repository<Post>;
let categoryRepository: Repository<Category>;
before(function() {
postRepository = connection.getRepository(Post);
categoryRepository = connection.getRepository(Category);
});
// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------
describe("add exist element to exist object with empty one-to-many relation and save it", function() {
let newPost: Post, newCategory: Category, loadedPost: Post;
before(reloadDatabase);
// save a new category
before(function () {
newCategory = categoryRepository.create();
newCategory.name = "Animals";
return categoryRepository.persist(newCategory);
});
// save a new post
before(function() {
newPost = postRepository.create();
newPost.title = "All about animals";
return postRepository.persist(newPost);
});
// add category to post and save it
before(function() {
newPost.categories = [newCategory];
return postRepository.persist(newPost);
});
// load a post and join its category
before(function() {
return postRepository
.findOneById(1, { alias: "post", innerJoinAndSelect: { categories: "post.categories" } })
.then(post => loadedPost = post);
});
it("should contain a new category", function () {
expect(loadedPost).not.to.be.empty;
expect(loadedPost.categories).not.to.be.empty;
if (loadedPost.categories) {
expect(loadedPost.categories[0]).not.to.be.empty;
}
});
});
describe("add exist element to new object with empty one-to-many relation and save it", function() {
let newPost: Post, newCategory: Category, loadedPost: Post;
before(reloadDatabase);
// save a new category
before(function () {
newCategory = categoryRepository.create();
newCategory.name = "Animals";
return categoryRepository.persist(newCategory);
});
// save a new post
before(function() {
newPost = postRepository.create();
newPost.title = "All about animals";
newPost.categories = [newCategory];
return postRepository.persist(newPost);
});
// load a post and join its category
before(function() {
return postRepository
.findOneById(1, { alias: "post", innerJoinAndSelect: { categories: "post.categories" } })
.then(post => loadedPost = post);
});
it("should contain a new element", function () {
expect(loadedPost).not.to.be.empty;
expect(loadedPost.categories).not.to.be.empty;
if (loadedPost.categories) {
expect(loadedPost.categories[0]).not.to.be.empty;
}
});
});
describe("remove exist element from one-to-many relation and save it", function() {
let newPost: Post, firstNewCategory: Category, secondNewCategory: Category, loadedPost: Post;
before(reloadDatabase);
// save a new category
before(function () {
firstNewCategory = categoryRepository.create();
firstNewCategory.name = "Animals";
return categoryRepository.persist(firstNewCategory);
});
// save a second category
before(function () {
secondNewCategory = categoryRepository.create();
secondNewCategory.name = "Insects";
return categoryRepository.persist(secondNewCategory);
});
// save a new post
before(function() {
newPost = postRepository.create();
newPost.title = "All about animals";
return postRepository.persist(newPost);
});
// add categories to post and save it
before(function() {
newPost.categories = [firstNewCategory, secondNewCategory];
return postRepository.persist(newPost);
});
// remove one of the categories and save it
before(function() {
newPost.categories = [firstNewCategory];
return postRepository.persist(newPost);
});
// load a post and join its category
before(function() {
return postRepository
.findOneById(1, { alias: "post", innerJoinAndSelect: { categories: "post.categories" } })
.then(post => loadedPost = post);
});
it("should have only one category", function () {
expect(loadedPost).not.to.be.empty;
expect(loadedPost.categories).not.to.be.empty;
if (loadedPost.categories) {
expect(loadedPost.categories[0]).not.to.be.empty;
expect(loadedPost.categories[1]).to.be.empty;
}
});
});
describe("remove all elements from one-to-many relation and save it", function() {
let newPost: Post, firstNewCategory: Category, secondNewCategory: Category, loadedPost: Post;
before(reloadDatabase);
// save a new category
before(function () {
firstNewCategory = categoryRepository.create();
firstNewCategory.name = "Animals";
return categoryRepository.persist(firstNewCategory);
});
// save a second category
before(function () {
secondNewCategory = categoryRepository.create();
secondNewCategory.name = "Insects";
return categoryRepository.persist(secondNewCategory);
});
// save a new post
before(function() {
newPost = postRepository.create();
newPost.title = "All about animals";
return postRepository.persist(newPost);
});
// add categories to post and save it
before(function() {
newPost.categories = [firstNewCategory, secondNewCategory];
return postRepository.persist(newPost);
});
// remove one of the categories and save it
before(function() {
newPost.categories = [];
return postRepository.persist(newPost);
});
// load a post and join its category
before(function() {
return postRepository
.findOneById(1, { alias: "post", leftJoinAndSelect: { categories: "post.categories" } })
.then(post => loadedPost = post);
});
it("should not have categories since they all are removed", function () {
expect(loadedPost).not.to.be.empty;
expect(loadedPost.categories).to.be.empty;
});
});
describe("set relation to null (elements exist there) from one-to-many relation and save it", function() {
let newPost: Post, firstNewCategory: Category, secondNewCategory: Category, loadedPost: Post;
before(reloadDatabase);
// save a new category
before(function () {
firstNewCategory = categoryRepository.create();
firstNewCategory.name = "Animals";
return categoryRepository.persist(firstNewCategory);
});
// save a second category
before(function () {
secondNewCategory = categoryRepository.create();
secondNewCategory.name = "Insects";
return categoryRepository.persist(secondNewCategory);
});
// save a new post
before(function() {
newPost = postRepository.create();
newPost.title = "All about animals";
return postRepository.persist(newPost);
});
// add categories to post and save it
before(function() {
newPost.categories = [firstNewCategory, secondNewCategory];
return postRepository.persist(newPost);
});
// remove one of the categories and save it
before(function() {
newPost.categories = null; // todo: what to do with undefined?
return postRepository.persist(newPost);
});
// load a post and join its category
before(function() {
return postRepository
.findOneById(1, { alias: "post", leftJoinAndSelect: { categories: "post.categories" } })
.then(post => loadedPost = post);
});
it("should not have categories since they all are removed", function () {
expect(loadedPost).not.to.be.empty;
expect(loadedPost.categories).to.be.empty;
});
});
});

40
test/utils/utils.ts Normal file
View File

@ -0,0 +1,40 @@
import {CreateConnectionOptions} from "../../src/connection-manager/CreateConnectionOptions";
import {createConnection} from "../../src/typeorm";
import {Connection} from "../../src/connection/Connection";
export function setupConnection(entities: Function[], callback?: (connection: Connection) => any) {
const parameters: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true
},
entities: entities
};
return function() {
console.log("creating connection");
return createConnection(parameters)
.then(connection => {
console.log("connection");
if (callback)
callback(connection);
return connection;
})
.catch(e => console.log("Error during connection to db: " + e));
};
}
export function reloadDatabase(connection: Connection) {
return function () {
return connection.driver
.clearDatabase()
.then(() => connection.syncSchema())
.catch(e => console.log("Error during schema re-creation: ", e));
};
}