implemented basic entity persistment

This commit is contained in:
Umed Khudoiberdiev 2016-02-29 02:44:35 +05:00
parent 9cd8d5aced
commit 35d363f887
5 changed files with 455 additions and 217 deletions

View File

@ -19,6 +19,74 @@ let options = {
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails, Cover, Category, Chapter]).then(connection => {
let postRepository = connection.getRepository<Post>(Post);
let postCover = new Cover();
postCover.url = "http://covers.com/post.jpg";
let details = new PostDetails();
details.meta = "hello";
details.comment = "wow";
let category1 = new Category();
category1.description = "about post1";
let category2 = new Category();
category2.description = "about post2";
let image = new Image();
image.name = "post.jpg";
let post = new Post();
post.title = "Hello post";
post.text = "Hello world of post#1";
post.cover = postCover;
post.details = details;
post.images.push(image);
post.categories = [category1, category2];
postRepository.persist(post).then(result => {
const qb = postRepository.createQueryBuilder("post")
.leftJoinAndSelect("post.details", "details")
.leftJoinAndSelect("post.images", "images")
// .leftJoinAndSelect("post.coverId", "coverId")
.leftJoinAndSelect("post.categories", "categories")
.where("post.id=:id")
.setParameter("id", 6);
return qb
.getSingleResult()
.then(post => {
console.log("loaded post: ", post);
let category1 = new Category();
category1.id = 12;
category1.description = "about cat#12";
let category2 = new Category();
category2.id = 52;
category2.description = "about cat#52";
let image = new Image();
image.name = "second image of the post";
//post
post.title = "This! is updated post$";
post.text = "Hello world of post#4";
post.categories = [category2, category1];
post.images.push(image);
return postRepository.persist(post);
})
.then(() => qb.getSingleResult())
.then(reloadedPost => console.log("reloadedPost: ", reloadedPost));
})
.then(result => console.log(result))
.catch(error => console.log(error.stack ? error.stack : error));
return;
const postJson = {
id: 1, // changed
text: "This is post about hello", // changed
@ -59,7 +127,6 @@ TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails,
}]
};
let postRepository = connection.getRepository<Post>(Post);
let entity = postRepository.create(postJson);
return postRepository.initialize(postJson)
.then(result => {

View File

@ -34,7 +34,7 @@ export class Post {
isCascadeUpdate: true,
isCascadeRemove: true
})
images: Image[];
images: Image[] = [];
@OneToMany<Image>(type => Image, image => image.secondaryPost)
secondaryImages: Image[];
@ -53,6 +53,7 @@ export class Post {
@ManyToMany<Category>(true, type => Category, category => category.posts, {
isCascadeInsert: true,
isCascadeUpdate: true,
isCascadeRemove: true
})
categories: Category[];

View File

@ -1,7 +1,6 @@
import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {Connection} from "../connection/Connection";
/**

View File

@ -1,17 +1,62 @@
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {EntityDifferenceMap} from "./Repository";
import {RelationMetadata} from "../metadata-builder/metadata/RelationMetadata";
import metadata = Reflect.metadata;
interface EntityWithId {
export interface EntityWithId {
id: any;
entity: any;
}
interface UpdateOperation {
export interface InsertOperation {
entity: any;
entityId: number;
}
export interface UpdateOperation {
entity: any;
columns: ColumnMetadata[];
}
export interface JunctionInsertOperation {
metadata: EntityMetadata;
entity1: any;
entity2: any;
}
export interface JunctionRemoveOperation {
metadata: EntityMetadata;
entity1: any;
entity2: any;
}
export class PersistOperation {
inserts: InsertOperation[];
removes: any[];
updates: UpdateOperation[];
junctionInserts: JunctionInsertOperation[];
junctionRemoves: JunctionRemoveOperation[];
constructor(inserts: any[],
removes: any[],
updates: UpdateOperation[],
junctionInserts: JunctionInsertOperation[],
junctionRemoves: JunctionRemoveOperation[]) {
this.inserts = inserts;
this.removes = removes;
this.updates = updates;
this.junctionInserts = junctionInserts;
this.junctionRemoves = junctionRemoves;
}
}
/*export class JunctionEntity {
relation: RelationMetadata;
entity1: any;
entity2: any;
}*/
export class EntityPersistOperationsBuilder {
// 1. collect all exist objects from the db entity
@ -28,6 +73,8 @@ export class EntityPersistOperationsBuilder {
// 5.1. compare with entities from the collection of db entities, find difference and generate a change set
// 5.2. check if relation has rights to do cascade operation and throw exception if it cannot
// 5.3.
// 6. go throw all relations and find junction
// 6.1.
// if relation has "all" then all of above:
// if relation has "insert" it can insert a new entity
@ -41,13 +88,15 @@ export class EntityPersistOperationsBuilder {
/**
* Finds columns and relations from entity2 which does not exist or does not match in entity1.
*/
difference(metadata: EntityMetadata, entity1: any, entity2: any): EntityDifferenceMap[] {
const diffMaps: EntityDifferenceMap[] = [];
difference(metadata: EntityMetadata, entity1: any, entity2: any): PersistOperation {
const dbEntities = this.extractObjectsById(entity1, metadata);
const allEntities = this.extractObjectsById(entity2, metadata);
const insertedEntities = this.findCascadeInsertedEntities(entity2, metadata, dbEntities);
const removedEntities = this.findCascadeRemovedEntities(entity1, metadata, allEntities);
const updatedEntities = this.findCascadeUpdateEntities(metadata, entity1, entity2);
const insertOperations = this.findCascadeInsertedEntities(metadata, entity2, dbEntities, null);
const removeOperations = this.findCascadeRemovedEntities(metadata, entity1, allEntities);
const updateOperations = this.findCascadeUpdateEntities(metadata, entity1, entity2, null);
const junctionInsertOperations = this.findJunctionInsertOperations(metadata, entity2, dbEntities);
const junctionRemoveOperations = this.findJunctionRemoveOperations(metadata, entity1, allEntities);
//const insertJunctionOperations = ;//this.a();
console.log("---------------------------------------------------------");
console.log("DB ENTITIES");
console.log("---------------------------------------------------------");
@ -59,106 +108,158 @@ export class EntityPersistOperationsBuilder {
console.log("---------------------------------------------------------");
console.log("INSERTED ENTITIES");
console.log("---------------------------------------------------------");
console.log(insertedEntities);
console.log(insertOperations);
console.log("---------------------------------------------------------");
console.log("REMOVED ENTITIES");
console.log("---------------------------------------------------------");
console.log(removedEntities);
console.log(removeOperations);
console.log("---------------------------------------------------------");
console.log("UPDATED ENTITIES");
console.log("---------------------------------------------------------");
console.log(updatedEntities);
console.log(updateOperations);
console.log("---------------------------------------------------------");
return diffMaps;
console.log("JUNCTION INSERT ENTITIES");
console.log("---------------------------------------------------------");
console.log(junctionInsertOperations);
console.log("---------------------------------------------------------");
console.log("JUNCTION REMOVE ENTITIES");
console.log("---------------------------------------------------------");
console.log(junctionRemoveOperations);
console.log("---------------------------------------------------------");
// now normalize inserted entities
// no need probably, since we cant rely on deepness because of recursion: insertOperations.sort((a, b) => a.deepness + b.deepness);
// todo: implement duplicates removal later
// remove duplicates from inserted entities (todo: shall we check if duplicates differ and throw exception?)
/*const uniqueInsertOperations = insertOperations.map(insertOperation => {
const otherInsertOperations = insertOperations.filter(o => o !== insertOperation && o.entity === insertOperation.entity);
const otherRelations = otherInsertOperations.reduce((relations, o) => {
return relations.concat(o.relations);
}, <RelationMetadata[]> []);
return <InsertOperation> {
entity: insertOperation.entity,
relations: insertOperation.relations.concat(otherRelations)
};
});*/
// sort inserts
return new PersistOperation(
insertOperations,
removeOperations,
updateOperations,
junctionInsertOperations,
junctionRemoveOperations
);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual)
.filter(column => newEntity[column.propertyName] !== dbEntity[column.propertyName]);
private findJunctionInsertOperations(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[]): JunctionInsertOperation[] {
const dbEntity = dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor.name === metadata.name;
});
return metadata.relations
.filter(relation => relation.isManyToMany)
.filter(relation => newEntity[relation.propertyName] instanceof Array)
.reduce((operations, relation) => {
const relationMetadata = relation.relatedEntityMetadata;
const relationIdProperty = relationMetadata.primaryColumn.name;
newEntity[relation.propertyName].map((subEntity: any) => {
const has = !dbEntity ||
!dbEntity.entity[relation.propertyName] ||
!dbEntity.entity[relation.propertyName].find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: newEntity,
entity2: subEntity
});
}
const subOperations = this.findJunctionInsertOperations(relationMetadata, subEntity, dbEntities);
operations = operations.concat(subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
}
private findCascadeUpdateEntities(metadata: EntityMetadata, newEntity: any, dbEntity: any): UpdateOperation[] {
const updatedColumns = [{
entity: newEntity,
columns: this.diffColumns(metadata, newEntity, dbEntity)
}];
private findJunctionRemoveOperations(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): JunctionInsertOperation[] {
if (!dbEntity) // if new entity is persisted then it does not have anything to be deleted
return [];
const newEntity = newEntities.find(newEntity => {
return newEntity.id === dbEntity[metadata.primaryColumn.name] && newEntity.entity.constructor.name === metadata.name;
});
return metadata.relations
.filter(relation => newEntity[relation.propertyName] && dbEntity[relation.propertyName])
.reduce((updatedColumns, relation) => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
if (newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].forEach((subEntity: any) => {
const subDbEntity = (dbEntity[relation.propertyName] as any[]).find(subDbEntity => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
.filter(relation => relation.isManyToMany)
.filter(relation => dbEntity[relation.propertyName] instanceof Array)
.reduce((operations, relation) => {
const relationMetadata = relation.relatedEntityMetadata;
const relationIdProperty = relationMetadata.primaryColumn.name;
dbEntity[relation.propertyName].map((subEntity: any) => {
const has = !newEntity ||
!newEntity.entity[relation.propertyName] ||
!newEntity.entity[relation.propertyName].find((e: any) => e[relationIdProperty] === subEntity[relationIdProperty]);
if (has) {
operations.push({
metadata: relation.junctionEntityMetadata,
entity1: dbEntity,
entity2: subEntity
});
if (subDbEntity) {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, subEntity, subDbEntity);
if (!relation.isCascadeUpdate)
throw new Error("Cascade updates are not allowed in " + metadata.name + "#" + relation.propertyName);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
});
} else {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, newEntity[relation.propertyName], dbEntity[relation.propertyName]);
if (updatedColumns.length > 0) {
if (!relation.isCascadeUpdate)
throw new Error("Cascade updates are not allowed in " + metadata.name + "#" + relation.propertyName);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
}
return updatedColumns;
}, updatedColumns);
const subOperations = this.findJunctionRemoveOperations(relationMetadata, subEntity, newEntities);
operations = operations.concat(subOperations);
});
return operations;
}, <JunctionInsertOperation[]> []);
}
private findCascadeInsertedEntities(newEntity: any, metadata: EntityMetadata, dbEntities: any[]): any[] {
private findCascadeInsertedEntities(metadata: EntityMetadata, newEntity: any, dbEntities: EntityWithId[], fromRelation: RelationMetadata): any[] {
const insertedEntities: any[] = [];
const isObjectNew = !dbEntities.find(dbEntity => {
return dbEntity.id === newEntity[metadata.primaryColumn.name] && dbEntity.entity.constructor.name === metadata.name;
});
if (isObjectNew && fromRelation && !fromRelation.isCascadeInsert)
throw new Error("Cascade inserts are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
if (isObjectNew)
insertedEntities.push({
entity: newEntity
});
return metadata.relations
.filter(relation => !!newEntity[relation.propertyName])
.reduce((insertedEntities, relation) => {
const relationIdColumnName = relation.relatedEntityMetadata.primaryColumn.name;
const relMetadata = relation.relatedEntityMetadata;
if (newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].forEach((subEntity: any) => {
const isObjectNew = !dbEntities.find(dbEntity => {
return dbEntity.id === subEntity[relationIdColumnName] && dbEntity.entity === relMetadata.name;
});
if (isObjectNew) {
if (!relation.isCascadeInsert)
throw new Error("Cascade inserts are not allowed in " + metadata.name + "#" + relation.propertyName);
insertedEntities.push(subEntity);
}
insertedEntities = insertedEntities.concat(this.findCascadeInsertedEntities(subEntity, relMetadata, dbEntities));
const value = newEntity[relation.propertyName];
if (value instanceof Array) {
value.forEach((subEntity: any) => {
const subInserted = this.findCascadeInsertedEntities(relMetadata, subEntity, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
});
} else {
const relationId = newEntity[relation.propertyName][relationIdColumnName];
const isObjectNew = !dbEntities.find(dbEntity => {
return dbEntity.id === relationId && dbEntity.entity === relMetadata.name;
});
if (isObjectNew) {
if (!relation.isCascadeInsert)
throw new Error("Cascade inserts are not allowed in " + metadata.name + "#" + relation.propertyName);
insertedEntities.push(newEntity[relation.propertyName]);
}
insertedEntities = insertedEntities.concat(this.findCascadeInsertedEntities(newEntity[relation.propertyName], relMetadata, dbEntities));
const subInserted = this.findCascadeInsertedEntities(relMetadata, value, dbEntities, relation);
insertedEntities = insertedEntities.concat(subInserted);
}
return insertedEntities;
}, []);
}
private findCascadeRemovedEntities(dbEntity: any, metadata: EntityMetadata, newEntities: any[]): any[] {
return insertedEntities;
}, insertedEntities);
}
private findCascadeRemovedEntities(metadata: EntityMetadata, dbEntity: any, newEntities: EntityWithId[]): any[] {
if (!dbEntity)
return [];
return metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.reduce((removedEntities, relation) => {
@ -167,22 +268,22 @@ export class EntityPersistOperationsBuilder {
if (dbEntity[relation.propertyName] instanceof Array) {
dbEntity[relation.propertyName].forEach((subEntity: any) => {
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === subEntity[relationIdColumnName] && newEntity.entity === relMetadata.name;
return newEntity.id === subEntity[relationIdColumnName] && newEntity.entity.constructor.name === relMetadata.name;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push(subEntity);
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(subEntity, relMetadata, newEntities));
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, subEntity, newEntities));
});
} else {
const relationId = dbEntity[relation.propertyName][relationIdColumnName];
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === relationId && newEntity.entity === relMetadata.name;
return newEntity.id === relationId && newEntity.entity.constructor.name === relMetadata.name;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push(dbEntity[relation.propertyName]);
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(dbEntity[relation.propertyName], relMetadata, newEntities));
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(relMetadata, dbEntity[relation.propertyName], newEntities));
}
return removedEntities;
@ -193,6 +294,9 @@ export class EntityPersistOperationsBuilder {
* Extracts unique objects from given entity and all its downside relations.
*/
private extractObjectsById(entity: any, metadata: EntityMetadata): EntityWithId[] {
if (!entity)
return [];
return metadata.relations
.filter(relation => !!entity[relation.propertyName])
.map(relation => {
@ -207,9 +311,56 @@ export class EntityPersistOperationsBuilder {
.reduce((col1: any[], col2: any[]) => col1.concat(col2), []) // flatten
.concat([{
id: entity[metadata.primaryColumn.name],
entity: entity.constructor.name
entity: entity//.constructor.name
}])
.filter((entity: any, index: number, allEntities: any[]) => allEntities.indexOf(entity) === index); // unique
}
private findCascadeUpdateEntities(metadata: EntityMetadata, dbEntity: any, newEntity: any, fromRelation: RelationMetadata): UpdateOperation[] {
if (!dbEntity)
return [];
const updatedEntities: any[] = [];
const diff = this.diffColumns(metadata, newEntity, dbEntity);
if (diff.length && fromRelation && !fromRelation.isCascadeUpdate)
throw new Error("Cascade updates are not allowed in " + metadata.name + "#" + fromRelation.propertyName);
if (diff.length) {
updatedEntities.push({
entity: newEntity,
columns: diff
});
}
return metadata.relations
.filter(relation => newEntity[relation.propertyName] && dbEntity[relation.propertyName])
.reduce((updatedColumns, relation) => {
const relMetadata = relation.relatedEntityMetadata;
const relationIdColumnName = relMetadata.primaryColumn.name;
if (newEntity[relation.propertyName] instanceof Array) {
newEntity[relation.propertyName].forEach((subEntity: any) => {
const subDbEntity = (dbEntity[relation.propertyName] as any[]).find(subDbEntity => {
return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName];
});
if (subDbEntity) {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, subDbEntity, subEntity, relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
});
} else {
const relationUpdatedColumns = this.findCascadeUpdateEntities(relMetadata, dbEntity[relation.propertyName], newEntity[relation.propertyName], relation);
updatedColumns = updatedColumns.concat(relationUpdatedColumns);
}
return updatedColumns;
}, updatedEntities);
}
private diffColumns(metadata: EntityMetadata, newEntity: any, dbEntity: any) {
return metadata.columns
.filter(column => !column.isVirtual)
.filter(column => newEntity[column.propertyName] !== dbEntity[column.propertyName]);
}
}

View File

@ -4,36 +4,15 @@ import {OrmBroadcaster} from "../subscriber/OrmBroadcaster";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {PlainObjectToNewEntityTransformer} from "../query-builder/transformer/PlainObjectToNewEntityTransformer";
import {PlainObjectToDatabaseEntityTransformer} from "../query-builder/transformer/PlainObjectToDatabaseEntityTransformer";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {RelationMetadata} from "../metadata-builder/metadata/RelationMetadata";
import {EntityPersistOperationsBuilder} from "./EntityPersistOperationsBuilder";
import {
EntityPersistOperationsBuilder, PersistOperation, JunctionInsertOperation,
InsertOperation, JunctionRemoveOperation, UpdateOperation
} from "./EntityPersistOperationsBuilder";
// todo: think how we can implement queryCount, queryManyAndCount
// todo: extract non safe methods from repository (removeById, removeByConditions)
interface RelationDifference {
value: any;
relation: RelationMetadata;
}
export interface EntityDifferenceMap {
entity: any;
columns: ColumnMetadata[];
changedRelations: RelationDifference[];
removedRelations: RelationDifference[];
addedRelations: RelationDifference[];
}
interface EntityWithId {
id: any;
entity: any;
}
interface UpdateOperation {
entity: any;
columns: ColumnMetadata[];
}
/**
* Repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
*/
@ -96,125 +75,166 @@ export class Repository<Entity> {
return Object.assign(this.metadata.create(), entity1, entity2);
}
// 1. collect all exist objects from the db entity
// 2. collect all objects from the new entity
// 3. first need to go throw all relations of the new entity and:
// 3.1. find all objects that are new (e.g. cascade="insert") by comparing ids from the exist objects
// 3.2. check if relation has rights to do cascade operation and throw exception if it cannot
// 3.3. save new objects for insert operation
// 4. second need to go throw all relations of the db entity and:
// 4.1. find all objects that are removed (e.g. cascade="remove") by comparing data with collected objects of the new entity
// 4.2. check if relation has rights to do cascade operation and throw exception if it cannot
// 4.3. save new objects for remove operation
// 5. third need to go throw collection of all new entities
// 5.1. compare with entities from the collection of db entities, find difference and generate a change set
// 5.2. check if relation has rights to do cascade operation and throw exception if it cannot
// 5.3.
// if relation has "all" then all of above:
// if relation has "insert" it can insert a new entity
// if relation has "update" it can only update related entity
// if relation has "remove" it can only remove related entity
/**
* Finds columns and relations from entity2 which does not exist or does not match in entity1.
*/
difference(entity1: Entity, entity2: Entity): EntityDifferenceMap[] {
difference(entity1: Entity, entity2: Entity): PersistOperation {
const builder = new EntityPersistOperationsBuilder();
return builder.difference(this.metadata, entity1, entity2);
}
findDifference(e1: any, e2: any, metadata: EntityMetadata, diffMaps: EntityDifferenceMap[]) {
const diffColumns = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => e1[column.propertyName] !== e2[column.propertyName]);
const changedRelations = metadata.relations
.filter(relation => relation.isOneToOne || relation.isManyToOne)
.filter(relation => e1[relation.propertyName] && e2[relation.propertyName])
.filter(relation => {
const relationId = relation.relatedEntityMetadata.primaryColumn.name;
return e1[relation.propertyName][relationId] !== e2[relation.propertyName][relationId];
})
.map(relation => ({ value: e2[relation.propertyName], relation: relation }));
const removedRelations = metadata.relations
.filter(relation => relation.isOneToOne || relation.isManyToOne)
.filter(relation => e1[relation.propertyName] && !e2[relation.propertyName])
.map(relation => ({ value: e2[relation.propertyName], relation: relation }));
const addedRelations = metadata.relations
.filter(relation => relation.isOneToOne || relation.isManyToOne)
.filter(relation => !e1[relation.propertyName] && e2[relation.propertyName])
.map(relation => ({ value: e2[relation.propertyName], relation: relation }));
const addedManyRelations = metadata.relations
.filter(relation => relation.isManyToMany || relation.isOneToMany)
.filter(relation => e2[relation.propertyName] instanceof Array)
.map(relation => {
const relationId = relation.relatedEntityMetadata.primaryColumn.name;
return e2[relation.propertyName].filter((e2Item: any) => {
if (!e1[relation.propertyName]) return false;
return !e1[relation.propertyName].find((e1Item: any) => e1Item[relationId] === e2Item[relationId]);
}).map((e2Item: any) => {
return { value: e2Item, relation: relation };
persist(entity: Entity) {
const promise = !this.hasId(entity) ? Promise.resolve(null) : this.initialize(entity);
//if (!this.hasId(entity)) { // do insert
return promise.then(dbEntity => {
const persistOperations = this.difference(dbEntity, entity);
// create update queries based on diff map
return Promise.all(persistOperations.inserts.map(operation => {
return this.insert(operation.entity).then((result: any) => {
operation.entityId = result.insertId;
});
}).reduce((a: EntityDifferenceMap[], b: EntityDifferenceMap[]) => a.concat(b), []);
const removedManyRelations = metadata.relations
.filter(relation => relation.isManyToMany || relation.isOneToMany)
.filter(relation => e2[relation.propertyName] instanceof Array)
.map(relation => {
const relationId = relation.relatedEntityMetadata.primaryColumn.name;
return e1[relation.propertyName].filter((e1Item: any) => {
if (!e2[relation.propertyName]) return false;
return !e2[relation.propertyName].find((e2Item: any) => e2Item[relationId] === e1Item[relationId]);
}).map((e1Item: any) => {
return { value: e1Item, relation: relation };
});
}).reduce((a: EntityDifferenceMap[], b: EntityDifferenceMap[]) => a.concat(b), []);
})).then(() => { // insert junction table insertions
metadata.relations
.filter(relation => e2[relation.propertyName])
.filter(relation => relation.isOneToOne || relation.isManyToOne)
.forEach(relation => {
const property = relation.propertyName;
this.findDifference(e1[property] || {}, e2[property], relation.relatedEntityMetadata, diffMaps);
});
return Promise.all(persistOperations.junctionInserts.map(junctionOperation => {
return this.insertJunctions(junctionOperation, persistOperations.inserts);
}));
}).then(() => { // remove junction table insertions
metadata.relations
.filter(relation => /*e1[relation.propertyName] && */e2[relation.propertyName] instanceof Array)
.filter(relation => relation.isManyToMany || relation.isOneToMany)
.forEach(relation => {
const relationId = relation.relatedEntityMetadata.primaryColumn.name;
const e1Items = e1[relation.propertyName] || [];
e2[relation.propertyName].map((e2Item: any) => {
const e1Item = e1Items.find((e1Item: any) => e1Item[relationId] === e2Item[relationId]);
this.findDifference(e1Item || {}, e2Item, relation.relatedEntityMetadata, diffMaps);
});
});
return Promise.all(persistOperations.junctionRemoves.map(junctionOperation => {
return this.removeJunctions(junctionOperation);
}));
}).then(() => {
return Promise.all(persistOperations.inserts.map(operation => {
const meta = this.connection.getMetadata(operation.entity.constructor);
const oneToOneManyToOneUpdates = Promise.all(meta.relations.map(relation => {
let insertOperationUpdates: Promise<any>, updateOperationUpdates: Promise<any>;
if (operation.entity[relation.propertyName] instanceof Array && relation.isOneToMany) {
insertOperationUpdates = Promise.all(persistOperations.inserts.filter(o => {
return operation.entity[relation.propertyName].indexOf(o.entity) !== -1;
}).map(o => {
const oMetadata = this.connection.getMetadata(o.entity.constructor);
const inverseRelation = relation.inverseRelation;
const query = `UPDATE ${oMetadata.table.name} SET ${inverseRelation.name}='${operation.entityId}' WHERE ${oMetadata.primaryColumn.name}='${o.entityId}'`;
return this.connection.driver.query(query);
}));
updateOperationUpdates = Promise.all(persistOperations.updates.filter(o => {
return operation.entity[relation.propertyName].indexOf(o.entity) !== -1;
}).map(o => {
const oMetadata = this.connection.getMetadata(o.entity.constructor);
const inverseRelation = relation.inverseRelation;
const id = operation.entity[meta.primaryColumn.name];
const query = `UPDATE ${oMetadata.table.name} SET ${inverseRelation.name}='${operation.entityId}' WHERE ${oMetadata.primaryColumn.name}='${id}'`;
return this.connection.driver.query(query);
}));
} else {
insertOperationUpdates = Promise.all(persistOperations.inserts.filter(o => {
return operation.entity[relation.propertyName] === o.entity; // only one-to-one and many-to-one
}).map(o => {
const query = `UPDATE ${meta.table.name} SET ${relation.name}='${o.entityId}' WHERE ${meta.primaryColumn.name}='${operation.entityId}'`;
return this.connection.driver.query(query);
}));
updateOperationUpdates = Promise.all(persistOperations.updates.filter(o => {
return operation.entity[relation.propertyName] === o.entity; // only one-to-one and many-to-one
}).map(o => {
const reverseMeta = this.connection.getMetadata(o.entity.constructor);
const id = o.entity[reverseMeta.primaryColumn.name];
const query = `UPDATE ${meta.table.name} SET ${relation.name}='${id}' WHERE ${meta.primaryColumn.name}='${operation.entityId}'`;
return this.connection.driver.query(query);
}));
}
return Promise.all([insertOperationUpdates, updateOperationUpdates]);
}));
return Promise.all([oneToOneManyToOneUpdates]);
}));
}).then(() => { // perform updates
return Promise.all(persistOperations.updates.map(updateOperation => {
return this.update(updateOperation);
}));
if (diffColumns.length > 0 || changedRelations.length > 0 || removedRelations.length > 0 || addedRelations.length > 0)
diffMaps.push({
entity: e2,
columns: diffColumns,
changedRelations: changedRelations,
removedRelations: removedRelations.concat(removedManyRelations),
addedRelations: addedRelations.concat(addedManyRelations)
});
//} else {
// do update
/*return this.initialize(entity).then(dbEntity => {
const persistOperations = this.difference(dbEntity, entity);
// create update queries based on diff map
return Promise.all(persistOperations.inserts.map(operation => {
return this.insert(operation.entity);
}));
});*/
//}
});
}
private update(updateOperation: UpdateOperation) {
const entity = updateOperation.entity;
const metadata = this.connection.getMetadata(entity.constructor);
const values = updateOperation.columns.map(column => {
return column.name + "='" + entity[column.propertyName] + "'";
});
const query = `UPDATE ${metadata.table.name} SET ${values} WHERE ${metadata.primaryColumn.name}='${metadata.getEntityId(entity)}'` ;
return this.connection.driver.query(query);
}
persist(entity: Entity) {
if (!this.hasId(entity)) {
// do insert
} else {
// do update
this.initialize(entity)
.then(dbEntity => {
const diffMap = this.difference(dbEntity, entity);
// create update queries based on diff map
});
}
private insert(entity: any) {
const metadata = this.connection.getMetadata(entity.constructor);
const columns = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => column.name);
/*const virtualColumns = metadata.columns
.filter(column => column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => column.name);*/
const values = metadata.columns
.filter(column => !column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => "'" + entity[column.propertyName] + "'");
/*const virtualValues = metadata.columns
.filter(column => column.isVirtual)
.filter(column => entity.hasOwnProperty(column.propertyName))
.map(column => "'" + entity[column.propertyName] + "'");
const allColumns = columns.concat(virtualColumns);
const allVolumes = values.concat(virtualValues);*/
const query = `INSERT INTO ${metadata.table.name}(${columns.join(",")}) VALUES (${values.join(",")})`;
return this.connection.driver.query(query);
}
private insertJunctions(junctionOperation: JunctionInsertOperation, insertOperations: InsertOperation[]) {
const junctionMetadata = junctionOperation.metadata;
const metadata1 = this.connection.getMetadata(junctionOperation.entity1.constructor);
const metadata2 = this.connection.getMetadata(junctionOperation.entity2.constructor);
const columns = junctionMetadata.columns.map(column => column.name);
const id1 = junctionOperation.entity1[metadata1.primaryColumn.name] || insertOperations.find(o => o.entity === junctionOperation.entity1).entityId;
const id2 = junctionOperation.entity2[metadata2.primaryColumn.name] || insertOperations.find(o => o.entity === junctionOperation.entity2).entityId;
const values = [id1, id2]; // todo: order may differ, find solution (column.table to compare with entity metadata table?)
const query = `INSERT INTO ${junctionMetadata.table.name}(${columns.join(",")}) VALUES (${values.join(",")})`;
return this.connection.driver.query(query);
}
private removeJunctions(junctionOperation: JunctionRemoveOperation) {
const junctionMetadata = junctionOperation.metadata;
const metadata1 = this.connection.getMetadata(junctionOperation.entity1.constructor);
const metadata2 = this.connection.getMetadata(junctionOperation.entity2.constructor);
const columns = junctionMetadata.columns.map(column => column.name);
const id1 = junctionOperation.entity1[metadata1.primaryColumn.name];
const id2 = junctionOperation.entity2[metadata2.primaryColumn.name];
const query = `DELETE FROM ${junctionMetadata.table.name} WHERE ${columns[0]}='${id1}' AND ${columns[1]}='${id2}'`;
return this.connection.driver.query(query);
}
/*copyEntity(entity1: Entity, entity2: Entity) {