diff --git a/sample/sample3-many-to-one/entity/Post.ts b/sample/sample3-many-to-one/entity/Post.ts index 710f784f0..5d8a11c48 100644 --- a/sample/sample3-many-to-one/entity/Post.ts +++ b/sample/sample3-many-to-one/entity/Post.ts @@ -45,7 +45,7 @@ export class Post { @ManyToOne(type => PostMetadata, metadata => metadata.posts, { cascadeRemove: true }) - metadata: PostMetadata|undefined; + metadata: PostMetadata|null; // post has relation with details. full cascades here @ManyToOne(type => PostInformation, information => information.posts, { diff --git a/sample/sample3-many-to-one/entity/PostDetails.ts b/sample/sample3-many-to-one/entity/PostDetails.ts index e60abe7b7..f0fb0ec3a 100644 --- a/sample/sample3-many-to-one/entity/PostDetails.ts +++ b/sample/sample3-many-to-one/entity/PostDetails.ts @@ -27,6 +27,6 @@ export class PostDetails { cascadeUpdate: true, cascadeRemove: true }) - posts: Post[] = []; + posts: Post[]; } \ No newline at end of file diff --git a/sample/sample4-many-to-many/entity/Post.ts b/sample/sample4-many-to-many/entity/Post.ts index 65e11d488..06629f73a 100644 --- a/sample/sample4-many-to-many/entity/Post.ts +++ b/sample/sample4-many-to-many/entity/Post.ts @@ -26,7 +26,7 @@ export class Post { cascadeRemove: true }) @JoinTable() - categories: PostCategory[] = []; + categories: PostCategory[]; // post has relation with details. cascade inserts here means if new PostDetails instance will be set to this // relation it will be inserted automatically to the db when you save this Post entity @@ -34,7 +34,7 @@ export class Post { cascadeInsert: true }) @JoinTable() - details: PostDetails[] = []; + details: PostDetails[]; // post has relation with details. cascade update here means if new PostDetail instance will be set to this relation // it will be inserted automatically to the db when you save this Post entity @@ -42,7 +42,7 @@ export class Post { cascadeUpdate: true }) @JoinTable() - images: PostImage[] = []; + images: PostImage[]; // post has relation with details. cascade update here means if new PostDetail instance will be set to this relation // it will be inserted automatically to the db when you save this Post entity @@ -50,7 +50,7 @@ export class Post { cascadeRemove: true }) @JoinTable() - metadatas: PostMetadata[] = []; + metadatas: PostMetadata[]; // post has relation with details. full cascades here @ManyToMany(type => PostInformation, information => information.posts, { @@ -59,11 +59,11 @@ export class Post { cascadeRemove: true }) @JoinTable() - informations: PostInformation[] = []; + informations: PostInformation[]; // post has relation with details. not cascades here. means cannot be persisted, updated or removed @ManyToMany(type => PostAuthor, author => author.posts) @JoinTable() - authors: PostAuthor[] = []; + authors: PostAuthor[]; } \ No newline at end of file diff --git a/sample/sample4-many-to-many/entity/PostDetails.ts b/sample/sample4-many-to-many/entity/PostDetails.ts index 64740b016..3ebc7f4d5 100644 --- a/sample/sample4-many-to-many/entity/PostDetails.ts +++ b/sample/sample4-many-to-many/entity/PostDetails.ts @@ -27,6 +27,6 @@ export class PostDetails { cascadeUpdate: true, cascadeRemove: true }) - posts: Post[] = []; + posts: Post[]; } \ No newline at end of file diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index 5134e6411..55d70e386 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -179,13 +179,24 @@ export class MysqlQueryRunner implements QueryRunner { /** * Deletes from the given table by a given conditions. */ - async delete(tableName: string, conditions: ObjectLiteral): Promise { + async delete(tableName: string, condition: string, parameters?: any[]): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); - const conditionString = this.parametrize(conditions).join(" AND "); + const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND "); + const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters; + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; - const parameters = Object.keys(conditions).map(key => conditions[key]); await this.query(sql, parameters); } diff --git a/src/driver/oracle/OracleQueryRunner.ts b/src/driver/oracle/OracleQueryRunner.ts index 6d2dab9bb..1fb4a7309 100644 --- a/src/driver/oracle/OracleQueryRunner.ts +++ b/src/driver/oracle/OracleQueryRunner.ts @@ -191,13 +191,24 @@ export class OracleQueryRunner implements QueryRunner { /** * Deletes from the given table by a given conditions. */ - async delete(tableName: string, conditions: ObjectLiteral): Promise { + async delete(tableName: string, condition: string, parameters?: any[]): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); - const conditionString = this.parametrize(conditions).join(" AND "); + const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND "); + const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters; + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; - const parameters = Object.keys(conditions).map(key => conditions[key]); await this.query(sql, parameters); } diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 09dd3a7b6..c1ab4ea37 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -175,14 +175,25 @@ export class PostgresQueryRunner implements QueryRunner { /** * Deletes from the given table by a given conditions. */ - async delete(tableName: string, conditions: ObjectLiteral): Promise { + async delete(tableName: string, condition: string, parameters?: any[]): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); - const conditionString = this.parametrize(conditions).join(" AND "); - const parameters = Object.keys(conditions).map(key => conditions[key]); - const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`; - await this.query(query, parameters); + const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND "); + const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters; + + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; + await this.query(sql, parameters); } /** diff --git a/src/driver/sqlite/SqliteQueryRunner.ts b/src/driver/sqlite/SqliteQueryRunner.ts index 661de3228..f44deae12 100644 --- a/src/driver/sqlite/SqliteQueryRunner.ts +++ b/src/driver/sqlite/SqliteQueryRunner.ts @@ -188,14 +188,25 @@ export class SqliteQueryRunner implements QueryRunner { /** * Deletes from the given table by a given conditions. */ - async delete(tableName: string, conditions: ObjectLiteral): Promise { + async delete(tableName: string, condition: string, parameters?: any[]): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); - const conditionString = this.parametrize(conditions).join(" AND "); - const parameters = Object.keys(conditions).map(key => conditions[key]); - const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`; - await this.query(query, parameters); + const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND "); + const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters; + + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; + await this.query(sql, parameters); } /** diff --git a/src/driver/sqlserver/SqlServerQueryRunner.ts b/src/driver/sqlserver/SqlServerQueryRunner.ts index 97cf0acdb..ff2464cbf 100644 --- a/src/driver/sqlserver/SqlServerQueryRunner.ts +++ b/src/driver/sqlserver/SqlServerQueryRunner.ts @@ -236,13 +236,24 @@ export class SqlServerQueryRunner implements QueryRunner { /** * Deletes from the given table by a given conditions. */ - async delete(tableName: string, conditions: ObjectLiteral): Promise { + async delete(tableName: string, condition: string, parameters?: any[]): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise; + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral|string, maybeParameters?: any[]): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); - const conditionString = this.parametrize(conditions).join(" AND "); + const conditionString = typeof conditions === "string" ? conditions : this.parametrize(conditions).join(" AND "); + const parameters = conditions instanceof Object ? Object.keys(conditions).map(key => (conditions as ObjectLiteral)[key]) : maybeParameters; + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; - const parameters = Object.keys(conditions).map(key => conditions[key]); await this.query(sql, parameters); } diff --git a/src/metadata-builder/EntityMetadataValidator.ts b/src/metadata-builder/EntityMetadataValidator.ts index a4e9e8edb..6f9a84691 100644 --- a/src/metadata-builder/EntityMetadataValidator.ts +++ b/src/metadata-builder/EntityMetadataValidator.ts @@ -7,7 +7,6 @@ import {MissingJoinTableError} from "./error/MissingJoinTableError"; import {EntityMetadata} from "../metadata/EntityMetadata"; import {MissingPrimaryColumnError} from "./error/MissingPrimaryColumnError"; import {CircularRelationsError} from "./error/CircularRelationsError"; -const DepGraph = require("dependency-graph").DepGraph; /// todo: add check if there are multiple tables with the same name /// todo: add checks when generated column / table names are too long for the specific driver @@ -32,6 +31,8 @@ export class EntityMetadataValidator { * Validates dependencies of the entity metadatas. */ validateDependencies(entityMetadatas: EntityMetadata[]) { + + const DepGraph = require("dependency-graph").DepGraph; const graph = new DepGraph(); entityMetadatas.forEach(entityMetadata => { graph.addNode(entityMetadata.name); diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index 2bd6e6412..15635a06e 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -561,6 +561,27 @@ export class EntityMetadata { return hasAllIds ? map : undefined; } + /** + * Same as getEntityIdMap, but instead of id column property names it returns database column names. + */ + getDatabaseEntityIdMap(entity: ObjectLiteral): ObjectLiteral|undefined { + const map: ObjectLiteral = {}; + if (this.parentEntityMetadata) { + this.primaryColumnsWithParentIdColumns.forEach(column => { + map[column.name] = entity[column.propertyName]; + }); + + } else { + this.primaryColumns.forEach(column => { + map[column.name] = entity[column.propertyName]; + }); + } + const hasAllIds = this.primaryColumns.every(primaryColumn => { + return map[primaryColumn.name] !== undefined && map[primaryColumn.name] !== null; + }); + return hasAllIds ? map : undefined; + } + /** */ createSimpleIdMap(id: any): ObjectLiteral { @@ -578,6 +599,24 @@ export class EntityMetadata { return map; } + /** + * Same as createSimpleIdMap, but instead of id column property names it returns database column names. + */ + createSimpleDatabaseIdMap(id: any): ObjectLiteral { + const map: ObjectLiteral = {}; + if (this.parentEntityMetadata) { + this.primaryColumnsWithParentIdColumns.forEach(column => { + map[column.name] = id; + }); + + } else { + this.primaryColumns.forEach(column => { + map[column.name] = id; + }); + } + return map; + } + /** * todo: undefined entities should not go there?? * todo: shouldnt be entity ObjectLiteral here? diff --git a/src/metadata/RelationMetadata.ts b/src/metadata/RelationMetadata.ts index 01e21b52f..89ef9e5fb 100644 --- a/src/metadata/RelationMetadata.ts +++ b/src/metadata/RelationMetadata.ts @@ -426,6 +426,17 @@ export class RelationMetadata { /** * todo: lazy relations are not supported here? implement logic? + * + * examples: + * + * - isOneToOneNotOwner or isOneToMany: + * Post has a Category. + * Post is owner side. + * Category is inverse side. + * Post.category is mapped to Category.id + * + * if from Post relation we are passing Category here, + * it should return a post.category */ getOwnEntityRelationId(ownEntity: ObjectLiteral): any { if (this.isManyToManyOwner) { @@ -442,6 +453,21 @@ export class RelationMetadata { } } + /** + * + * examples: + * + * - isOneToOneNotOwner or isOneToMany: + * Post has a Category. + * Post is owner side. + * Category is inverse side. + * Post.category is mapped to Category.id + * + * if from Post relation we are passing Category here, + * it should return a category.id + * + * @deprecated Looks like this method does not make sence and does same as getOwnEntityRelationId ? + */ getInverseEntityRelationId(inverseEntity: ObjectLiteral): any { if (this.isManyToManyOwner) { return inverseEntity[this.joinTable.inverseReferencedColumn.propertyName]; diff --git a/src/persistment/DatabaseEntityLoader.ts b/src/persistment/DatabaseEntityLoader.ts index 115753b9b..240c042c3 100644 --- a/src/persistment/DatabaseEntityLoader.ts +++ b/src/persistment/DatabaseEntityLoader.ts @@ -69,8 +69,8 @@ export class DatabaseEntityLoader { * Or from reused just extract databaseEntities from their subjects? (looks better) */ loadedSubjects: SubjectCollection = new SubjectCollection(); - junctionInsertOperations: NewJunctionInsertOperation[]; - junctionRemoveOperations: NewJunctionRemoveOperation[]; + junctionInsertOperations: NewJunctionInsertOperation[] = []; + junctionRemoveOperations: NewJunctionRemoveOperation[] = []; // ------------------------------------------------------------------------- // Constructor @@ -83,7 +83,7 @@ export class DatabaseEntityLoader { // Public Methods // ------------------------------------------------------------------------- - async load(entity: Entity, metadata: EntityMetadata): Promise { + async persist(entity: Entity, metadata: EntityMetadata): Promise { const persistedEntity = new Subject(metadata, entity); persistedEntity.canBeInserted = true; persistedEntity.canBeUpdated = true; @@ -110,8 +110,32 @@ export class DatabaseEntityLoader { // when executing insert/update operations we need to exclude entities scheduled for remove // for junction operations we only get insert and update operations - // persistedEntity.mustBeRemoved = true; - // todo: execute operations + console.log("subjects: ", this.loadedSubjects); + } + + async remove(entity: Entity, metadata: EntityMetadata): Promise { + const persistedEntity = new Subject(metadata, entity); + persistedEntity.mustBeRemoved = true; + this.loadedSubjects.push(persistedEntity); + this.populateSubjectsWithCascadeRemoveEntities(entity, metadata); + await this.loadDatabaseEntities(); + // this.findCascadeInsertAndUpdateEntities(entity, metadata); + + // console.log("loadedSubjects: ", this.loadedSubjects); + + const findCascadeRemoveOperations = this.loadedSubjects + .filter(subject => !!subject.databaseEntity) // means we only attempt to load for non new entities + .map(subject => this.findCascadeRemovedEntitiesToLoad(subject)); + await Promise.all(findCascadeRemoveOperations); + + // find subjects that needs to be inserted and removed from junction table + const [junctionRemoveOperations] = await Promise.all([ + this.buildRemoveJunctionOperations() + ]); + this.junctionRemoveOperations = junctionRemoveOperations; + + // when executing insert/update operations we need to exclude entities scheduled for remove + // for junction operations we only get insert and update operations } @@ -138,8 +162,14 @@ export class DatabaseEntityLoader { if (relation.isEntityDefined(value) && (relation.isCascadeInsert || relation.isCascadeUpdate)) { // if we already has this entity in list of loaded subjects then skip it to avoid recursion - if (this.loadedSubjects.hasWithEntity(value)) + const alreadyExistSubject = this.loadedSubjects.findByEntity(value); + if (alreadyExistSubject) { + if (alreadyExistSubject.canBeInserted === false) + alreadyExistSubject.canBeInserted = relation.isCascadeInsert === true; + if (alreadyExistSubject.canBeUpdated === false) + alreadyExistSubject.canBeUpdated = relation.isCascadeUpdate === true; return; + } // add to the array of subjects to load only if there is no same entity there already const subject = new Subject(valueMetadata, value); // todo: store relations inside to create correct order then? // todo: try to find by likeDatabaseEntity and replace its persistment entity? @@ -153,6 +183,34 @@ export class DatabaseEntityLoader { }); } + /** + */ + protected populateSubjectsWithCascadeRemoveEntities(entity: ObjectLiteral, metadata: EntityMetadata): void { + metadata + .extractRelationValuesFromEntity(entity, metadata.relations) + .forEach(([relation, value, valueMetadata]) => { + + // if there is a value in the relation and remove cascades are set - it means we must load entity + if (relation.isEntityDefined(value) && relation.isCascadeRemove) { + + // if we already has this entity in list of loaded subjects then skip it to avoid recursion + const alreadyExistSubject = this.loadedSubjects.findByEntity(value); + if (alreadyExistSubject) { + alreadyExistSubject.mustBeRemoved = true; + return; + } + + // add to the array of subjects to load only if there is no same entity there already + const subject = new Subject(valueMetadata, value); // todo: store relations inside to create correct order then? // todo: try to find by likeDatabaseEntity and replace its persistment entity? + subject.mustBeRemoved = true; + this.loadedSubjects.push(subject); // todo: throw exception if same persistment entity already exist? or simply replace? + + // go recursively and find other entities to load by cascades in currently inserted entities + this.populateSubjectsWithCascadeRemoveEntities(value, valueMetadata); + } + }); + } + /** * Loads database entities for all loaded subjects which does not have database entities set. */ @@ -266,7 +324,7 @@ export class DatabaseEntityLoader { return; // if this subject is persisted subject then we get its value to check if its not empty or its values changed - let persistValueRelationId: any = null; + let persistValueRelationId: any = undefined; if (subject.entity) { const persistValue = relation.getEntityValue(subject.entity); if (persistValue) persistValueRelationId = relation.getInverseEntityRelationId(persistValue); @@ -337,8 +395,8 @@ export class DatabaseEntityLoader { // (example) "subject.databaseEntity" - is a details object // if this subject is persisted subject then we get its value to check if its not empty or its values changed - let persistValueRelationId: any = null; - if (subject.entity) { + let persistValueRelationId: any = undefined; + if (subject.entity && !subject.mustBeRemoved) { const persistValue = relation.getEntityValue(subject.entity); if (persistValue) persistValueRelationId = relation.getInverseEntityRelationId(persistValue); if (persistValueRelationId === undefined) return; // skip undefined properties @@ -361,7 +419,7 @@ export class DatabaseEntityLoader { // (example) here we seek a Post loaded from the database in the subjects // (example) here relatedSubject.databaseEntity is a Post // (example) and we need to compare post.detailsId === details.id - return relation.getInverseEntityRelationId(relatedSubject.databaseEntity) === relationIdInDatabaseEntity; + return relatedSubject.databaseEntity[relation.inverseRelation.joinColumn.propertyName] === relationIdInDatabaseEntity; }); // if not loaded yet then load it from the database @@ -426,6 +484,8 @@ export class DatabaseEntityLoader { // (example) "valueMetadata" - is an entity metadata of the Post object. // (example) "subject.databaseEntity" - is a details object + console.log("one to many. I shall remove"); + // (example) returns us referenced column (detail's id) const relationIdInDatabaseEntity = relation.getOwnEntityRelationId(subject.databaseEntity); @@ -434,7 +494,7 @@ export class DatabaseEntityLoader { return; // if this subject is persisted subject then we get its value to check if its not empty or its values changed - let persistValue: any = null; + let persistValue: any = undefined; if (subject.entity) { persistValue = relation.getEntityValue(subject.entity); if (persistValue === undefined) return; // skip undefined properties @@ -525,11 +585,15 @@ export class DatabaseEntityLoader { */ private async buildInsertJunctionOperations(): Promise { const junctionInsertOperations: NewJunctionInsertOperation[] = []; - const promises = this.loadedSubjects.map(persistedSubject => { // todo: exclude if object is removed? - const promises = persistedSubject.metadata.manyToManyRelations.map(async relation => { + + // no need to insert junctions of the removed entities + const persistedSubjects = this.loadedSubjects.filter(subject => !subject.mustBeRemoved); + + const promises = persistedSubjects.map(subject => { + const promises = subject.metadata.manyToManyRelations.map(async relation => { // extract entity value - we only need to proceed if value is defined and its an array - const value = relation.getEntityValue(persistedSubject.entity); + const value = relation.getEntityValue(subject.entity); if (!(value instanceof Array)) return; @@ -541,21 +605,21 @@ export class DatabaseEntityLoader { // load from db all relation ids of inverse entities "bind" to the currently persisted entity // this way we gonna check which relation ids are new const existInverseEntityRelationIds = await this.connection - .getSpecificRepository(persistedSubject.entityTarget) - .findRelationIds(relation, persistedSubject.entity, inverseEntityRelationIds); + .getSpecificRepository(subject.entityTarget) + .findRelationIds(relation, subject.entity, inverseEntityRelationIds); // now from all entities in the persisted entity find only those which aren't found in the db /*const newRelationIds = inverseEntityRelationIds.filter(inverseEntityRelationId => { return !existInverseEntityRelationIds.find(relationId => inverseEntityRelationId === relationId); - });*/ + });*/ // todo: remove later if not necessary const persistedEntities = value.filter(val => { const relationValue = relation.getInverseEntityRelationId(val); return !relationValue || !existInverseEntityRelationIds.find(relationId => relationValue === relationId); - }); // todo: remove later if not necessary + }); // finally create a new junction insert operation and push it to the array of such operations if (persistedEntities.length > 0) { - const operation = new NewJunctionInsertOperation(relation, persistedSubject, persistedEntities); + const operation = new NewJunctionInsertOperation(relation, subject, persistedEntities); junctionInsertOperations.push(operation); } }); @@ -572,29 +636,47 @@ export class DatabaseEntityLoader { */ private async buildRemoveJunctionOperations(): Promise { const junctionRemoveOperations: NewJunctionRemoveOperation[] = []; - const promises = this.loadedSubjects.map(persistedSubject => { // todo: exclude if object is removed? - const promises = persistedSubject.metadata.manyToManyRelations.map(async relation => { + const promises = this.loadedSubjects.map(subject => { + const promises = subject.metadata.manyToManyRelations.map(async relation => { - // extract entity value - we only need to proceed if value is defined and its an array - const value = relation.getEntityValue(persistedSubject.entity); - if (!(value instanceof Array)) - return; + // if subject marked to be removed then all its junctions must be removed + if (subject.mustBeRemoved) { - // get all inverse entities that are "bind" to the currently persisted entity - const inverseEntityRelationIds = value - .map(v => relation.getInverseEntityRelationId(v)) - .filter(v => v !== undefined && v !== null); + // load from db all relation ids of inverse entities that are NOT "bind" to the currently persisted entity + // this way we gonna check which relation ids are missing (e.g. removed) + const removedInverseEntityRelationIds = await this.connection + .getSpecificRepository(subject.entityTarget) + .findRelationIds(relation, subject.databaseEntity); - // load from db all relation ids of inverse entities that are NOT "bind" to the currently persisted entity - // this way we gonna check which relation ids are missing (e.g. removed) - const removedInverseEntityRelationIds = await this.connection - .getSpecificRepository(persistedSubject.entityTarget) - .findRelationIds(relation, persistedSubject.entity, undefined, inverseEntityRelationIds); + // finally create a new junction remove operation and push it to the array of such operations + if (removedInverseEntityRelationIds.length > 0) { + const operation = new NewJunctionRemoveOperation(relation, subject, removedInverseEntityRelationIds); + junctionRemoveOperations.push(operation); + } - // finally create a new junction remove operation and push it to the array of such operations - if (removedInverseEntityRelationIds.length > 0) { - const operation = new NewJunctionRemoveOperation(relation, persistedSubject.entity, removedInverseEntityRelationIds); - junctionRemoveOperations.push(operation); + } else { // else simply check changed junctions in the persisted entity + + // extract entity value - we only need to proceed if value is defined and its an array + const value = relation.getEntityValue(subject.entity); + if (!(value instanceof Array)) + return; + + // get all inverse entities that are "bind" to the currently persisted entity + const inverseEntityRelationIds = value + .map(v => relation.getInverseEntityRelationId(v)) + .filter(v => v !== undefined && v !== null); + + // load from db all relation ids of inverse entities that are NOT "bind" to the currently persisted entity + // this way we gonna check which relation ids are missing (e.g. removed) + const removedInverseEntityRelationIds = await this.connection + .getSpecificRepository(subject.entityTarget) + .findRelationIds(relation, subject.entity, undefined, inverseEntityRelationIds); + + // finally create a new junction remove operation and push it to the array of such operations + if (removedInverseEntityRelationIds.length > 0) { + const operation = new NewJunctionRemoveOperation(relation, subject, removedInverseEntityRelationIds); + junctionRemoveOperations.push(operation); + } } }); diff --git a/src/persistment/EntityPersister.ts b/src/persistment/EntityPersister.ts index 10b22ea68..41367b7a4 100644 --- a/src/persistment/EntityPersister.ts +++ b/src/persistment/EntityPersister.ts @@ -1,9 +1,6 @@ import {EntityMetadata} from "../metadata/EntityMetadata"; import {ObjectLiteral} from "../common/ObjectLiteral"; import {QueryBuilder} from "../query-builder/QueryBuilder"; -import {PlainObjectToDatabaseEntityTransformer} from "../query-builder/transformer/PlainObjectToDatabaseEntityTransformer"; -import {EntityPersistOperationBuilder} from "./EntityPersistOperationsBuilder"; -import {PersistOperationExecutor} from "./PersistOperationExecutor"; import {Connection} from "../connection/Connection"; import {QueryRunner} from "../query-runner/QueryRunner"; import {Subject} from "./subject/Subject"; @@ -113,6 +110,38 @@ export class EntityPersister { }*/ + /** + * Removes given entity from the database. + */ + async remove(entity: Entity): Promise { + + const databaseEntityLoader = new DatabaseEntityLoader(this.connection); + await databaseEntityLoader.remove(entity, this.metadata); + // console.log("all persistence subjects: ", databaseEntityLoader.loadedSubjects); + + const executor = new PersistSubjectExecutor(this.connection, this.queryRunner); + await executor.execute(databaseEntityLoader.loadedSubjects, databaseEntityLoader.junctionInsertOperations, databaseEntityLoader.junctionRemoveOperations); + + /* + const queryBuilder = new QueryBuilder(this.connection, this.queryRunner) + .select(this.metadata.table.name) + .from(this.metadata.target, this.metadata.table.name); + const plainObjectToDatabaseEntityTransformer = new PlainObjectToDatabaseEntityTransformer(); + const dbEntity = await plainObjectToDatabaseEntityTransformer.transform(entity, this.metadata, queryBuilder); + + this.metadata.primaryColumnsWithParentPrimaryColumns.forEach(primaryColumn => entity[primaryColumn.name] = undefined); + const dbEntities = this.flattenEntityRelationTree(dbEntity, this.metadata); + const allPersistedEntities = this.flattenEntityRelationTree(entity, this.metadata); + const entityWithId = new Subject(this.metadata, entity); + const dbEntityWithId = new Subject(this.metadata, dbEntity); + + const entityPersistOperationBuilder = new EntityPersistOperationBuilder(this.connection.entityMetadatas); + const persistOperation = entityPersistOperationBuilder.buildOnlyRemovement(this.metadata, dbEntityWithId, entityWithId, dbEntities, allPersistedEntities); + const persistOperationExecutor = new PersistOperationExecutor(this.connection.driver, this.connection.entityMetadatas, this.connection.broadcaster, this.queryRunner); // todo: better to pass connection? + await persistOperationExecutor.executePersistOperation(persistOperation);*/ + return entity; + } + /** * Persists given entity in the database. * Persistence is a complex process: @@ -121,9 +150,8 @@ export class EntityPersister { */ async persist(entity: Entity): Promise { const databaseEntityLoader = new DatabaseEntityLoader(this.connection); - await databaseEntityLoader.load(entity, this.metadata); - console.log("all persistence subjects: ", databaseEntityLoader.loadedSubjects); - + await databaseEntityLoader.persist(entity, this.metadata); + // console.log("all persistence subjects: ", databaseEntityLoader.loadedSubjects); const executor = new PersistSubjectExecutor(this.connection, this.queryRunner); await executor.execute(databaseEntityLoader.loadedSubjects, databaseEntityLoader.junctionInsertOperations, databaseEntityLoader.junctionRemoveOperations); @@ -504,29 +532,6 @@ export class EntityPersister { return insertOperations; } - /** - * Removes given entity from the database. - */ - async remove(entity: Entity): Promise { - const queryBuilder = new QueryBuilder(this.connection, this.queryRunner) - .select(this.metadata.table.name) - .from(this.metadata.target, this.metadata.table.name); - const plainObjectToDatabaseEntityTransformer = new PlainObjectToDatabaseEntityTransformer(); - const dbEntity = await plainObjectToDatabaseEntityTransformer.transform(entity, this.metadata, queryBuilder); - - this.metadata.primaryColumnsWithParentPrimaryColumns.forEach(primaryColumn => entity[primaryColumn.name] = undefined); - const dbEntities = this.flattenEntityRelationTree(dbEntity, this.metadata); - const allPersistedEntities = this.flattenEntityRelationTree(entity, this.metadata); - const entityWithId = new Subject(this.metadata, entity); - const dbEntityWithId = new Subject(this.metadata, dbEntity); - - const entityPersistOperationBuilder = new EntityPersistOperationBuilder(this.connection.entityMetadatas); - const persistOperation = entityPersistOperationBuilder.buildOnlyRemovement(this.metadata, dbEntityWithId, entityWithId, dbEntities, allPersistedEntities); - const persistOperationExecutor = new PersistOperationExecutor(this.connection.driver, this.connection.entityMetadatas, this.connection.broadcaster, this.queryRunner); // todo: better to pass connection? - await persistOperationExecutor.executePersistOperation(persistOperation); - return entity; - } - // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- diff --git a/src/persistment/PersistSubjectExecutor.ts b/src/persistment/PersistSubjectExecutor.ts index 211514fe6..ef1b70048 100644 --- a/src/persistment/PersistSubjectExecutor.ts +++ b/src/persistment/PersistSubjectExecutor.ts @@ -1,6 +1,5 @@ import {PersistOperation} from "./operation/PersistOperation"; import {InsertOperation} from "./operation/InsertOperation"; -import {JunctionRemoveOperation} from "./operation/JunctionRemoveOperation"; import {UpdateByRelationOperation} from "./operation/UpdateByRelationOperation"; import {UpdateByInverseSideOperation} from "./operation/UpdateByInverseSideOperation"; import {RelationMetadata} from "../metadata/RelationMetadata"; @@ -43,11 +42,11 @@ export class PersistSubjectExecutor { // check if remove subject also must be inserted or updated - then we throw an exception const removeInInserts = removeSubjects.find(removeSubject => insertSubjects.indexOf(removeSubject) !== -1); if (removeInInserts) - throw new Error(`Removed entity ${removeInInserts.entityTarget} is also scheduled for insert operation. This looks like ORM problem. Please report a github issue.`); + throw new Error(`Removed entity ${removeInInserts.entityTargetName} is also scheduled for insert operation. This looks like ORM problem. Please report a github issue.`); const removeInUpdates = removeSubjects.find(removeSubject => updateSubjects.indexOf(removeSubject) !== -1); if (removeInUpdates) - throw new Error(`Removed entity "${removeInUpdates.entityTarget}" is also scheduled for update operation. ` + + throw new Error(`Removed entity "${removeInUpdates.entityTargetName}" is also scheduled for update operation. ` + `Make sure you are not updating and removing same object (note that update or remove may be executed by cascade operations).`); // todo: there is nothing to update in inserted entity too @@ -64,22 +63,21 @@ export class PersistSubjectExecutor { } await this.executeInsertOperations(insertSubjects); - // await this.executeInsertClosureTableOperations(insertSubjects); - // await this.executeUpdateTreeLevelOperations(insertSubjects); + await this.executeInsertClosureTableOperations(insertSubjects); await this.executeInsertJunctionsOperations(junctionInsertOperations, insertSubjects); - // await this.executeRemoveJunctionsOperations(junctionRemoveOperations); + await this.executeRemoveJunctionsOperations(junctionRemoveOperations); // await this.executeRemoveRelationOperations(persistOperation); // todo: can we add these operations into list of updated? // await this.executeUpdateRelationsOperations(persistOperation); // todo: merge these operations with update operations? // await this.executeUpdateInverseRelationsOperations(persistOperation); // todo: merge these operations with update operations? - // await this.executeUpdateOperations(updateSubjects); - // await this.executeRemoveOperations(removeSubjects); + await this.executeUpdateOperations(updateSubjects); + await this.executeRemoveOperations(removeSubjects); // commit transaction if it was started by us if (isTransactionStartedByItself === true) await this.queryRunner.commitTransaction(); // update all special columns in persisted entities, like inserted id or remove ids from the removed entities - // await this.updateSpecialColumnsInPersistedEntities(insertSubjects, updateSubjects, removeSubjects); + await this.updateSpecialColumnsInPersistedEntities(insertSubjects, updateSubjects, removeSubjects); // finally broadcast events this.connection.broadcaster.broadcastAfterEventsForAll(insertSubjects, updateSubjects, removeSubjects); @@ -154,7 +152,7 @@ export class PersistSubjectExecutor { }); }); if (Object.keys(updateOptions).length > 0) { - const conditions = subject.metadata.getEntityIdMap(subject.entity) || subject.metadata.createSimpleIdMap(subject.newlyGeneratedId); + const conditions = subject.metadata.getDatabaseEntityIdMap(subject.entity) || subject.metadata.createSimpleDatabaseIdMap(subject.newlyGeneratedId); const updatePromise = this.queryRunner.update(subject.metadata.table.name, updateOptions, conditions); updatePromises.push(updatePromise); } @@ -170,7 +168,7 @@ export class PersistSubjectExecutor { if (subValue === insertedSubject.entity) { if (referencedColumn.isGenerated) { - const conditions = insertedSubject.metadata.getEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleIdMap(insertedSubject.newlyGeneratedId); + const conditions = insertedSubject.metadata.getDatabaseEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleDatabaseIdMap(insertedSubject.newlyGeneratedId); const updateOptions = { [relation.inverseRelation.joinColumn.name]: subject.newlyGeneratedId }; const updatePromise = this.queryRunner.update(relation.inverseRelation.entityMetadata.table.name, updateOptions, conditions); updatePromises.push(updatePromise); @@ -191,7 +189,7 @@ export class PersistSubjectExecutor { if (subject.entity[relation.propertyName] === insertedSubject.entity) { if (referencedColumn.isGenerated) { - const conditions = insertedSubject.metadata.getEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleIdMap(insertedSubject.newlyGeneratedId); + const conditions = insertedSubject.metadata.getDatabaseEntityIdMap(insertedSubject.entity) || insertedSubject.metadata.createSimpleDatabaseIdMap(insertedSubject.newlyGeneratedId); const updateOptions = { [relation.inverseRelation.joinColumn.name]: subject.newlyGeneratedId }; const updatePromise = this.queryRunner.update(relation.inverseRelation.entityMetadata.table.name, updateOptions, conditions); updatePromises.push(updatePromise); @@ -212,22 +210,23 @@ export class PersistSubjectExecutor { /** * Executes insert operations for closure tables. */ - private executeInsertClosureTableOperations(insertSubjects: Subject[], updatesByRelations: Subject[]) { // todo: what to do with updatesByRelations + private executeInsertClosureTableOperations(insertSubjects: Subject[]/*, updatesByRelations: Subject[]*/) { // todo: what to do with updatesByRelations const promises = insertSubjects .filter(subject => subject.metadata.table.isClosure) .map(async subject => { - const relationsUpdateMap = this.findUpdateOperationForEntity(updatesByRelations, insertSubjects, subject.entity); - subject.treeLevel = await this.insertIntoClosureTable(subject, relationsUpdateMap); + // const relationsUpdateMap = this.findUpdateOperationForEntity(updatesByRelations, insertSubjects, subject.entity); + // subject.treeLevel = await this.insertIntoClosureTable(subject, relationsUpdateMap); + await this.insertClosureTableValues(subject, insertSubjects); }); return Promise.all(promises); } /** * Executes update tree level operations in inserted entities right after data into closure table inserted. - */ + private executeUpdateTreeLevelOperations(insertOperations: Subject[]) { return Promise.all(insertOperations.map(subject => this.updateTreeLevel(subject))); - } + }*/ /** * Executes insert junction operations. @@ -287,8 +286,34 @@ export class PersistSubjectExecutor { /** * Executes remove operations. */ - private executeRemoveOperations(removeSubjects: Subject[]) { - return Promise.all(removeSubjects.map(subject => this.remove(subject))); + private async executeRemoveOperations(removeSubjects: Subject[]): Promise { + // order subjects in a proper order + + /*const DepGraph = require("dependency-graph").DepGraph; + const graph = new DepGraph(); + removeSubjects.forEach(subject => { + // console.log("adding node: ", subject.metadata.name); + if (!graph.hasNode(subject.metadata.name)) + graph.addNode(subject.metadata.name); + }); + removeSubjects.forEach(subject => { + subject.metadata + .relationsWithJoinColumns + .filter(relation => relation.isCascadeRemove) + .forEach(relation => { + if (graph.hasNode(subject.metadata.name) && graph.hasNode(relation.inverseEntityMetadata.name)) + graph.addDependency(subject.metadata.name, relation.inverseEntityMetadata.name); + }); + }); + try { + const order = graph.overallOrder(); + console.log("order: ", order); + + } catch (err) { + throw new Error(err.toString().replace("Error: Dependency Cycle Found: ", "")); + }*/ + + await Promise.all(removeSubjects.map(subject => this.remove(subject))); } /** @@ -299,12 +324,12 @@ export class PersistSubjectExecutor { // update entity ids of the newly inserted entities insertSubjects.forEach(subject => { subject.metadata.primaryColumns.forEach(primaryColumn => { - if (subject.entityId) - subject.entity[primaryColumn.propertyName] = subject.entityId[primaryColumn.propertyName]; + if (subject.newlyGeneratedId) + subject.entity[primaryColumn.propertyName] = subject.newlyGeneratedId; }); subject.metadata.parentPrimaryColumns.forEach(primaryColumn => { - if (subject.entityId) - subject.entity[primaryColumn.propertyName] = subject.entityId[primaryColumn.propertyName]; + if (subject.newlyGeneratedId) + subject.entity[primaryColumn.propertyName] = subject.newlyGeneratedId; }); }); @@ -335,6 +360,7 @@ export class PersistSubjectExecutor { // remove ids from the entities that were removed removeSubjects.forEach(subject => { + if (!subject.entity) return; // const removedEntity = removeSubjects.allPersistedEntities.find(allNewEntity => { // return allNewEntity.entityTarget === subject.entityTarget && allNewEntity.compareId(subject.metadata.getEntityIdMap(subject.entity)!); // }); @@ -394,14 +420,14 @@ export class PersistSubjectExecutor { const metadata = this.connection.entityMetadatas.findByTarget(operation.entityTarget); let idInInserts: ObjectLiteral|undefined = undefined; if (relatedInsertOperation && relatedInsertOperation.entityId) { - idInInserts = { [metadata.firstPrimaryColumn.propertyName]: relatedInsertOperation.entityId[metadata.firstPrimaryColumn.propertyName] }; + idInInserts = { [metadata.firstPrimaryColumn.name]: relatedInsertOperation.entityId[metadata.firstPrimaryColumn.propertyName] }; } // todo: use join column instead of primary column here tableName = metadata.table.name; relationName = operation.updatedRelation.name; relationId = operation.insertOperation.entityId[metadata.firstPrimaryColumn.propertyName]; // todo: make sure entityId is always a map // idColumn = metadata.primaryColumn.name; // id = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts; - updateMap = metadata.getEntityIdColumnMap(operation.targetEntity) || idInInserts; // todo: make sure idInInserts always object even when id is single!!! + updateMap = metadata.getDatabaseEntityIdMap(operation.targetEntity) || idInInserts; // todo: make sure idInInserts always object even when id is single!!! } if (!updateMap) throw new Error(`Cannot execute update by relation operation, because cannot find update criteria`); @@ -414,7 +440,7 @@ export class PersistSubjectExecutor { const fromEntityMetadata = this.connection.entityMetadatas.findByTarget(operation.fromEntityTarget); const tableName = targetEntityMetadata.table.name; const targetRelation = operation.fromRelation.inverseRelation; - const updateMap = targetEntityMetadata.getEntityIdColumnMap(operation.targetEntity); + const updateMap = targetEntityMetadata.getDatabaseEntityIdMap(operation.targetEntity); if (!updateMap) return; // todo: is return correct here? const fromEntityInsertOperation = insertOperations.find(o => o.entity === operation.fromEntity); @@ -439,7 +465,7 @@ export class PersistSubjectExecutor { const valueMaps: { tableName: string, metadata: EntityMetadata, values: ObjectLiteral }[] = []; subject.diffColumns.forEach(column => { - if (!column.entityTarget) return; + if (!column.entityTarget) return; // todo: how this can be possible? const metadata = this.connection.entityMetadatas.findByTarget(column.entityTarget); let valueMap = valueMaps.find(valueMap => valueMap.tableName === metadata.table.name); if (!valueMap) { @@ -543,17 +569,17 @@ export class PersistSubjectExecutor { if (subject.metadata.parentEntityMetadata) { const parentConditions: ObjectLiteral = {}; subject.metadata.parentPrimaryColumns.forEach(column => { - parentConditions[column.name] = subject.entity[column.propertyName]; + parentConditions[column.name] = subject.databaseEntity![column.propertyName]; }); await this.queryRunner.delete(subject.metadata.parentEntityMetadata.table.name, parentConditions); const childConditions: ObjectLiteral = {}; subject.metadata.primaryColumnsWithParentIdColumns.forEach(column => { - childConditions[column.name] = subject.entity[column.propertyName]; + childConditions[column.name] = subject.databaseEntity![column.propertyName]; }); await this.queryRunner.delete(subject.metadata.table.name, childConditions); } else { - await this.queryRunner.delete(subject.metadata.table.name, subject.metadata.getEntityIdColumnMap(subject.entity)!); + await this.queryRunner.delete(subject.metadata.table.name, subject.metadata.getEntityIdColumnMap(subject.databaseEntity!)!); } } @@ -692,6 +718,46 @@ export class PersistSubjectExecutor { return this.zipObject(allColumnNames, allValues); } + private async insertClosureTableValues(subject: Subject, insertedSubjects: Subject[]): Promise { + // todo: since closure tables do not support compose primary keys - throw an exception? + // todo: what if parent entity or parentEntityId is empty?! + const tableName = subject.metadata.closureJunctionTable.table.name; + const referencedColumn = subject.metadata.treeParentRelation.joinColumn.referencedColumn; // todo: check if joinColumn works + + let newEntityId = subject.entity[referencedColumn.propertyName]; + if (!newEntityId && referencedColumn.isGenerated) { + newEntityId = subject.newlyGeneratedId; + } // todo: implement other special column types too + + const parentEntity = subject.entity[subject.metadata.treeParentRelation.propertyName]; + let parentEntityId = parentEntity[referencedColumn.propertyName]; + if (!parentEntityId && referencedColumn.isGenerated) { + const parentInsertedSubject = insertedSubjects.find(subject => subject.entity === parentEntity); + // todo: throw exception if parentInsertedSubject is not set + parentEntityId = parentInsertedSubject.newlyGeneratedId; + } // todo: implement other special column types too + + subject.treeLevel = await this.queryRunner.insertIntoClosureTable(tableName, newEntityId, parentEntityId, subject.metadata.hasTreeLevelColumn); + + if (subject.metadata.hasTreeLevelColumn) { + const values = { [subject.metadata.treeLevelColumn.name]: subject.treeLevel }; + await this.queryRunner.update(subject.metadata.table.name, values, { [referencedColumn.name]: newEntityId }); + } + } + + /** + * @deprecated + + private async updateTreeLevel(subject: Subject): Promise { + if (subject.metadata.hasTreeLevelColumn && subject.treeLevel) { + const values = { [subject.metadata.treeLevelColumn.name]: subject.treeLevel }; + await this.queryRunner.update(subject.metadata.table.name, values, subject.entityId); + } + }*/ + + /** + * @deprecated + */ private insertIntoClosureTable(subject: Subject, updateMap: ObjectLiteral) { // here we can only support to work only with single primary key entities @@ -721,16 +787,6 @@ export class PersistSubjectExecutor { })*/; } - private async updateTreeLevel(subject: Subject): Promise { - if (subject.metadata.hasTreeLevelColumn && subject.treeLevel) { - if (!subject.entityId) - throw new Error(`remove operation does not have entity id`); - - const values = { [subject.metadata.treeLevelColumn.name]: subject.treeLevel }; - await this.queryRunner.update(subject.metadata.table.name, values, subject.entityId); - } - } - /*private insertJunctions(junctionOperation: NewJunctionInsertOperation, insertOperations: Subject[]) { // I think here we can only support to work only with single primary key entities @@ -815,15 +871,17 @@ export class PersistSubjectExecutor { await Promise.all(promises); } - private removeJunctions(junctionOperation: JunctionRemoveOperation) { - // I think here we can only support to work only with single primary key entities - const junctionMetadata = junctionOperation.metadata; - const metadata1 = this.connection.entityMetadatas.findByTarget(junctionOperation.entity1Target); - const metadata2 = this.connection.entityMetadatas.findByTarget(junctionOperation.entity2Target); - const columns = junctionMetadata.columns.map(column => column.name); - const id1 = junctionOperation.entity1[metadata1.firstPrimaryColumn.propertyName]; - const id2 = junctionOperation.entity2[metadata2.firstPrimaryColumn.propertyName]; - return this.queryRunner.delete(junctionMetadata.table.name, { [columns[0]]: id1, [columns[1]]: id2 }); + private async removeJunctions(junctionOperation: NewJunctionRemoveOperation) { + const junctionMetadata = junctionOperation.relation.junctionEntityMetadata; + const ownId = junctionOperation.relation.getOwnEntityRelationId(junctionOperation.subject.entity || junctionOperation.subject.databaseEntity); + const ownColumn = junctionOperation.relation.isOwning ? junctionMetadata.columns[0] : junctionMetadata.columns[1]; + const relateColumn = junctionOperation.relation.isOwning ? junctionMetadata.columns[1] : junctionMetadata.columns[0]; + + const removePromises = junctionOperation.junctionEntityRelationIds.map(async relationId => { + await this.queryRunner.delete(junctionMetadata.table.name, { [ownColumn.name]: ownId, [relateColumn.name]: relationId }); + }); + + await Promise.all(removePromises); } private zipObject(keys: any[], values: any[]): Object { diff --git a/src/persistment/operation/NewJunctionRemoveOperation.ts b/src/persistment/operation/NewJunctionRemoveOperation.ts index 7175059a7..3f429cd21 100644 --- a/src/persistment/operation/NewJunctionRemoveOperation.ts +++ b/src/persistment/operation/NewJunctionRemoveOperation.ts @@ -1,13 +1,13 @@ -import {ObjectLiteral} from "../../common/ObjectLiteral"; import {RelationMetadata} from "../../metadata/RelationMetadata"; +import {Subject} from "../subject/Subject"; // todo: for both remove and insert can be used same object export class NewJunctionRemoveOperation { // todo: we can send subjects instead of entities and junction entities if needed constructor(public relation: RelationMetadata, - public entity: ObjectLiteral, - public junctionEntityRelationIds: ObjectLiteral[]) { + public subject: Subject, + public junctionEntityRelationIds: any[]) { } } \ No newline at end of file diff --git a/src/persistment/subject/Subject.ts b/src/persistment/subject/Subject.ts index e31ed3778..4088c1f39 100644 --- a/src/persistment/subject/Subject.ts +++ b/src/persistment/subject/Subject.ts @@ -67,6 +67,19 @@ export class Subject { // todo: move entity with id creation into metadata? // t return this.metadata.target; } + /** + * Returns readable / loggable name of the entity target. + */ + get entityTargetName(): string { + if (this.entityTarget instanceof Function) { + if (this.entityTarget.name) { + return this.entityTarget.name; + } + } + + return this.entityTarget as string; + } + get id() { return this.metadata.getEntityIdMap(this.entity); } @@ -90,8 +103,8 @@ export class Subject { // todo: move entity with id creation into metadata? // t set databaseEntity(databaseEntity: ObjectLiteral|undefined) { this._databaseEntity = databaseEntity; if (this.entity && databaseEntity) { - this.buildDiffColumns(); - this.buildDiffRelationalColumns(); + this.diffColumns = this.buildDiffColumns(); + this.diffRelations = this.buildDiffRelationalColumns(); } } diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index d78a5272e..d6c247d67 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -1038,7 +1038,7 @@ export class QueryBuilder { const [sql, parameters] = this.getSqlWithParameters(); try { - console.log(sql); + // console.log(sql); return await queryRunner.query(sql, parameters) .then(results => { scalarResults = results; @@ -1101,7 +1101,7 @@ export class QueryBuilder { }).join(", ") + ") as cnt"; const countQuery = this - .clone({ queryRunner: queryRunner, skipOrderBys: true, ignoreParentTablesJoins: true }) + .clone({ queryRunner: queryRunner, skipOrderBys: true, ignoreParentTablesJoins: true, skipLimit: true, skipOffset: true }) .select(countSql); const [countQuerySql, countQueryParameters] = countQuery.getSqlWithParameters(); diff --git a/src/query-runner/QueryRunner.ts b/src/query-runner/QueryRunner.ts index 6a7519821..caadb7c26 100644 --- a/src/query-runner/QueryRunner.ts +++ b/src/query-runner/QueryRunner.ts @@ -57,6 +57,11 @@ export interface QueryRunner { */ insert(tableName: string, valuesMap: Object, generatedColumn?: ColumnMetadata): Promise; + /** + * Performs a simple DELETE query by a given conditions in a given table. + */ + delete(tableName: string, condition: string, parameters?: any[]): Promise; + /** * Performs a simple DELETE query by a given conditions in a given table. */ diff --git a/test/functional/persistence/cascade-operations/cascade-operations.ts b/test/functional/persistence/cascade-operations/cascade-operations.ts index fd3a0e67f..bda9bc275 100644 --- a/test/functional/persistence/cascade-operations/cascade-operations.ts +++ b/test/functional/persistence/cascade-operations/cascade-operations.ts @@ -3,7 +3,6 @@ import {setupTestingConnections, closeConnections, reloadDatabases} from "../../ import {Connection} from "../../../../src/connection/Connection"; import {Post} from "./entity/Post"; import {Category} from "./entity/Category"; -import {Photo} from "./entity/Photo"; describe("persistence > cascade operations", () => { @@ -16,9 +15,9 @@ describe("persistence > cascade operations", () => { beforeEach(() => reloadDatabases(connections)); after(() => closeConnections(connections)); - describe("cascade insert", function() { + describe.skip("cascade insert", function() { - it.only("should work perfectly", () => Promise.all(connections.map(async connection => { + it("should work perfectly", () => Promise.all(connections.map(async connection => { // create category @@ -34,25 +33,41 @@ describe("persistence > cascade operations", () => { // create post const post1 = new Post(); post1.title = "Hello Post #1"; - // post1.oneCategory = category1; + post1.oneCategory = category1; // todo(next): check out to one // create photos - const photo1 = new Photo(); + /*const photo1 = new Photo(); photo1.url = "http://me.com/photo"; photo1.post = post1; photo1.categories = [category1, category2]; const photo2 = new Photo(); photo2.url = "http://me.com/photo"; - photo2.post = post1; + photo2.post = post1;*/ // category1.photos = [photo1, photo2]; // post1.category = category1; // post1.category.photos = [photo1, photo2]; - await connection.entityManager.persist(photo1); + await connection.entityManager.persist(post1); + + console.log("********************************************************"); + console.log("updating: ", post1); + console.log("********************************************************"); + + post1.title = "updated post #1"; + post1.oneCategory.name = "updated category"; + await connection.entityManager.persist(post1); + + console.log("********************************************************"); + console.log("removing: ", post1); + console.log("********************************************************"); + + await connection.entityManager.remove(post1); + + // await connection.entityManager.persist(post1); console.log("********************************************************"); diff --git a/test/functional/persistence/custom-column-name-pk/custom-column-name-pk.ts b/test/functional/persistence/custom-column-name-pk/custom-column-name-pk.ts index 9831a0689..373561b03 100644 --- a/test/functional/persistence/custom-column-name-pk/custom-column-name-pk.ts +++ b/test/functional/persistence/custom-column-name-pk/custom-column-name-pk.ts @@ -4,7 +4,7 @@ import {Connection} from "../../../../src/connection/Connection"; import {Post} from "./entity/Post"; import {Category} from "./entity/Category"; -describe("persistence > cascade operations with custom name", () => { +describe.only("persistence > cascade operations with custom name", () => { let connections: Connection[]; before(async () => connections = await setupTestingConnections({ diff --git a/test/functional/persistence/insert-operations/entity/Category.ts b/test/functional/persistence/insert-operations/entity/Category.ts index 1447c2724..857a93da7 100644 --- a/test/functional/persistence/insert-operations/entity/Category.ts +++ b/test/functional/persistence/insert-operations/entity/Category.ts @@ -19,20 +19,51 @@ export class Category { @Column() name: string; - @OneToOne(type => Post, post => post.oneCategory, { + @OneToMany(type => Post, post => post.manyToOneCategory, { cascadeInsert: true, cascadeUpdate: true, - cascadeRemove: true + cascadeRemove: true, + }) + oneToManyPosts: Post[]; + + @OneToMany(type => Post, post => post.noCascadeManyToOneCategory, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + noCascadeOneToManyPosts: Post[]; + + @OneToOne(type => Post, post => post.oneToOneCategory, { + cascadeInsert: true, + cascadeUpdate: true, + cascadeRemove: true, }) @JoinColumn() - onePost: Post; + oneToOneOwnerPost: Post; - @OneToMany(type => Post, post => post.category, { + @OneToOne(type => Post, post => post.noCascadeOneToOneCategory, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + @JoinColumn() + noCascadeOneToOnePost: Post; + + @ManyToMany(type => Post, post => post.manyToManyOwnerCategories, { cascadeInsert: true, cascadeUpdate: true, - cascadeRemove: true + cascadeRemove: true, }) - posts: Post[]; + @JoinTable() + manyToManyPosts: Post[]; + + @ManyToMany(type => Post, post => post.noCascadeManyToManyOwnerCategories, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + @JoinTable() + noCascadeManyToManyPosts: Post[]; @ManyToMany(type => Photo, { cascadeInsert: true, diff --git a/test/functional/persistence/insert-operations/entity/Photo.ts b/test/functional/persistence/insert-operations/entity/Photo.ts index fca47268c..9457694d1 100644 --- a/test/functional/persistence/insert-operations/entity/Photo.ts +++ b/test/functional/persistence/insert-operations/entity/Photo.ts @@ -3,6 +3,7 @@ import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/Prima import {Column} from "../../../../../src/decorator/columns/Column"; import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne"; import {Post} from "../entity/Post"; +import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany"; @Table() export class Photo { @@ -21,4 +22,11 @@ export class Photo { }) post: Post|null; + @ManyToMany(type => Post, photo => photo.photos, { + cascadeInsert: true, + cascadeUpdate: true, + cascadeRemove: true, + }) + posts: Post[]; + } \ No newline at end of file diff --git a/test/functional/persistence/insert-operations/entity/Post.ts b/test/functional/persistence/insert-operations/entity/Post.ts index a0723c5f7..618fadf4e 100644 --- a/test/functional/persistence/insert-operations/entity/Post.ts +++ b/test/functional/persistence/insert-operations/entity/Post.ts @@ -17,26 +17,64 @@ export class Post { @Column() title: string; - @ManyToOne(type => Category, category => category.posts, { + @ManyToOne(type => Category, category => category.oneToManyPosts, { cascadeInsert: true, cascadeUpdate: true, - cascadeRemove: true + cascadeRemove: true, }) - category: Category|null; + manyToOneCategory: Category; - @ManyToMany(type => Photo, { + @ManyToOne(type => Category, category => category.noCascadeOneToManyPosts, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + noCascadeManyToOneCategory: Category; + + @OneToOne(type => Category, category => category.oneToOneOwnerPost, { cascadeInsert: true, cascadeUpdate: true, - cascadeRemove: true + cascadeRemove: true, + }) + oneToOneCategory: Category; + + @OneToOne(type => Category, category => category.noCascadeOneToOnePost, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + noCascadeOneToOneCategory: Category; + + @ManyToMany(type => Category, category => category.manyToManyPosts, { + cascadeInsert: true, + cascadeUpdate: true, + cascadeRemove: true, + }) + @JoinTable() + manyToManyOwnerCategories: Category[]; + + @ManyToMany(type => Category, category => category.noCascadeManyToManyPosts, { + cascadeInsert: false, + cascadeUpdate: false, + cascadeRemove: false, + }) + @JoinTable() + noCascadeManyToManyOwnerCategories: Category[]; + + @ManyToMany(type => Photo, photo => photo.posts, { + cascadeInsert: true, + cascadeUpdate: true, + cascadeRemove: true, }) @JoinTable() photos: Photo[]; - @OneToOne(type => Category, category => category.onePost, { + @ManyToMany(type => Photo, { cascadeInsert: true, cascadeUpdate: true, - cascadeRemove: true + cascadeRemove: true, }) - oneCategory: Category; + @JoinTable() + noInversePhotos: Photo[]; } \ No newline at end of file diff --git a/test/functional/persistence/insert-operations/insert-operations.ts b/test/functional/persistence/insert-operations/insert-operations.ts index 16f04e03e..680a47123 100644 --- a/test/functional/persistence/insert-operations/insert-operations.ts +++ b/test/functional/persistence/insert-operations/insert-operations.ts @@ -3,9 +3,8 @@ import {setupTestingConnections, closeConnections, reloadDatabases} from "../../ import {Connection} from "../../../../src/connection/Connection"; import {Post} from "./entity/Post"; import {Category} from "./entity/Category"; -import {Photo} from "./entity/Photo"; -describe("persistence > insert operations", () => { +describe.skip("persistence > insert operations", () => { let connections: Connection[]; before(async () => connections = await setupTestingConnections({ @@ -28,21 +27,21 @@ describe("persistence > insert operations", () => { // create post const post1 = new Post(); post1.title = "Hello Post #1"; - post1.oneCategory = category1; // todo(next): check out to one // create photos - const photo1 = new Photo(); + /* const photo1 = new Photo(); photo1.url = "http://me.com/photo"; photo1.post = post1; const photo2 = new Photo(); photo2.url = "http://me.com/photo"; - photo2.post = post1; + photo2.post = post1;*/ // post1.category = category1; // post1.category.photos = [photo1, photo2]; await connection.entityManager.persist(post1); + await connection.entityManager.persist(category1); console.log("********************************************************"); @@ -81,106 +80,6 @@ describe("persistence > insert operations", () => { }))); - it("should insert entity when cascade option is set", () => Promise.all(connections.map(async connection => { - - // create first category and post and save them - const category1 = new Category(); - category1.name = "Category saved by cascades #1"; - - const post1 = new Post(); - post1.title = "Hello Post #1"; - post1.category = category1; - - await connection.entityManager.persist(post1); - - // create second category and post and save them - const category2 = new Category(); - category2.name = "Category saved by cascades #2"; - - const post2 = new Post(); - post2.title = "Hello Post #2"; - post2.category = category2; - - await connection.entityManager.persist(post2); - - // now check - const posts = await connection.entityManager.find(Post, { - alias: "post", - innerJoinAndSelect: { - category: "post.category" - }, - orderBy: { - "post.id": "ASC" - } - }); - - posts.should.be.eql([{ - id: 1, - title: "Hello Post #1", - category: { - id: 1, - name: "Category saved by cascades #1" - } - }, { - id: 2, - title: "Hello Post #2", - category: { - id: 2, - name: "Category saved by cascades #2" - } - }]); - }))); - - it("should insert from inverse side when cascade option is set", () => Promise.all(connections.map(async connection => { - - // create first post and category and save them - const post1 = new Post(); - post1.title = "Hello Post #1"; - - const category1 = new Category(); - category1.name = "Category saved by cascades #1"; - category1.posts = [post1]; - - await connection.entityManager.persist(category1); - - // create first post and category and save them - const post2 = new Post(); - post2.title = "Hello Post #2"; - - const category2 = new Category(); - category2.name = "Category saved by cascades #2"; - category2.posts = [post2]; - - await connection.entityManager.persist(category2); - - // now check - const posts = await connection.entityManager.find(Post, { - alias: "post", - innerJoinAndSelect: { - category: "post.category" - }, - orderBy: { - "post.id": "ASC" - } - }); - - posts.should.be.eql([{ - id: 1, - title: "Hello Post #1", - category: { - id: 1, - name: "Category saved by cascades #1" - } - }, { - id: 2, - title: "Hello Post #2", - category: { - id: 2, - name: "Category saved by cascades #2" - } - }]); - }))); - }); }); \ No newline at end of file diff --git a/test/integration/sample2-one-to-one.ts b/test/integration/sample2-one-to-one.ts index 1539cdd93..2c437206e 100644 --- a/test/integration/sample2-one-to-one.ts +++ b/test/integration/sample2-one-to-one.ts @@ -19,7 +19,7 @@ describe("one-to-one", function() { // ------------------------------------------------------------------------- const options: ConnectionOptions = { - driver: createTestingConnectionOptions("postgres"), + driver: createTestingConnectionOptions("mysql"), entities: [Post, PostDetails, PostCategory, PostMetadata, PostImage, PostInformation, PostAuthor] }; @@ -356,6 +356,7 @@ describe("one-to-one", function() { .getSingleResult(); }).then(loadedPost => { + console.log("loadedPost: ", loadedPost); loadedPost.image.url = "new-logo.png"; return postRepository.persist(loadedPost); diff --git a/test/integration/sample3-many-to-one.ts b/test/integration/sample3-many-to-one.ts index 86e53af23..df4a84fa7 100644 --- a/test/integration/sample3-many-to-one.ts +++ b/test/integration/sample3-many-to-one.ts @@ -138,6 +138,7 @@ describe("many-to-one", function() { expectedPost.text = savedPost.text; expectedPost.title = savedPost.title; + expectedDetails.posts = []; expectedDetails.posts.push(expectedPost); return postDetailsRepository @@ -407,7 +408,7 @@ describe("many-to-one", function() { .getSingleResult(); }).then(loadedPost => { - loadedPost.metadata = undefined; + loadedPost.metadata = null; return postRepository.persist(loadedPost); }).then(() => { @@ -437,6 +438,7 @@ describe("many-to-one", function() { details = new PostDetails(); details.comment = "post details comment"; + details.posts = []; details.posts.push(newPost); return postDetailsRepository.persist(details).then(details => savedDetails = details); @@ -474,6 +476,7 @@ describe("many-to-one", function() { const expectedDetails = new PostDetails(); expectedDetails.id = savedDetails.id; expectedDetails.comment = savedDetails.comment; + expectedDetails.posts = []; expectedDetails.posts.push(new Post()); expectedDetails.posts[0].id = newPost.id; expectedDetails.posts[0].text = newPost.text; diff --git a/test/integration/sample4-many-to-many.ts b/test/integration/sample4-many-to-many.ts index 8607fc354..d368ec790 100644 --- a/test/integration/sample4-many-to-many.ts +++ b/test/integration/sample4-many-to-many.ts @@ -17,7 +17,7 @@ describe("many-to-many", function() { // ------------------------------------------------------------------------- const options: ConnectionOptions = { - driver: createTestingConnectionOptions("postgres"), + driver: createTestingConnectionOptions("mysql"), entities: [__dirname + "/../../sample/sample4-many-to-many/entity/*"], // logging: { // logQueries: true, @@ -71,6 +71,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.details = []; newPost.details.push(details); return postRepository.persist(newPost).then(post => savedPost = post); @@ -113,6 +114,7 @@ describe("many-to-many", function() { expectedPost.id = savedPost.id; expectedPost.text = savedPost.text; expectedPost.title = savedPost.title; + expectedPost.details = []; expectedPost.details.push(new PostDetails()); expectedPost.details[0].id = savedPost.details[0].id; expectedPost.details[0].authorName = savedPost.details[0].authorName; @@ -140,7 +142,8 @@ describe("many-to-many", function() { expectedPost.id = savedPost.id; expectedPost.text = savedPost.text; expectedPost.title = savedPost.title; - + + expectedDetails.posts = []; expectedDetails.posts.push(expectedPost); return postDetailsRepository @@ -193,6 +196,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.categories = []; newPost.categories.push(category); return postRepository.persist(newPost).then(post => savedPost = post); @@ -231,6 +235,7 @@ describe("many-to-many", function() { expectedPost.id = savedPost.id; expectedPost.title = savedPost.title; expectedPost.text = savedPost.text; + expectedPost.categories = []; expectedPost.categories.push(new PostCategory()); expectedPost.categories[0].id = savedPost.categories[0].id; expectedPost.categories[0].name = savedPost.categories[0].name; @@ -286,6 +291,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.details = []; newPost.details.push(details); return postRepository @@ -324,6 +330,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.details = []; newPost.details.push(details); return postRepository @@ -374,6 +381,7 @@ describe("many-to-many", function() { .persist(newImage) .then(image => { savedImage = image; + newPost.images = []; newPost.images.push(image); return postRepository.persist(newPost); @@ -423,6 +431,7 @@ describe("many-to-many", function() { .persist(newMetadata) .then(metadata => { savedMetadata = metadata; + newPost.metadatas = []; newPost.metadatas.push(metadata); return postRepository.persist(newPost); @@ -466,6 +475,7 @@ describe("many-to-many", function() { details = new PostDetails(); details.comment = "post details comment"; + details.posts = []; details.posts.push(newPost); return postDetailsRepository.persist(details).then(details => savedDetails = details); @@ -503,6 +513,7 @@ describe("many-to-many", function() { const expectedDetails = new PostDetails(); expectedDetails.id = savedDetails.id; expectedDetails.comment = savedDetails.comment; + expectedDetails.posts = []; expectedDetails.posts.push(new Post()); expectedDetails.posts[0].id = newPost.id; expectedDetails.posts[0].text = newPost.text; @@ -534,6 +545,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.details = []; newPost.details.push(details); return postRepository @@ -587,6 +599,7 @@ describe("many-to-many", function() { newPost = new Post(); newPost.text = "Hello post"; newPost.title = "this is post title"; + newPost.categories = []; newPost.categories.push(category1, category2); return postRepository