diff --git a/package.json b/package.json index 2fade5425..5f3e95536 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/persistment/EntityPersistOperationsBuilder.ts b/src/persistment/EntityPersistOperationsBuilder.ts index 62ffaebb4..63f41c9f8 100644 --- a/src/persistment/EntityPersistOperationsBuilder.ts +++ b/src/persistment/EntityPersistOperationsBuilder.ts @@ -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; }, []); } - 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; }, []); } diff --git a/test/functional/persistence/many-to-many/entity/Category.ts b/test/functional/persistence/many-to-many/entity/Category.ts new file mode 100644 index 000000000..d36d60b35 --- /dev/null +++ b/test/functional/persistence/many-to-many/entity/Category.ts @@ -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[]; + +} \ No newline at end of file diff --git a/test/functional/persistence/many-to-many/entity/Post.ts b/test/functional/persistence/many-to-many/entity/Post.ts new file mode 100644 index 000000000..4d18bcf35 --- /dev/null +++ b/test/functional/persistence/many-to-many/entity/Post.ts @@ -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; + +} \ No newline at end of file diff --git a/test/functional/persistence/many-to-many/entity/User.ts b/test/functional/persistence/many-to-many/entity/User.ts new file mode 100644 index 000000000..bc22679ed --- /dev/null +++ b/test/functional/persistence/many-to-many/entity/User.ts @@ -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; + +} \ No newline at end of file diff --git a/test/functional/persistence/many-to-many/persistence-many-to-many.ts b/test/functional/persistence/many-to-many/persistence-many-to-many.ts new file mode 100644 index 000000000..7976c1964 --- /dev/null +++ b/test/functional/persistence/many-to-many/persistence-many-to-many.ts @@ -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; + let categoryRepository: Repository; + let userRepository: Repository; + 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; + }); + + }); + +}); \ No newline at end of file diff --git a/test/functional/persistence/one-to-many/persistment-one-to-many.ts b/test/functional/persistence/one-to-many/persistence-one-to-many.ts similarity index 100% rename from test/functional/persistence/one-to-many/persistment-one-to-many.ts rename to test/functional/persistence/one-to-many/persistence-one-to-many.ts