added functionality to create, initailize, merge and diff entities

This commit is contained in:
Umed Khudoiberdiev 2016-02-27 13:58:39 +05:00
parent 886212c873
commit 9cd8d5aced
19 changed files with 742 additions and 109 deletions

View File

@ -36,7 +36,7 @@ export class EntityCreator {
objectToEntity<Entity>(objects: any[], metadata: EntityMetadata, aliasMap: AliasMap,fetchProperty?: Object): Entity;
objectToEntity<Entity>(objects: any[], metadata: EntityMetadata, aliasMap: AliasMap, fetchOption?: boolean|Object): Entity {
return this.toEntity(objects, metadata, aliasMap.getMainAlias(), aliasMap);
return this.toEntity(objects, metadata, aliasMap.mainAlias(), aliasMap);
//return this.objectToEntity(object, metadata, fetchOption);
}

View File

@ -5,6 +5,7 @@ import {Image} from "./entity/Image";
import {ImageDetails} from "./entity/ImageDetails";
import {Cover} from "./entity/Cover";
import {Category} from "./entity/Category";
import {Chapter} from "./entity/Chapter";
// first create a connection
let options = {
@ -16,31 +17,73 @@ let options = {
autoSchemaCreate: true
};
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails, Cover, Category]).then(connection => {
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails, Cover, Category, Chapter]).then(connection => {
const postJson = {
id: 1,
text: "This is post about hello",
title: "hello",
details: {
id: 1,
id: 1, // changed
text: "This is post about hello", // changed
title: "hello", // changed
details: { // new relation added
id: 10, // new object persisted
comment: "This is post about hello",
meta: "about-hello"
}
meta: "about-hello!",
chapter: {
id: 1, // new object persisted
about: "part I"
},
categories: [{
id: 5, // new object persisted
description: "cat5"
}]
},
cover: null, // relation removed
images: [{ // new relation added
id: 4, // new object persisted
name: "post!.jpg",
secondaryPost: {
id: 2,
title: "secondary post"
}
}, { // secondaryPost relation removed
id: 3,
name: "post_2!.jpg", // changed
details: { // new relation added
id: 3, // new object persisted
meta: "sec image",
comment: "image sec"
}
}],
categories: [{ // two categories removed, new category added
id: 4, // new persisted
description: "cat2"
}]
};
let postRepository = connection.getRepository<Post>(Post);
let entity = postRepository.create(postJson);
return postRepository.initialize(postJson)
.then(result => {
const mergedEntity = postRepository.merge(result, entity);
console.log("entity created from json: ", entity);
console.log("entity initialized from db: ", result);
console.log("entity merged: ", mergedEntity);
const diff = postRepository.difference(result, mergedEntity);
console.log("diff: ", diff);
//console.log("diff[0]: ", diff[0].removedRelations);
})
.catch(error => console.log(error.stack ? error.stack : error));
let qb = postRepository
.createQueryBuilder("post")
.addSelect("cover")
.addSelect("image")
.addSelect("imageDetails")
.addSelect("secondaryImage")
.addSelect("cover")
.addSelect("category")
.innerJoin("post.coverId", "cover")
.leftJoin("post.images", "image")
.leftJoin("post.secondaryImages", "secondaryImage")
.leftJoin("image.details", "imageDetails", "on", "imageDetails.meta=:meta")
.innerJoin("post.coverId", "cover")
.leftJoin("post.categories", "category", "on", "category.description=:description")
//.leftJoin(Image, "image", "on", "image.post=post.id")
//.where("post.id=:id")
@ -50,7 +93,7 @@ TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails,
return qb
.getSingleResult()
.then(result => console.log(result))
.then(post => console.log(post))
// .then(result => console.log(JSON.stringify(result, null, 4)))
.catch(error => console.log(error.stack ? error.stack : error));

View File

@ -1,7 +1,8 @@
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
import {Table} from "../../../src/decorator/Tables";
import {OneToMany, ManyToMany} from "../../../src/decorator/Relations";
import {OneToMany, ManyToMany, ManyToOne} from "../../../src/decorator/Relations";
import {Post} from "./Post";
import {PostDetails} from "./PostDetails";
@Table("sample2_category")
export class Category {
@ -15,4 +16,7 @@ export class Category {
@ManyToMany<Post>(false, type => Post, post => post.categories)
posts: Post[];
@ManyToOne<PostDetails>(_ => PostDetails, postDetails => postDetails.categories)
details: PostDetails;
}

View File

@ -0,0 +1,18 @@
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
import {Table} from "../../../src/decorator/Tables";
import {OneToMany} from "../../../src/decorator/Relations";
import {PostDetails} from "./PostDetails";
@Table("sample2_chapter")
export class Chapter {
@PrimaryColumn("int", { autoIncrement: true })
id: number;
@Column()
about: string;
@OneToMany<PostDetails>(type => PostDetails, postDetails => postDetails.chapter)
postDetails: PostDetails[];
}

View File

@ -16,10 +16,16 @@ export class Image {
@ManyToOne<Post>(() => Post, post => post.images)
post: Post;
@ManyToOne<Post>(() => Post, post => post.secondaryImages)
@ManyToOne<Post>(() => Post, post => post.secondaryImages, {
isCascadeInsert: true
})
secondaryPost: Post;
@OneToOne<ImageDetails>(true, () => ImageDetails, details => details.image)
@OneToOne<ImageDetails>(true, () => ImageDetails, details => details.image, {
isCascadeInsert: true,
isCascadeUpdate: true,
isCascadeRemove: true
})
details: ImageDetails;
}

View File

@ -22,27 +22,39 @@ export class Post {
})
text: string;
@OneToOne<PostDetails>(true, () => PostDetails, details => details.post)
@OneToOne<PostDetails>(true, () => PostDetails, details => details.post, {
isCascadeInsert: true,
isCascadeUpdate: true,
isCascadeRemove: true
})
details: PostDetails;
@OneToMany<Image>(type => Image, image => image.post)
@OneToMany<Image>(type => Image, image => image.post, {
isCascadeInsert: true,
isCascadeUpdate: true,
isCascadeRemove: true
})
images: Image[];
@OneToMany<Image>(type => Image, image => image.secondaryPost)
secondaryImages: Image[];
@ManyToOne<Cover>(type => Cover, cover => cover.posts, {
name: "coverId"
name: "coverId",
isCascadeInsert: true,
isCascadeRemove: true
})
cover: Cover;
/*@Column({
nullable: true,
type: "int"
@Column("int", {
nullable: true
})
coverId: number;*/
coverId: number;
@ManyToMany<Category>(true, type => Category, category => category.posts)
categories: Category;
@ManyToMany<Category>(true, type => Category, category => category.posts, {
isCascadeInsert: true,
isCascadeRemove: true
})
categories: Category[];
}

View File

@ -1,7 +1,9 @@
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
import {Table} from "../../../src/decorator/Tables";
import {OneToOne} from "../../../src/decorator/Relations";
import {OneToOne, OneToMany, ManyToOne} from "../../../src/decorator/Relations";
import {Post} from "./Post";
import {Chapter} from "./Chapter";
import {Category} from "./Category";
@Table("sample2_post_details")
export class PostDetails {
@ -15,7 +17,19 @@ export class PostDetails {
@Column()
comment: string;
@OneToOne<Post>(false, () => Post, post => post.details)
@OneToOne<Post>(false, type => Post, post => post.details)
post: Post;
@OneToMany<Category>(type => Category, category => category.details, {
isCascadeInsert: true,
isCascadeRemove: true
})
categories: Category[];
@ManyToOne<Chapter>(_ => Chapter, chapter => chapter.postDetails, {
isCascadeInsert: true,
isCascadeRemove: true
})
chapter: Chapter;
}

View File

@ -32,7 +32,7 @@ export function Column(typeOrOptions?: string|ColumnOptions, options?: ColumnOpt
// todo: need proper type validation here
const metadata = new ColumnMetadata(object.constructor, propertyName, false, false, false, options);
const metadata = new ColumnMetadata(object.constructor, propertyName, false, false, false, false, options);
defaultMetadataStorage.addColumnMetadata(metadata);
};
}
@ -66,7 +66,7 @@ export function PrimaryColumn(typeOrOptions?: string|ColumnOptions, options?: Co
// todo: need proper type validation here
const metadata = new ColumnMetadata(object.constructor, propertyName, true, false, false, options);
const metadata = new ColumnMetadata(object.constructor, propertyName, true, false, false, false, options);
defaultMetadataStorage.addColumnMetadata(metadata);
};
}

