fixed bug with many-to-many when not all junction operations were collected

This commit is contained in:
Umed Khudoiberdiev 2016-05-21 16:55:45 +05:00
parent 711e3a0698
commit cbdaf1acaf
7 changed files with 232 additions and 36 deletions

View File

@ -1,7 +1,7 @@
{
"name": "typeorm",
"private": true,
"version": "0.0.2-alpha.20",
"version": "0.0.2-alpha.23",
"description": "Data-mapper ORM for Typescript",
"license": "Apache-2.0",
"readmeFilename": "README.md",

View File

@ -312,42 +312,50 @@ export class EntityPersistOperationBuilder {
return operations;
}
private findJunctionInsertOperations(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[]): JunctionInsertOperation[] {
private findJunctionInsertOperations(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[], isRoot = true): JunctionInsertOperation[] {
const dbEntity = dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor === metadata.target;
});
return metadata.relations
.filter(relation => relation.isManyToMany)
// .filter(relation => newEntity[relation.propertyName] instanceof Array)
.filter(relation => newEntity[relation.propertyName] !== null && newEntity[relation.propertyName] !== undefined)
.reduce((operations, relation) => {
const relationMetadata = relation.inverseEntityMetadata;
const relationIdProperty = relationMetadata.primaryColumn.name;
const value = this.getEntityRelationValue(relation, newEntity);
const dbValue = dbEntity ? this.getEntityRelationValue(relation, dbEntity.entity) : null;
if (!(value instanceof Array))
return operations;
value.forEach((subEntity: any) => {
const has = !dbValue || !dbValue.find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (value instanceof Array) {
value.forEach((subEntity: any) => {
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: newEntity,
entity2: subEntity
});
if (relation.isManyToMany) {
const has = !dbValue || !dbValue.find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: newEntity,
entity2: subEntity
});
}
}
if (isRoot || this.checkCascadesAllowed("update", metadata, relation)) {
const subOperations = this.findJunctionInsertOperations(relationMetadata, subEntity, dbEntities, false);
operations.push(...subOperations);
}
});
} else {
if (isRoot || this.checkCascadesAllowed("update", metadata, relation)) {
const subOperations = this.findJunctionInsertOperations(relationMetadata, value, dbEntities, false);
operations.push(...subOperations);
}
}
const subOperations = this.findJunctionInsertOperations(relationMetadata, subEntity, dbEntities);
operations.push(...subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
}
private findJunctionRemoveOperations(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): JunctionInsertOperation[] {
private findJunctionRemoveOperations(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[], isRoot = true): JunctionInsertOperation[] {
if (!dbEntity) // if new entity is persisted then it does not have anything to be deleted
return [];
@ -355,32 +363,40 @@ export class EntityPersistOperationBuilder {
return newEntity.id === dbEntity[metadata.primaryColumn.name] && newEntity.entity.constructor === metadata.target;
});
return metadata.relations
.filter(relation => relation.isManyToMany)
// .filter(relation => dbEntity[relation.propertyName] instanceof Array)
.filter(relation => dbEntity[relation.propertyName] !== null && dbEntity[relation.propertyName] !== undefined)
.reduce((operations, relation) => {
const relationMetadata = relation.inverseEntityMetadata;
const relationIdProperty = relationMetadata.primaryColumn.name;
const value = newEntity ? this.getEntityRelationValue(relation, newEntity.entity) : null;
const dbValue = this.getEntityRelationValue(relation, dbEntity);
if (!(dbValue instanceof Array))
return operations;
dbValue.forEach((subEntity: any) => {
if (dbValue instanceof Array) {
dbValue.forEach((subEntity: any) => {
const has = !value || !value.find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (relation.isManyToMany) {
const has = !value || !value.find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: dbEntity,
entity2: subEntity
});
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: dbEntity,
entity2: subEntity
});
}
}
if (isRoot || this.checkCascadesAllowed("update", metadata, relation)) {
const subOperations = this.findJunctionRemoveOperations(relationMetadata, subEntity, newEntities, false);
operations.push(...subOperations);
}
});
} else {
if (isRoot || this.checkCascadesAllowed("update", metadata, relation)) {
const subOperations = this.findJunctionRemoveOperations(relationMetadata, dbValue, newEntities, false);
operations.push(...subOperations);
}
}
const subOperations = this.findJunctionRemoveOperations(relationMetadata, subEntity, newEntities);
operations.push(...subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,121 @@
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";
import {User} from "./entity/User";
chai.should();
chai.use(require("sinon-chai"));
chai.use(require("chai-as-promised"));
describe("persistence > many-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,
logging: {
logFailedQueryError: true
}
},
entities: [Post, Category, User]
};
// 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);
throw 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>;
let userRepository: Repository<User>;
before(function() {
postRepository = connection.getRepository(Post);
categoryRepository = connection.getRepository(Category);
userRepository = connection.getRepository(User);
});
// -------------------------------------------------------------------------
// Specifications
// -------------------------------------------------------------------------
describe("add exist element to exist object with empty one-to-many relation and save it", function() {
let newPost: Post, newCategory: Category, newUser: User, loadedUser: User;
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);
});
// save a new user
before(function() {
newUser = userRepository.create();
newUser.name = "Dima";
return userRepository.persist(newUser);
});
// now add a category to the post and attach post to a user and save a user
before(function() {
newPost.categories = [newCategory];
newUser.post = newPost;
return userRepository.persist(newUser);
});
// load a post
before(function() {
return userRepository
.findOneById(1, { alias: "user", leftJoinAndSelect: { post: "user.post", categories: "post.categories" } })
.then(post => loadedUser = post);
});
it("should contain a new category", function () {
expect(loadedUser).not.to.be.empty;
expect(loadedUser.post).not.to.be.empty;
expect(loadedUser.post.categories).not.to.be.empty;
});
});
});