View File

@ -108,7 +108,7 @@ export class EntityMetadataBuilder {
oldColumnName: relation.oldColumnName,
nullable: relation.isNullable
};
relationalColumn = new ColumnMetadata(metadata.target, relation.name, false, false, false, options);
relationalColumn = new ColumnMetadata(metadata.target, relation.name, false, false, false, true, options);
metadata.columns.push(relationalColumn);
}
@ -145,8 +145,8 @@ export class EntityMetadataBuilder {
name: inverseSideMetadata.table.name + "_" + inverseSideMetadata.primaryColumn.name
};
const columns = [
new ColumnMetadata(null, null, false, false, false, column1options),
new ColumnMetadata(null, null, false, false, false, column2options)
new ColumnMetadata(null, null, false, false, false, false, column1options),
new ColumnMetadata(null, null, false, false, false, false, column2options)
];
const foreignKeys = [
new ForeignKeyMetadata(tableMetadata, [columns[0]], metadata.table, [metadata.primaryColumn]),

View File

@ -62,6 +62,11 @@ export class ColumnMetadata extends PropertyMetadata {
*/
private _isUpdateDate: boolean = false;
/**
* Indicates if column will contain an updated date or not.
*/
private _isVirtual: boolean = false;
/**
* Extra sql definition for the given column.
*/
@ -86,14 +91,22 @@ export class ColumnMetadata extends PropertyMetadata {
isPrimaryKey: boolean,
isCreateDate: boolean,
isUpdateDate: boolean,
isVirtual: boolean,
options: ColumnOptions) {
super(target, propertyName);
this._isPrimary = isPrimaryKey;
this._isCreateDate = isCreateDate;
this._isUpdateDate = isUpdateDate;
this._name = options.name;
this._type = this.convertType(options.type);
if (isPrimaryKey)
this._isPrimary = isPrimaryKey;
if (isCreateDate)
this._isCreateDate = isCreateDate;
if (isUpdateDate)
this._isUpdateDate = isUpdateDate;
if (isVirtual)
this._isVirtual = isVirtual;
if (options.name)
this._name = options.name;
if (options.type)
this._type = this.convertType(options.type);
if (options.length)
this._length = options.length;
@ -157,6 +170,10 @@ export class ColumnMetadata extends PropertyMetadata {
return this._isUpdateDate;
}
get isVirtual(): boolean {
return this._isVirtual;
}
get columnDefinition(): string {
return this._columnDefinition;
}

View File

@ -44,6 +44,10 @@ export class EntityMetadata {
// Accessors
// -------------------------------------------------------------------------
get name(): string {
return (<any> this._table.target).name;
}
get target(): Function {
return this._table.target;
}

View File

@ -1,7 +1,7 @@
import {Alias} from "./alias/Alias";
import {AliasMap} from "./alias/AliasMap";
import {Connection} from "../connection/Connection";
import {RawSqlResultsToObjectTransformer} from "./transformer/RawSqlResultsToObjectTransformer";
import {RawSqlResultsToEntityTransformer} from "./transformer/RawSqlResultsToEntityTransformer";
export interface Join {
alias: Alias;
@ -16,7 +16,7 @@ export class QueryBuilder<Entity> {
// Pirvate properties
// -------------------------------------------------------------------------
private aliasMap: AliasMap;
private _aliasMap: AliasMap;
private type: "select"|"update"|"delete";
private selects: string[] = [];
private froms: { alias: Alias };
@ -34,7 +34,15 @@ export class QueryBuilder<Entity> {
// -------------------------------------------------------------------------
constructor(private connection: Connection) {
this.aliasMap = new AliasMap(connection.metadatas);
this._aliasMap = new AliasMap(connection.metadatas);
}
// -------------------------------------------------------------------------
// Accessors
// -------------------------------------------------------------------------
get aliasMap() {
return this._aliasMap;
}
// -------------------------------------------------------------------------
@ -83,17 +91,31 @@ export class QueryBuilder<Entity> {
//from(entityOrTableName: Function|string, alias: string): this {
const aliasObj = new Alias(alias);
aliasObj.target = entity;
this.aliasMap.addMainAlias(aliasObj);
this._aliasMap.addMainAlias(aliasObj);
this.froms = { alias: aliasObj };
return this;
}
innerJoinAndSelect(property: string, alias: string, conditionType?: "on"|"with", condition?: string): this;
innerJoinAndSelect(entity: Function, alias: string, conditionType?: "on"|"with", condition?: string): this;
innerJoinAndSelect(entityOrProperty: Function|string, alias: string, conditionType?: "on"|"with", condition?: string): this {
this.addSelect(alias);
return this.join("inner", entityOrProperty, alias, conditionType, condition);
}
innerJoin(property: string, alias: string, conditionType?: "on"|"with", condition?: string): this;
innerJoin(entity: Function, alias: string, conditionType?: "on"|"with", condition?: string): this;
innerJoin(entityOrProperty: Function|string, alias: string, conditionType?: "on"|"with", condition?: string): this {
return this.join("inner", entityOrProperty, alias, conditionType, condition);
}
leftJoinAndSelect(property: string, alias: string, conditionType?: "on"|"with", condition?: string): this;
leftJoinAndSelect(entity: Function, alias: string, conditionType?: "on"|"with", condition?: string): this;
leftJoinAndSelect(entityOrProperty: Function|string, alias: string, conditionType: "on"|"with" = "on", condition?: string): this {
this.addSelect(alias);
return this.join("left", entityOrProperty, alias, conditionType, condition);
}
leftJoin(property: string, alias: string, conditionType?: "on"|"with", condition?: string): this;
leftJoin(entity: Function, alias: string, conditionType?: "on"|"with", condition?: string): this;
leftJoin(entityOrProperty: Function|string, alias: string, conditionType: "on"|"with" = "on", condition?: string): this {
@ -106,7 +128,7 @@ export class QueryBuilder<Entity> {
join(joinType: "inner"|"left", entityOrProperty: Function|string, alias: string, conditionType: "on"|"with" = "on", condition?: string): this {
const aliasObj = new Alias(alias);
this.aliasMap.addAlias(aliasObj);
this._aliasMap.addAlias(aliasObj);
if (entityOrProperty instanceof Function) {
aliasObj.target = entityOrProperty;
@ -204,21 +226,22 @@ export class QueryBuilder<Entity> {
return sql;
}
execute<T>(): Promise<T> {
return this.connection.driver.query<T>(this.getSql());
execute(): Promise<void> {
return this.connection.driver.query(this.getSql()).then(() => {});
}
getScalarResults(): Promise<any[]> {
return this.execute<any[]>().then(results => this.rawResultsToObjects(results));
getScalarResults<T>(): Promise<T[]> {
return this.connection.driver.query<T[]>(this.getSql());
}
getSingleScalarResult(): Promise<any> {
getSingleScalarResult<T>(): Promise<T> {
return this.getScalarResults().then(results => results[0]);
}
getResults(): Promise<Entity[]> {
return this.getScalarResults().then(objects => this.objectsToEntities(objects));
return this.connection.driver
.query<any[]>(this.getSql())
.then(results => this.rawResultsToEntities(results));
}
getSingleResult(): Promise<Entity> {
@ -229,18 +252,14 @@ export class QueryBuilder<Entity> {
// Protected Methods
// -------------------------------------------------------------------------
protected rawResultsToObjects(results: any[]) {
const transformer = new RawSqlResultsToObjectTransformer(this.aliasMap);
protected rawResultsToEntities(results: any[]) {
const transformer = new RawSqlResultsToEntityTransformer(this._aliasMap);
return transformer.transform(results);
}
protected objectsToEntities(entities: any[]) {
return entities;
}
protected createSelectExpression() {
// todo throw exception if selects or from is missing
const metadata = this.aliasMap.getEntityMetadataByAlias(this.froms.alias);
const metadata = this._aliasMap.getEntityMetadataByAlias(this.froms.alias);
const tableName = metadata.table.name;
const alias = this.froms.alias.name;
const allSelects: string[] = [];
@ -255,7 +274,7 @@ export class QueryBuilder<Entity> {
this.joins
.filter(join => this.selects.indexOf(join.alias.name) !== -1)
.forEach(join => {
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
const joinMetadata = this._aliasMap.getEntityMetadataByAlias(join.alias);
joinMetadata.columns.forEach(column => {
allSelects.push(join.alias.name + "." + column.name + " AS " + join.alias.name + "_" + column.propertyName);
});
@ -297,12 +316,12 @@ export class QueryBuilder<Entity> {
const joinType = join.type === "inner" ? "INNER" : "LEFT";
const appendedCondition = join.condition ? " AND " + join.condition : "";
const parentAlias = join.alias.parentAliasName;
const parentMetadata = this.aliasMap.getEntityMetadataByAlias(this.aliasMap.findAliasByName(parentAlias));
const parentMetadata = this._aliasMap.getEntityMetadataByAlias(this._aliasMap.findAliasByName(parentAlias));
const parentTable = parentMetadata.table.name;
const parentTableColumn = parentMetadata.primaryColumn.name;
const relation = parentMetadata.findRelationWithDbName(join.alias.parentPropertyName);
const junctionMetadata = relation.junctionEntityMetadata;
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
const joinMetadata = this._aliasMap.getEntityMetadataByAlias(join.alias);
const joinTable = joinMetadata.table.name;
const joinTableColumn = joinMetadata.primaryColumn.name;

View File

@ -21,7 +21,7 @@ export class AliasMap {
// -------------------------------------------------------------------------
addMainAlias(alias: Alias) {
const mainAlias = this.getMainAlias();
const mainAlias = this.mainAlias;
if (mainAlias)
this.aliases.splice(this.aliases.indexOf(mainAlias), 1);
@ -33,7 +33,7 @@ export class AliasMap {
this.aliases.push(alias);
}
getMainAlias() {
get mainAlias() {
return this.aliases.find(alias => alias.isMain);
}

View File

@ -0,0 +1,61 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {QueryBuilder} from "../QueryBuilder";
interface LoadMap {
name: string;
child: LoadMap[];
}
/**
* Transforms plain old javascript object
* Entity is constructed based on its entity metadata.
*/
export class PlainObjectToDatabaseEntityTransformer<Entity> {
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
transform(object: any, metadata: EntityMetadata, queryBuilder: QueryBuilder<Entity>): Promise<Entity> {
// if object does not have id then nothing to load really
if (!metadata.hasPrimaryKey || !object[metadata.primaryColumn.name])
return null;
const alias = queryBuilder.aliasMap.mainAlias.name;
const needToLoad = this.buildLoadMap(object, metadata);
this.join(queryBuilder, needToLoad, alias);
return queryBuilder
.where(alias + "." + metadata.primaryColumn.name + "=:id")
.setParameter("id", object[metadata.primaryColumn.name])
.getSingleResult();
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private join(qb: QueryBuilder<Entity>, needToLoad: LoadMap[], parentAlias: string) {
needToLoad.forEach(i => {
const alias = parentAlias + "_" + i.name;
qb.leftJoinAndSelect(parentAlias + "." + i.name, alias);
if (i.child && i.child.length)
this.join(qb, i.child, alias);
});
}
private buildLoadMap(object: any, metadata: EntityMetadata): LoadMap[] {
return metadata.relations
.filter(relation => object.hasOwnProperty(relation.propertyName))
.map(relation => {
let value = object[relation.propertyName];
if (value instanceof Array)
value = Object.assign({}, ...value);
const child = value ? this.buildLoadMap(value, relation.relatedEntityMetadata) : [];
return <LoadMap> { name: relation.name, child: child };
});
}
}

View File

@ -0,0 +1,68 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
/**
* Transforms plain old javascript object
* Entity is constructed based on its entity metadata.
*/
export class PlainObjectToNewEntityTransformer {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor() {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
transform(object: any, metadata: EntityMetadata): any {
return this.groupAndTransform(object, metadata);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
/**
* Since db returns a duplicated rows of the data where accuracies of the same object can be duplicated
* we need to group our result and we must have some unique id (primary key in our case)
*/
private groupAndTransform(object: any, metadata: EntityMetadata) {
const entity = metadata.create();
// copy regular column properties from the given object
metadata.columns
.filter(column => object.hasOwnProperty(column.propertyName))
.forEach(column => entity[column.propertyName] = object[column.propertyName]); // todo: also need to be sure that type is correct
// if relation is loaded then go into it recursively and transform its values too
metadata.relations
.filter(relation => object.hasOwnProperty(relation.propertyName))
.forEach(relation => {
const relationMetadata = relation.relatedEntityMetadata;
if (!relationMetadata)
throw new Error("Relation metadata for the relation " + (<any> metadata.target).name + "#" + relation.propertyName + " is missing");
if (relation.isManyToMany || relation.isOneToMany) {
if (object[relation.propertyName] instanceof Array) {
entity[relation.propertyName] = object[relation.propertyName].map((subObject: any) => {
return this.groupAndTransform(subObject, relationMetadata);
});
} else {
entity[relation.propertyName] = object[relation.propertyName];
}
} else {
if (object[relation.propertyName]) {
entity[relation.propertyName] = this.groupAndTransform(object[relation.propertyName], relationMetadata);
} else {
entity[relation.propertyName] = object[relation.propertyName];
}
}
});
return entity;
}
}

View File

@ -4,13 +4,10 @@ import * as _ from "lodash";
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
/**
* Transforms raw sql results returned from the database into object. Object is constructed for entity
* based on the entity metadata.
* Transforms raw sql results returned from the database into entity object.
* Entity is constructed based on its entity metadata.
*/
export class RawSqlResultsToObjectTransformer {
// todo: add check for property relation with id as a column
// todo: create metadata or do it later?
export class RawSqlResultsToEntityTransformer {
// -------------------------------------------------------------------------
// Constructor
@ -24,7 +21,7 @@ export class RawSqlResultsToObjectTransformer {
// -------------------------------------------------------------------------
transform(rawSqlResults: any[]): any[] {
return this.groupAndTransform(rawSqlResults, this.aliasMap.getMainAlias());
return this.groupAndTransform(rawSqlResults, this.aliasMap.mainAlias);
}
// -------------------------------------------------------------------------
@ -48,30 +45,35 @@ export class RawSqlResultsToObjectTransformer {
}
/**
* Transforms set of data results of the single value.
* Transforms set of data results into single entity.
*/
private transformIntoSingleResult(rawSqlResults: any[], alias: Alias, metadata: EntityMetadata) {
const jsonObject: any = {};
const entity: any = metadata.create();
let hasData = false;
// get value from columns selections and put them into object
metadata.columns.forEach(column => {
const valueInObject = alias.getColumnValue(rawSqlResults[0], column); // we use zero index since its grouped data
if (valueInObject && column.propertyName)
jsonObject[column.propertyName] = valueInObject;
if (valueInObject && column.propertyName && !column.isVirtual) {
entity[column.propertyName] = valueInObject;
hasData = true;
}
});
// if relation is loaded then go into it recursively and transform its values too
metadata.relations.forEach(relation => {
const relationAlias = this.aliasMap.findAliasByParent(alias.name, relation.propertyName);
const relationAlias = this.aliasMap.findAliasByParent(alias.name, relation.name);
if (relationAlias) {
const relatedObjects = this.groupAndTransform(rawSqlResults, relationAlias);
const result = (relation.isManyToOne || relation.isOneToOne) ? relatedObjects[0] : relatedObjects;
if (result)
jsonObject[relation.propertyName] = result;
const relatedEntities = this.groupAndTransform(rawSqlResults, relationAlias);
const result = (relation.isManyToOne || relation.isOneToOne) ? relatedEntities[0] : relatedEntities;
if (result) {
entity[relation.propertyName] = result;
hasData = true;
}
}
});
return Object.keys(jsonObject).length > 0 ? jsonObject : null;
return hasData ? entity : null;
}
}

View File

@ -0,0 +1,215 @@
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {EntityDifferenceMap} from "./Repository";
interface EntityWithId {
id: any;
entity: any;
}
interface UpdateOperation {
entity: any;
columns: ColumnMetadata[];
}
export class EntityPersistOperationsBuilder {
// 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
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* 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[] = [];
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);
console.log("---------------------------------------------------------");
console.log("DB ENTITIES");
console.log("---------------------------------------------------------");
console.log(dbEntities);
console.log("---------------------------------------------------------");
console.log("ALL NEW ENTITIES");
console.log("---------------------------------------------------------");
console.log(allEntities);
console.log("---------------------------------------------------------");
console.log("INSERTED ENTITIES");
console.log("---------------------------------------------------------");
console.log(insertedEntities);
console.log("---------------------------------------------------------");
console.log("REMOVED ENTITIES");
console.log("---------------------------------------------------------");
console.log(removedEntities);
console.log("---------------------------------------------------------");
console.log("UPDATED ENTITIES");
console.log("---------------------------------------------------------");
console.log(updatedEntities);
console.log("---------------------------------------------------------");
return diffMaps;
}
// -------------------------------------------------------------------------
// 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 findCascadeUpdateEntities(metadata: EntityMetadata, newEntity: any, dbEntity: any): UpdateOperation[] {
const updatedColumns = [{
entity: newEntity,
columns: this.diffColumns(metadata, newEntity, dbEntity)
}];
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, 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);
}
private findCascadeInsertedEntities(newEntity: any, metadata: EntityMetadata, dbEntities: any[]): any[] {
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));
});
} 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));
}
return insertedEntities;
}, []);
}
private findCascadeRemovedEntities(dbEntity: any, metadata: EntityMetadata, newEntities: any[]): any[] {
return metadata.relations
.filter(relation => !!dbEntity[relation.propertyName])
.reduce((removedEntities, relation) => {
const relationIdColumnName = relation.relatedEntityMetadata.primaryColumn.name;
const relMetadata = relation.relatedEntityMetadata;
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;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push(subEntity);
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(subEntity, relMetadata, newEntities));
});
} else {
const relationId = dbEntity[relation.propertyName][relationIdColumnName];
const isObjectRemoved = !newEntities.find(newEntity => {
return newEntity.id === relationId && newEntity.entity === relMetadata.name;
});
if (isObjectRemoved && relation.isCascadeRemove)
removedEntities.push(dbEntity[relation.propertyName]);
removedEntities = removedEntities.concat(this.findCascadeRemovedEntities(dbEntity[relation.propertyName], relMetadata, newEntities));
}
return removedEntities;
}, []);
}
/**
* Extracts unique objects from given entity and all its downside relations.
*/
private extractObjectsById(entity: any, metadata: EntityMetadata): EntityWithId[] {
return metadata.relations
.filter(relation => !!entity[relation.propertyName])
.map(relation => {
const relMetadata = relation.relatedEntityMetadata;
if (!(entity[relation.propertyName] instanceof Array))
return this.extractObjectsById(entity[relation.propertyName], relMetadata);
return entity[relation.propertyName]
.map((subEntity: any) => this.extractObjectsById(subEntity, relMetadata))
.reduce((col1: any[], col2: any[]) => col1.concat(col2), []); // flatten
})
.reduce((col1: any[], col2: any[]) => col1.concat(col2), []) // flatten
.concat([{
id: entity[metadata.primaryColumn.name],
entity: entity.constructor.name
}])
.filter((entity: any, index: number, allEntities: any[]) => allEntities.indexOf(entity) === index); // unique
}
}

View File

@ -2,10 +2,38 @@ import {Connection} from "../connection/Connection";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
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";
// 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.
*/
@ -50,9 +78,156 @@ export class Repository<Entity> {
/**
* Creates a new entity.
*/
create(): Entity {
create(copyFrom?: any): Entity {
if (copyFrom) {
const transformer = new PlainObjectToNewEntityTransformer();
return transformer.transform(copyFrom, this.metadata);
}
return <Entity> this.metadata.create();
}
initialize(object: any): Promise<Entity> {
const transformer = new PlainObjectToDatabaseEntityTransformer();
const queryBuilder = this.createQueryBuilder(this.metadata.table.name);
return transformer.transform(object, this.metadata, queryBuilder);
}
merge(entity1: Entity, entity2: Entity): 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[] {
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 };
});
}).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), []);
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);
});
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);
});
});
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)
});
}
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
});
}
}
/*copyEntity(entity1: Entity, entity2: Entity) {
this.metadata.columns
}*/
/**
* Creates a entities from the given array of plain javascript objects. If fetchAllData param is specified then
* entities data will be loaded from the database first, then filled with given json data.
*/
createMany(copyFromObjects: any[]): Entity[] {
return copyFromObjects.map(object => this.create(object));
}
/**
* Checks if entity has an id.
@ -61,32 +236,6 @@ export class Repository<Entity> {
return entity && this.metadata.primaryColumn && entity.hasOwnProperty(this.metadata.primaryColumn.propertyName);
}
/**
* Creates entity from the given json data. If fetchAllData param is specified then entity data will be
* loaded from the database first, then filled with given json data.
*/
createFromJson(json: any, fetchProperty?: boolean): Promise<Entity>;
createFromJson(json: any, fetchConditions?: Object): Promise<Entity>;
createFromJson(json: any, fetchOption?: boolean|Object): Promise<Entity> {
return Promise.resolve<Entity>(null); // todo
/* const creator = new EntityCreator(this.connection);
return creator.createFromJson<Entity>(json, this.metadata, fetchOption);*/
}
/**
* Creates a entities from the given array of plain javascript objects. If fetchAllData param is specified then
* entities data will be loaded from the database first, then filled with given json data.
*/
createManyFromJson(objects: any[], fetchProperties?: boolean[]): Promise<Entity[]>;
createManyFromJson(objects: any[], fetchConditions?: Object[]): Promise<Entity[]>;
createManyFromJson(objects: any[], fetchOption?: boolean[]|Object[]): Promise<Entity[]> {
return Promise.resolve<Entity[]>(null); // todo
/*return Promise.all(objects.map((object, key) => {
const fetchConditions = (fetchOption && fetchOption[key]) ? fetchOption[key] : undefined;
return this.createFromJson(object, fetchConditions);
}));*/
}
/**
* Creates a new query builder that can be used to build an sql query.
*/
@ -143,9 +292,9 @@ export class Repository<Entity> {
* Saves a given entity. If entity is not inserted yet then it inserts a new entity.
* If entity already inserted then performs its update.
*/
persist(entity: Entity/*, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>*/): Promise<Entity> {
//persist(entity: Entity/*, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>*/): Promise<Entity> {
// todo
return Promise.resolve<Entity>(null);
// return Promise.resolve<Entity>(null);
// if (!this.schema.isEntityTypeCorrect(entity))
// throw new BadEntityInstanceException(entity, this.schema.entityClass);
@ -158,7 +307,7 @@ export class Repository<Entity> {
.then(result => remover.executeRemoveOperations())
.then(result => remover.executeUpdateInverseSideRelationRemoveIds())
.then(result => entity);*/
}
// }
/*computeChangeSet(entity: Entity) {
// if there is no primary key - there is no way to determine if object needs to be updated or insert

View File

@ -28,6 +28,7 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
dbData.IS_NULLABLE !== isNullable ||
hasDbColumnAutoIncrement !== column.isAutoIncrement ||
hasDbColumnPrimaryIndex !== column.isPrimary;
}).map(column => {
const dbData = results.find(result => result.COLUMN_NAME === column.name);
const hasDbColumnPrimaryIndex = dbData.COLUMN_KEY.indexOf("PRI") !== -1;
@ -111,7 +112,7 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
}
createTableQuery(table: TableMetadata, columns: ColumnMetadata[]): Promise<void> {
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, true)).join(", ");
const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", ");
const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`;
return this.query(sql).then(() => {});
}
@ -130,7 +131,7 @@ export class MysqlSchemaBuilder extends SchemaBuilder {
c += " NOT NULL";
if (column.isPrimary === true && !skipPrimary)
c += " PRIMARY KEY";
if (column.isAutoIncrement === true && !skipPrimary)
if (column.isAutoIncrement === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.
c += " AUTO_INCREMENT";
if (column.comment)
c += " COMMENT '" + column.comment + "'";