implemented query builder and many-to-many queries

This commit is contained in:
Umed Khudoiberdiev 2016-02-23 00:56:45 +05:00
parent d01d2b6cea
commit a13ee96ba2
38 changed files with 472 additions and 352 deletions

View File

@ -12,3 +12,6 @@ Take a look on samples in [./sample](https://github.com/pleerock/typeorm/tree/ma
usages.
## Todos
* add partial selection support
* in query builder should we use property names or table names? (right now its mixed)

View File

@ -1,7 +1,7 @@
import {Connection} from "../../connection/Connection";
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";
import {AliasMap, Alias} from "../../driver/query-builder/QueryBuilder";
import {AliasMap, Alias} from "../../query-builder/QueryBuilder";
import * as _ from "lodash";
export class EntityCreator {
@ -142,26 +142,12 @@ export class EntityCreator {
const alias = aliasMap.findAliasByParent(mainAlias.name, relation.propertyName);
if (alias) {
//const id = relation.isManyToOne || relation.isOneToOne ? object[mainAlias.name + "_" + relation.name] : null;
const relatedEntities = this.toEntity(sqlResult, relation.relatedEntityMetadata, alias, aliasMap);
if (relation.isManyToOne || relation.isOneToOne) {
const relatedObject = relatedEntities.find(obj => {
return obj[relation.relatedEntityMetadata.primaryColumn.name] === object[mainAlias.name + "_" + relation.name];
});
if (relatedObject) {
jsonObject[relation.propertyName] = relatedObject;
isAnythingLoaded = true;
}
} else if (relation.isOneToMany) {
const relatedObjects = relatedEntities.filter(obj => {
return obj[relation.inverseSideProperty] === object[mainAlias.name + "_" + metadata.primaryColumn.name];
});
//if (relatedObjects) {
jsonObject[relation.propertyName] = relatedObjects;
isAnythingLoaded = true;
//}
const subSqlResult = sqlResult.filter(result => String(result[mainAlias.name + "_" + metadata.primaryColumn.name]) === key);
const relatedEntities = this.toEntity(subSqlResult, relation.relatedEntityMetadata, alias, aliasMap);
const res = relation.isManyToOne || relation.isOneToOne ? relatedEntities[0] : relatedEntities;
if (res) {
jsonObject[relation.propertyName] = res;
isAnythingLoaded = true;
}
}
});
@ -169,8 +155,6 @@ export class EntityCreator {
return isAnythingLoaded ? jsonObject : null;
}).filter(res => res !== null);
//return id ? final[0] : final;
}
private objectToEntity2(object: any, metadata: EntityMetadata, doFetchProperties?: boolean): Promise<any>;

View File

@ -2,9 +2,9 @@ import {Connection} from "../../connection/Connection";
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";
import {DocumentToDbObjectTransformer} from "./DocumentToDbObjectTransformer";
import {PersistOperation} from "./../operation/PersistOperation";
import {InverseSideUpdateOperation} from "./../operation/InverseSideUpdateOperation";
import {PersistOperationGrouppedByDeepness} from "../operation/PersistOperationGrouppedByDeepness";
import {PersistOperation} from "../../../odmhelpers/operation/PersistOperation";
import {InverseSideUpdateOperation} from "../../../odmhelpers/operation/InverseSideUpdateOperation";
import {PersistOperationGrouppedByDeepness} from "../../../odmhelpers/operation/PersistOperationGrouppedByDeepness";
export class EntityPersister {

View File

@ -1,10 +1,10 @@
import {Connection} from "../../connection/Connection";
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
import {DbObjectColumnValidator} from "./DbObjectColumnValidator";
import {ColumnTypeNotSupportedError} from "../error/ColumnTypeNotSupportedError";
import {PersistOperation} from "./../operation/PersistOperation";
import {ColumnTypeNotSupportedError} from "../../../odmhelpers/error/ColumnTypeNotSupportedError";
import {PersistOperation} from "../../../odmhelpers/operation/PersistOperation";
import {CascadeOptionUtils} from "../cascade/CascadeOptionUtils";
import {InverseSideUpdateOperation} from "../operation/InverseSideUpdateOperation";
import {InverseSideUpdateOperation} from "../../../odmhelpers/operation/InverseSideUpdateOperation";
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {RelationMetadata} from "../../metadata-builder/metadata/RelationMetadata";

View File

@ -2,10 +2,10 @@ import {DocumentSchema} from "../../schema/DocumentSchema";
import {Connection} from "../../connection/Connection";
import {RelationSchema} from "../../schema/RelationSchema";
import {CascadeOption, DynamicCascadeOptions} from "./../cascade/CascadeOption";
import {RemoveOperation} from "./../operation/RemoveOperation";
import {InverseSideUpdateOperation} from "./../operation/InverseSideUpdateOperation";
import {RemoveOperation} from "../../../odmhelpers/operation/RemoveOperation";
import {InverseSideUpdateOperation} from "../../../odmhelpers/operation/InverseSideUpdateOperation";
import {CascadeOptionUtils} from "../cascade/CascadeOptionUtils";
import {NoDocumentWithSuchIdError} from "../error/NoDocumentWithSuchIdError";
import {NoDocumentWithSuchIdError} from "../../../odmhelpers/error/NoDocumentWithSuchIdError";
import {ObjectID} from "mongodb";
/**

View File

@ -4,6 +4,7 @@ import {PostDetails} from "./entity/PostDetails";
import {Image} from "./entity/Image";
import {ImageDetails} from "./entity/ImageDetails";
import {Cover} from "./entity/Cover";
import {Category} from "./entity/Category";
// first create a connection
let options = {
@ -15,7 +16,7 @@ let options = {
autoSchemaCreate: true
};
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails, Cover]).then(connection => {
TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails, Cover, Category]).then(connection => {
const postJson = {
id: 1,
@ -35,22 +36,24 @@ TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails,
.addSelect("imageDetails")
.addSelect("secondaryImage")
.addSelect("cover")
.leftJoin("post.images", "image", "on", "image.post=post.id")
.leftJoin("post.secondaryImages", "secondaryImage", "on", "secondaryImage.secondaryPost=post.id")
.leftJoin("image.details", "imageDetails", "on", "imageDetails.id=image.details")
.innerJoin("post.cover", "cover", "on", "cover.id=post.cover")
.addSelect("category")
.leftJoin("post.images", "image")
.leftJoin("post.secondaryImages", "secondaryImage")
.leftJoin("image.details", "imageDetails", "on", "imageDetails.meta=:meta")
.innerJoin("post.cover", "cover")
.leftJoin("post.categories", "category", "on", "category.description=:description")
//.leftJoin(Image, "image", "on", "image.post=post.id")
//.where("post.id=:id")
.setParameter("id", 1);
.setParameter("id", 1)
.setParameter("description", "cat2")
.setParameter("meta", "sec image");
return postRepository
.queryMany(qb.getSql(), qb.generateAliasMap())
return qb
.getSingleResult()
.then(result => console.log(JSON.stringify(result, null, 4)))
.catch(err => console.log(err));
.catch(error => console.log(error.stack ? error.stack : error));
return;
let details = new PostDetails();
/*let details = new PostDetails();
details.comment = "This is post about hello";
details.meta = "about-hello";
@ -62,6 +65,6 @@ TypeORM.createMysqlConnection(options, [Post, PostDetails, Image, ImageDetails,
postRepository
.persist(post)
.then(post => console.log("Post has been saved"))
.catch(error => console.log("Cannot save. Error: ", error));
.catch(error => console.log("Cannot save. Error: ", error));*/
}).catch(error => console.log("Cannot connect: ", error));
}).catch(error => console.log(error.stack ? error.stack : error));

View File

@ -0,0 +1,18 @@
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
import {Table} from "../../../src/decorator/Tables";
import {OneToMany, ManyToMany} from "../../../src/decorator/Relations";
import {Post} from "./Post";
@Table("sample2_category")
export class Category {
@PrimaryColumn("int", { isAutoIncrement: true })
id: number;
@Column()
description: string;
@ManyToMany<Post>(false, type => Post, post => post.categories)
posts: Post[];
}

View File

@ -1,8 +1,10 @@
import {PrimaryColumn, Column} from "../../../src/decorator/Columns";
import {Table} from "../../../src/decorator/Tables";
import {OneToMany, ManyToOne} from "../../../src/decorator/Relations";
import {OneToMany, ManyToOne, ManyToMany, OneToOne} from "../../../src/decorator/Relations";
import {Image} from "./Image";
import {Cover} from "./Cover";
import {Category} from "./Category";
import {PostDetails} from "./PostDetails";
@Table("sample2_post")
export class Post {
@ -20,16 +22,19 @@ export class Post {
})
text: string;
/* @OneToOne<PostDetails>(true, () => PostDetails, details => details.post)
details: PostDetails;*/
@OneToOne<PostDetails>(true, () => PostDetails, details => details.post)
details: PostDetails;
@OneToMany<Image>(() => Image, image => image.post)
@OneToMany<Image>(type => Image, image => image.post)
images: Image[];
@OneToMany<Image>(() => Image, image => image.secondaryPost)
@OneToMany<Image>(type => Image, image => image.secondaryPost)
secondaryImages: Image[];
@ManyToOne<Cover>(() => Cover, cover => cover.posts)
@ManyToOne<Cover>(type => Cover, cover => cover.posts)
cover: Cover;
@ManyToMany<Category>(true, type => Category, category => category.posts)
categories: Category;
}

View File

@ -8,6 +8,7 @@ export interface ConnectionOptions {
*/
url?: string;
host?: string;
port?: number;
username?: string;
password?: string;
database?: string;

View File

@ -1,7 +1,8 @@
import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "./schema-builder/SchemaBuilder";
import {QueryBuilder} from "./query-builder/QueryBuilder";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {Connection} from "../connection/Connection";
/**
* Driver communicates with specific database.
@ -21,7 +22,7 @@ export interface Driver {
/**
* Creates a query builder which can be used to build an sql queries.
*/
createQueryBuilder(entityMetadatas: EntityMetadata[]): QueryBuilder;
createQueryBuilder<Entity>(connection: Connection): QueryBuilder<Entity>;
/**
* Creates a schema builder which can be used to build database/table schemas.

View File

@ -1,9 +1,10 @@
import {Driver} from "./Driver";
import {ConnectionOptions} from "../connection/ConnectionOptions";
import {SchemaBuilder} from "./schema-builder/SchemaBuilder";
import {QueryBuilder} from "./query-builder/QueryBuilder";
import {MysqlSchemaBuilder} from "./schema-builder/MysqlSchemaBuilder";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {MysqlSchemaBuilder} from "../schema-builder/MysqlSchemaBuilder";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {Connection} from "../connection/Connection";
/**
* This driver organizes work with mongodb database.
@ -45,8 +46,8 @@ export class MysqlDriver implements Driver {
/**
* Creates a query builder which can be used to build an sql queries.
*/
createQueryBuilder(entityMetadatas: EntityMetadata[]): QueryBuilder {
return new QueryBuilder(entityMetadatas);
createQueryBuilder<Entity>(connection: Connection): QueryBuilder<Entity> {
return new QueryBuilder<Entity>(connection);
}
/**

View File

@ -118,6 +118,13 @@ export class EntityMetadataBuilder {
});
});
// set inverse side (related) entity metadatas for all relation metadatas
entityMetadatas.forEach(entityMetadata => {
entityMetadata.relations.forEach(relation => {
relation.relatedEntityMetadata = entityMetadatas.find(m => m.target === relation.type);
});
});
// generate junction tables with its columns and foreign keys
const junctionEntityMetadatas: EntityMetadata[] = [];
entityMetadatas.forEach(metadata => {
@ -130,7 +137,7 @@ export class EntityMetadataBuilder {
const column1options: ColumnOptions = {
length: metadata.primaryColumn.length,
type: metadata.primaryColumn.type,
name: metadata.table.name + "_" + relation.name
name: metadata.table.name + "_" + metadata.primaryColumn.name
};
const column2options: ColumnOptions = {
length: inverseSideMetadata.primaryColumn.length,
@ -145,20 +152,14 @@ export class EntityMetadataBuilder {
new ForeignKeyMetadata(tableMetadata, [columns[0]], metadata.table, [metadata.primaryColumn]),
new ForeignKeyMetadata(tableMetadata, [columns[1]], inverseSideMetadata.table, [inverseSideMetadata.primaryColumn]),
];
junctionEntityMetadatas.push(new EntityMetadata(tableMetadata, columns, [], [], [], foreignKeys));
const junctionEntityMetadata = new EntityMetadata(tableMetadata, columns, [], [], [], foreignKeys);
junctionEntityMetadatas.push(junctionEntityMetadata);
relation.junctionEntityMetadata = junctionEntityMetadata;
relation.inverseRelation.junctionEntityMetadata = junctionEntityMetadata;
});
});
const allEntityMetadatas = entityMetadatas.concat(junctionEntityMetadatas);
// set inverse side (related) entity metadatas for all relation metadatas
allEntityMetadatas.forEach(entityMetadata => {
entityMetadata.relations.forEach(relation => {
relation.relatedEntityMetadata = allEntityMetadatas.find(m => m.target === relation.type);
})
});
return allEntityMetadatas;
return entityMetadatas.concat(junctionEntityMetadatas);
}
// -------------------------------------------------------------------------

View File

@ -108,6 +108,10 @@ export class EntityMetadata {
return this._columns.find(column => column.isUpdateDate);
}
get hasPrimaryKey(): boolean {
return !!this.primaryColumn;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------

View File

@ -86,6 +86,11 @@ export class RelationMetadata extends PropertyMetadata {
*/
private _relatedEntityMetadata: EntityMetadata;
/**
* Junction entity metadata.
*/
private _junctionEntityMetadata: EntityMetadata;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
@ -136,6 +141,14 @@ export class RelationMetadata extends PropertyMetadata {
this._relatedEntityMetadata = metadata;
}
get junctionEntityMetadata(): EntityMetadata {
return this._junctionEntityMetadata;
}
set junctionEntityMetadata(metadata: EntityMetadata) {
this._junctionEntityMetadata = metadata;
}
get relationType(): RelationTypes {
return this._relationType;
}
@ -148,6 +161,10 @@ export class RelationMetadata extends PropertyMetadata {
return this.computeInverseSide(this._inverseSideProperty);
}
get inverseRelation(): RelationMetadata {
return this._relatedEntityMetadata.findRelationByPropertyName(this.computeInverseSide(this._inverseSideProperty));
}
get isOwning(): boolean {
return this._isOwning;
}

View File

@ -49,7 +49,7 @@ export class ColumnTypes {
if (!type)
return false;
if (typeof type === "string" && !ColumnTypes.isTypeSupported(type))
if (typeof type === "string" && !ColumnTypes.isTypeSupported(String(type)))
return false;
return true;

View File

@ -1,73 +1,26 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {Alias} from "./alias/Alias";
import {AliasMap} from "./alias/AliasMap";
import {Connection} from "../connection/Connection";
import {RawSqlResultsToObjectTransformer} from "./transformer/RawSqlResultsToObjectTransformer";
export class Alias {
isMain: boolean;
entityMetadata: EntityMetadata;
name: string;
parentPropertyName: string;
parentAliasName: string;
constructor(name: string, entityMetadata: EntityMetadata, parentAliasName?: string, parentPropertyName?: string) {
this.name = name;
this.entityMetadata = entityMetadata;
this.parentAliasName = parentAliasName;
this.parentPropertyName = parentPropertyName;
}
export interface Join {
alias: Alias;
type: "left"|"inner";
conditionType: "on"|"with";
condition: string;
}
export class AliasMap {
constructor(public aliases: Alias[] = []) {
}
addMainAlias(alias: Alias) {
const mainAlias = this.getMainAlias();
if (mainAlias)
this.aliases.splice(this.aliases.indexOf(mainAlias), 1);
alias.isMain = true;
this.aliases.push(alias);
}
addAlias(alias: Alias) {
this.aliases.push(alias);
}
getMainAlias() {
return this.aliases.find(alias => alias.isMain);
}
findAliasByName(name: string) {
return this.aliases.find(alias => alias.name === name);
}
findAliasByParent(parentAliasName: string, parentPropertyName: string) {
return this.aliases.find(alias => {
return alias.parentAliasName === parentAliasName && alias.parentPropertyName === parentPropertyName;
});
}
}
/**
* @author Umed Khudoiberdiev <info@zar.tj>
*/
export class QueryBuilder {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(private entityMetadatas: EntityMetadata[]) {
}
export class QueryBuilder<Entity> {
// -------------------------------------------------------------------------
// Pirvate properties
// -------------------------------------------------------------------------
private aliasMap: AliasMap;
private type: "select"|"update"|"delete";
private selects: string[] = [];
private froms: { alias: Alias };
private leftJoins: { alias: Alias, conditionType: string, condition: string }[] = [];
private innerJoins: { alias: Alias, conditionType: string, condition: string }[] = [];
private joins: Join[] = [];
private groupBys: string[] = [];
private wheres: { type: "simple"|"and"|"or", condition: string }[] = [];
private havings: { type: "simple"|"and"|"or", condition: string }[] = [];
@ -76,41 +29,46 @@ export class QueryBuilder {
private limit: number;
private offset: number;
private aliasMap = new AliasMap();
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(private connection: Connection) {
this.aliasMap = new AliasMap(connection.metadatas);
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
//delete(selection: string): this;
//delete(selection: string[]): this;
//delete(selection: string|string[]): this {
delete(): this {
this.type = "delete";
// this.addSelection(selection);
return this;
}
// update(selection: string): this;
// update(selection: string[]): this;
// update(selection: string|string[]): this {
update(): this {
this.type = "update";
// this.addSelection(selection);
return this;
}
select(selection?: string): this;
select(selection?: string[]): this;
select(...selection: string[]): this;
select(selection?: string|string[]): this {
this.type = "select";
if (selection)
this.addSelection(selection);
if (selection) {
if (selection instanceof Array) {
this.selects = selection;
} else {
this.selects = [selection];
}
}
return this;
}
addSelect(selection: string): this;
addSelect(selection: string[]): this;
addSelect(...selection: string[]): this;
addSelect(selection: string|string[]): this {
if (selection instanceof Array)
this.selects = this.selects.concat(selection);
@ -123,49 +81,42 @@ export class QueryBuilder {
//from(tableName: string, alias: string): this;
from(entity: Function, alias?: string): this {
//from(entityOrTableName: Function|string, alias: string): this {
const aliasObj = new Alias(alias, this.findMetadata(entity));
const aliasObj = new Alias(alias);
aliasObj.target = entity;
this.aliasMap.addMainAlias(aliasObj);
this.froms = { alias: aliasObj };
return this;
}
innerJoin(property: string, alias: string, conditionType: string, condition: string): this;
innerJoin(entity: Function, alias: string, conditionType: string, condition: string): this;
innerJoin(entityOrProperty: Function|string, alias: string, conditionType: string, condition: string): this {
let parentPropertyName = "", parentAliasName = "";
let entityMetadata: EntityMetadata;
if (entityOrProperty instanceof Function) {
entityMetadata = this.findMetadata(entityOrProperty);
} else {
parentAliasName = (<string> entityOrProperty).split(".")[0];
parentPropertyName = (<string> entityOrProperty).split(".")[1];
const parentAliasMetadata = this.aliasMap.findAliasByName(parentAliasName).entityMetadata;
entityMetadata = parentAliasMetadata.findRelationWithDbName(parentPropertyName).relatedEntityMetadata;
}
const aliasObj = new Alias(alias, entityMetadata, parentAliasName, parentPropertyName);
this.aliasMap.addAlias(aliasObj);
this.innerJoins.push({ alias: aliasObj, conditionType: conditionType, condition: condition });
return this;
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);
}
leftJoin(property: string, alias: string, conditionType: string, condition: string): this;
leftJoin(entity: Function, alias: string, conditionType: string, condition: string): this;
leftJoin(entityOrProperty: Function|string, alias: string, conditionType: string, condition: string): this {
let parentPropertyName = "", parentAliasName = "";
let entityMetadata: EntityMetadata;
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 {
return this.join("left", entityOrProperty, alias, conditionType, condition);
}
join(joinType: "inner"|"left", property: string, alias: string, conditionType?: "on"|"with", condition?: string): this;
join(joinType: "inner"|"left", entity: Function, alias: string, conditionType?: "on"|"with", condition?: string): this;
join(joinType: "inner"|"left", entityOrProperty: Function|string, alias: string, conditionType: "on"|"with", condition: string): this;
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);
if (entityOrProperty instanceof Function) {
entityMetadata = this.findMetadata(entityOrProperty);
} else {
parentAliasName = (<string> entityOrProperty).split(".")[0];
parentPropertyName = (<string> entityOrProperty).split(".")[1];
const parentAliasMetadata = this.aliasMap.findAliasByName(parentAliasName).entityMetadata;
entityMetadata = parentAliasMetadata.findRelationByPropertyName(parentPropertyName).relatedEntityMetadata;
aliasObj.target = entityOrProperty;
} else if (typeof entityOrProperty === "string") {
aliasObj.parentAliasName = entityOrProperty.split(".")[0];
aliasObj.parentPropertyName = entityOrProperty.split(".")[1];
}
const aliasObj = new Alias(alias, entityMetadata, parentAliasName, parentPropertyName);
this.aliasMap.addAlias(aliasObj);
this.leftJoins.push({ alias: aliasObj, conditionType: conditionType, condition: condition });
const join: Join = { type: joinType, alias: aliasObj, conditionType: conditionType, condition: condition };
this.joins.push(join);
return this;
}
@ -240,9 +191,9 @@ export class QueryBuilder {
}
getSql(): string {
// joins are before because their many-to-many relations can add aliases
let sql = this.createSelectExpression();
sql += this.createLeftJoinExpression();
sql += this.createInnerJoinExpression();
sql += this.createJoinExpression();
sql += this.createWhereExpression();
sql += this.createGroupByExpression();
sql += this.createHavingExpression();
@ -253,69 +204,71 @@ export class QueryBuilder {
return sql;
}
generateAliasMap(): AliasMap {
return this.aliasMap;
/* const aliasesFromInnerJoins = this.innerJoins.map(join => join.alias);
const aliasesFromLeftJoins = this.leftJoins.map(join => join.alias);
return new AliasMap([this.froms.alias, ...aliasesFromLeftJoins, ...aliasesFromInnerJoins]);*/
execute(): Promise<any[]> {
return this.connection.driver.query<any[]>(this.getSql())
}
getScalarResults(): Promise<any[]> {
return this.execute().then(results => this.rawResultsToObjects(results));
}
getSingleScalarResult(): Promise<any> {
return this.getScalarResults().then(results => results[0]);
}
getResults(): Promise<Entity[]> {
return this.getScalarResults().then(objects => this.objectsToEntities(objects));
}
getSingleResult(): Promise<Entity> {
return this.getResults().then(entities => entities[0]);
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
protected addSelection(selection: string|string[]) {
if (selection instanceof Array)
this.selects = selection;
else
this.selects = [selection];
/*if (typeof selection === 'function') {
this.selects = this.selects.concat(selection(this.generatePropertyValuesEntity()));
} else if (typeof selection === 'string') {
this.selects.push(selection);
} else if (selection instanceof Array) {
this.selects = this.selects.concat(selection);
}*/
protected rawResultsToObjects(results: any[]) {
const transformer = new RawSqlResultsToObjectTransformer(this.aliasMap);
return transformer.transform(results);
}
protected findMetadata(target: Function) {
const metadata = this.entityMetadatas.find(metadata => metadata.target === target);
if (!metadata)
throw new Error("Metadata for " + (<any>target).name + " was not found.");
return metadata;
protected objectsToEntities(entities: any[]) {
return entities;
}
protected createSelectExpression() {
// todo throw exception if selects or from is missing
const metadata = this.froms.alias.entityMetadata;
const metadata = this.aliasMap.getEntityMetadataByAlias(this.froms.alias);
const tableName = metadata.table.name;
const alias = this.froms.alias.name;
const columns: string[] = [];
const allSelects: string[] = [];
// add select from the main table
if (this.selects.indexOf(alias) !== -1)
metadata.columns.forEach(column => {
columns.push(alias + "." + column.name + " AS " + alias + "_" + column.name);
allSelects.push(alias + "." + column.name + " AS " + alias + "_" + column.name);
});
// add selects from left and inner joins
this.leftJoins.concat(this.innerJoins)
// add selects from joins
this.joins
.filter(join => this.selects.indexOf(join.alias.name) !== -1)
.forEach(join => {
const joinMetadata = join.alias.entityMetadata;
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
joinMetadata.columns.forEach(column => {
columns.push(join.alias.name + "." + column.name + " AS " + join.alias.name + "_" + column.name);
allSelects.push(join.alias.name + "." + column.name + " AS " + join.alias.name + "_" + column.name);
});
});
// add all other selects
this.selects.filter(select => {
return select !== alias && !this.joins.find(join => join.alias.name === select);
}).forEach(select => allSelects.push(select));
switch (this.type) {
case "select":
return "SELECT " + columns.join(", ") + " FROM " + tableName + " " + alias;
return "SELECT " + allSelects.join(", ") + " FROM " + tableName + " " + alias;
case "update":
return "UPDATE " + tableName + " " + alias;
case "delete":
@ -339,23 +292,41 @@ export class QueryBuilder {
}).join(" ");
}
protected createInnerJoinExpression() {
if (!this.innerJoins || !this.innerJoins.length) return "";
protected createJoinExpression() {
return this.joins.map(join => {
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 parentTable = parentMetadata.table.name;
const parentTableColumn = parentMetadata.primaryColumn.name;
const relation = parentMetadata.findRelationByPropertyName(join.alias.parentPropertyName);
const junctionMetadata = relation.junctionEntityMetadata;
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
const joinTable = joinMetadata.table.name;
const joinTableColumn = joinMetadata.primaryColumn.name;
if (relation.isManyToMany) {
const junctionTable = junctionMetadata.table.name;
const junctionAlias = join.alias.parentAliasName + "_" + join.alias.name;
const joinAlias = join.alias.name;
const condition1 = junctionAlias + "." + parentTable + "_" + parentTableColumn + "=" + parentAlias + "." + joinTableColumn; // todo: use column names from junction table somehow
const condition2 = joinAlias + "." + joinTableColumn + "=" + junctionAlias + "." + joinTable + "_" + joinTableColumn;
return " " + joinType + " JOIN " + junctionTable + " " + junctionAlias + " " + join.conditionType + " " + condition1 +
" " + joinType + " JOIN " + joinTable + " " + joinAlias + " " + join.conditionType + " " + condition2 + appendedCondition;
} else if (relation.isOneToOne || relation.isManyToOne) {
const condition = join.alias.name + "." + joinTableColumn + "=" + parentAlias + "." + join.alias.parentPropertyName;
return " " + joinType + " JOIN " + joinTable + " " + join.alias.name + " " + join.conditionType + " " + condition + appendedCondition;
return this.innerJoins.map(join => {
const joinMetadata = join.alias.entityMetadata; // todo: throw exception if not found
const relationTable = joinMetadata.table.name;
return " INNER JOIN " + relationTable + " " + join.alias.name + " " + join.conditionType + " " + join.condition;
}).join(" ");
}
protected createLeftJoinExpression() {
if (!this.leftJoins || !this.leftJoins.length) return "";
return this.leftJoins.map(join => {
const joinMetadata = join.alias.entityMetadata;
const relationTable = joinMetadata.table.name;
return " LEFT JOIN " + relationTable + " " + join.alias.name + " " + join.conditionType + " " + join.condition;
} else if (relation.isOneToMany) {
const condition = join.alias.name + "." + relation.inverseSideProperty + "=" + parentAlias + "." + joinTableColumn;
return " " + joinType + " JOIN " + joinTable + " " + join.alias.name + " " + join.conditionType + " " + condition + appendedCondition;
} else {
return " " + joinType + " JOIN " + joinTable + " " + join.alias.name + " " + join.conditionType + " " + join.condition;
}
}).join(" ");
}

View File

@ -0,0 +1,22 @@
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
export class Alias {
isMain: boolean;
name: string;
target: Function;
parentPropertyName: string;
parentAliasName: string;
constructor(name: string) {
this.name = name;
}
getPrimaryKeyValue(result: any, primaryColumn: ColumnMetadata): any {
return result[this.name + "_" + primaryColumn.name];
}
getColumnValue(result: any, column: ColumnMetadata) {
return result[this.name + "_" + column.name];
}
}

View File

@ -0,0 +1,76 @@
import {EntityMetadata} from "../../metadata-builder/metadata/EntityMetadata";
import {Alias} from "./Alias";
export class AliasMap {
// -------------------------------------------------------------------------
// Properties
// -------------------------------------------------------------------------
aliases: Alias[] = [];
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(private entityMetadatas: EntityMetadata[]) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
addMainAlias(alias: Alias) {
const mainAlias = this.getMainAlias();
if (mainAlias)
this.aliases.splice(this.aliases.indexOf(mainAlias), 1);
alias.isMain = true;
this.aliases.push(alias);
}
addAlias(alias: Alias) {
this.aliases.push(alias);
}
getMainAlias() {
return this.aliases.find(alias => alias.isMain);
}
findAliasByName(name: string) {
return this.aliases.find(alias => alias.name === name);
}
findAliasByParent(parentAliasName: string, parentPropertyName: string) {
return this.aliases.find(alias => {
return alias.parentAliasName === parentAliasName && alias.parentPropertyName === parentPropertyName;
});
}
getEntityMetadataByAlias(alias: Alias): EntityMetadata {
if (alias.target) {
return this.findMetadata(alias.target);
} else if (alias.parentAliasName && alias.parentPropertyName) {
const parentAlias = this.findAliasByName(alias.parentAliasName); // todo: throw exceptions everywhere
const parentEntityMetadata = this.getEntityMetadataByAlias(parentAlias);
const relation = parentEntityMetadata.findRelationByPropertyName(alias.parentPropertyName);
return relation.relatedEntityMetadata;
}
throw new Error("Cannot get entity metadata for the given alias " + alias.name);
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private findMetadata(target: Function) {
const metadata = this.entityMetadatas.find(metadata => metadata.target === target);
if (!metadata)
throw new Error("Metadata for " + (<any>target).name + " was not found.");
return metadata;
}
}

View File

@ -0,0 +1,77 @@
import {AliasMap} from "../alias/AliasMap";
import {Alias} from "../alias/Alias";
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.
*/
export class RawSqlResultsToObjectTransformer {
// todo: add check for property relation with id as a column
// todo: create metadata or do it later?
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(private aliasMap: AliasMap) {
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
transform(rawSqlResults: any[]): any[] {
return this.groupAndTransform(rawSqlResults, this.aliasMap.getMainAlias());
}
// -------------------------------------------------------------------------
// 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(rawSqlResults: any[], alias: Alias) {
const metadata = this.aliasMap.getEntityMetadataByAlias(alias);
if (!metadata.hasPrimaryKey)
throw new Error("Metadata does not have primary key. You must have it to make convertation to object possible.");
const groupedRawResults = _.groupBy(rawSqlResults, result => alias.getPrimaryKeyValue(result, metadata.primaryColumn));
return Object.keys(groupedRawResults)
.map(key => this.transformIntoSingleResult(groupedRawResults[key], alias, metadata))
.filter(res => !!res);
}
/**
* Transforms set of data results of the single value.
*/
private transformIntoSingleResult(rawSqlResults: any[], alias: Alias, metadata: EntityMetadata) {
const jsonObject: any = {};
// 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 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);
if (relationAlias) {
const relatedObjects = this.groupAndTransform(rawSqlResults, relationAlias);
const result = (relation.isManyToOne || relation.isOneToOne) ? relatedObjects[0] : relatedObjects;
if (result)
jsonObject[relation.propertyName] = result;
}
});
return Object.keys(jsonObject).length > 0 ? jsonObject : null;
}
}

View File

@ -1,9 +1,10 @@
import {Connection} from "../connection/Connection";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {OrmBroadcaster} from "../subscriber/OrmBroadcaster";
import {QueryBuilder, AliasMap} from "../driver/query-builder/QueryBuilder";
import {DynamicCascadeOptions} from "./cascade/CascadeOption";
import {EntityCreator} from "./creator/EntityCreator";
import {QueryBuilder} from "../query-builder/QueryBuilder";
// todo: think how we can implement queryCount, queryManyAndCount
// todo: extract non safe methods from repository (removeById, removeByConditions)
/**
* Repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
@ -67,8 +68,9 @@ export class Repository<Entity> {
createFromJson(json: any, fetchProperty?: boolean): Promise<Entity>;
createFromJson(json: any, fetchConditions?: Object): Promise<Entity>;
createFromJson(json: any, fetchOption?: boolean|Object): Promise<Entity> {
const creator = new EntityCreator(this.connection);
return creator.createFromJson<Entity>(json, this.metadata, fetchOption);
return Promise.resolve<Entity>(null); // todo
/* const creator = new EntityCreator(this.connection);
return creator.createFromJson<Entity>(json, this.metadata, fetchOption);*/
}
/**
@ -78,74 +80,38 @@ export class Repository<Entity> {
createManyFromJson(objects: any[], fetchProperties?: boolean[]): Promise<Entity[]>;
createManyFromJson(objects: any[], fetchConditions?: Object[]): Promise<Entity[]>;
createManyFromJson(objects: any[], fetchOption?: boolean[]|Object[]): Promise<Entity[]> {
return Promise.all(objects.map((object, key) => {
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.
*/
createQueryBuilder(alias: string): QueryBuilder {
createQueryBuilder(alias: string): QueryBuilder<Entity> {
return this.connection.driver
.createQueryBuilder(this.connection.metadatas)
.createQueryBuilder<Entity>(this.connection)
.select(alias)
.from(this.metadata.target, alias);
}
/**
* Executes query. Expects query will return object in Entity format and creates Entity object from that result.
*/
queryOne(query: string, aliasMap: AliasMap): Promise<Entity> {
return this.connection.driver
.query<any[]>(query)
.then(results => this.objectToEntity(results, aliasMap))
.then(entities => {
this.broadcaster.broadcastAfterLoaded(entities[0]);
return entities[0];
});
}
/**
* Executes query. Expects query will return objects in Entity format and creates Entity objects from that result.
*/
queryMany(query: string, aliasMap: AliasMap): Promise<Entity[]> {
return this.connection.driver
.query<any[]>(query)
.then(results => this.objectToEntity(results, aliasMap))
.then(entities => {
this.broadcaster.broadcastAfterLoadedAll(entities);
return entities;
});
}
/**
* Executes query and returns raw result.
* Executes query and returns raw database results.
*/
query(query: string): Promise<any> {
return this.connection.driver.query(query);
}
/**
* Gives number of rows found by a given query.
*/
queryCount(query: any): Promise<number> {
return this.connection.driver
.query(query)
.then(result => parseInt(result));
}
/**
* Finds entities that match given conditions.
*/
find(conditions?: Object): Promise<Entity[]> {
const alias = this.metadata.table.name;
const builder = this.createQueryBuilder(alias);
Object.keys(conditions).forEach(key => {
builder.where(alias + "." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
});
return this.queryMany(builder.getSql(), builder.generateAliasMap());
Object.keys(conditions).forEach(key => builder.where(alias + "." + key + "=:" + key));
return builder.setParameters(conditions).getResults();
}
/**
@ -154,10 +120,8 @@ export class Repository<Entity> {
findOne(conditions: Object): Promise<Entity> {
const alias = this.metadata.table.name;
const builder = this.createQueryBuilder(alias);
Object.keys(conditions).forEach(key => {
builder.where(alias + "." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
});
return this.queryOne(builder.getSql(), builder.generateAliasMap());
Object.keys(conditions).forEach(key => builder.where(alias + "." + key + "=:" + key));
return builder.setParameters(conditions).getSingleResult();
}
/**
@ -165,12 +129,10 @@ export class Repository<Entity> {
*/
findById(id: any): Promise<Entity> {
const alias = this.metadata.table.name;
const builder = this.createQueryBuilder(alias)
return this.createQueryBuilder(alias)
.where(alias + "." + this.metadata.primaryColumn.name + "=:id")
.setParameter("id", id);
return this.queryOne(builder.getSql(), builder.generateAliasMap());
.setParameter("id", id)
.getSingleResult();
}
// -------------------------------------------------------------------------
@ -181,9 +143,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);
// if (!this.schema.isEntityTypeCorrect(entity))
// throw new BadEntityInstanceException(entity, this.schema.entityClass);
@ -191,14 +153,14 @@ export class Repository<Entity> {
// const remover = new EntityRemover<Entity>(this.connection);
// const persister = new EntityPersister<Entity>(this.connection);
return remover.computeRemovedRelations(this.metadata, entity, dynamicCascadeOptions)
/* return remover.computeRemovedRelations(this.metadata, entity, dynamicCascadeOptions)
.then(result => persister.persist(this.metadata, entity, dynamicCascadeOptions))
.then(result => remover.executeRemoveOperations())
.then(result => remover.executeUpdateInverseSideRelationRemoveIds())
.then(result => entity);
.then(result => entity);*/
}
computeChangeSet(entity: Entity) {
/*computeChangeSet(entity: Entity) {
// if there is no primary key - there is no way to determine if object needs to be updated or insert
// since we cannot get the target object without identifier, that's why we always do insert for such objects
if (!this.metadata.primaryColumn)
@ -208,11 +170,7 @@ export class Repository<Entity> {
this.findById(this.metadata.getEntityId(entity)).then(dbEntity => {
});
}
insert(entity: Entity) {
}
}*/
// -------------------------------------------------------------------------
// Persist ends
@ -221,68 +179,45 @@ export class Repository<Entity> {
/**
* Removes a given entity.
*/
remove(entity: Entity, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>): Promise<void> {
remove(entity: Entity/*, dynamicCascadeOptions?: DynamicCascadeOptions<Entity>*/): Promise<void> {
// todo
return Promise.resolve();
// if (!this.schema.isEntityTypeCorrect(entity))
// throw new BadEntityInstanceException(entity, this.schema.entityClass);
const remover = new EntityRemover<Entity>(this.connection);
/*const remover = new EntityRemover<Entity>(this.connection);
return remover.registerEntityRemoveOperation(this.metadata, this.metadata.getEntityId(entity), dynamicCascadeOptions)
.then(results => remover.executeRemoveOperations())
.then(results => remover.executeUpdateInverseSideRelationRemoveIds());
.then(results => remover.executeUpdateInverseSideRelationRemoveIds());*/
}
/**
* Removes entity by a given id.
* Removes entity by a given id. Does not take care about cascade remove operations.
*/
removeById(id: string): Promise<void> {
const builder = this.createQueryBuilder("entity")
const alias = this.metadata.table.name;
return this.createQueryBuilder(alias)
.delete()
.where("entity." + this.metadata.primaryColumn.name + "=:id")
.setParameter("id", id);
return this.query(builder.getSql());
.where(alias + "." + this.metadata.primaryColumn.name + "=:id")
.setParameter("id", id)
.execute()
.then(() => {});
}
/**
* Removes entities by a given simple conditions.
* Removes entities by a given simple conditions. Does not take care about cascade remove operations.
*/
removeByConditions(conditions: Object): Promise<any> {
const builder = this.createQueryBuilder("entity").delete();
Object.keys(conditions).forEach(key => {
builder.where("entity." + key + "=:" + key).setParameter(key, (<any> conditions)[key]);
});
return this.query(builder.getSql());
removeByConditions(conditions: Object): Promise<void> {
const alias = this.metadata.table.name;
const builder = this.createQueryBuilder(alias).delete();
Object.keys(conditions).forEach(key => builder.where(alias + "." + key + "=:" + key));
return builder
.setParameters(conditions)
.execute()
.then(() => {});
}
/**
* Finds entities by given criteria and returns them with the total number of
*/
queryManyAndCount(query: string, countQuery: string): Promise<{ entities: Entity[] }> {
return Promise.all<any>([
this.queryMany(query),
this.queryCount(countQuery)
]).then(([entities, count]) => {
return { entities: <Entity[]> entities, count: <number> count };
});
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
/**
* 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.
*/
private objectToEntity(objects: any, aliasMap: AliasMap) {
const creator = new EntityCreator(this.connection);
return creator.objectToEntity<Entity>(objects, this.metadata, aliasMap);
}
/*private dbObjectToEntity(dbObject: any): Promise<Entity> {
const hydrator = new EntityHydrator<Entity>(this.connection);
return hydrator.hydrate(this.metadata, dbObject, joinFields);
}*/
}

View File

@ -1,8 +1,8 @@
import {SchemaBuilder} from "./SchemaBuilder";
import {MysqlDriver} from "../MysqlDriver";
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../../metadata-builder/metadata/ForeignKeyMetadata";
import {TableMetadata} from "../../metadata-builder/metadata/TableMetadata";
import {MysqlDriver} from "../driver/MysqlDriver";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata-builder/metadata/ForeignKeyMetadata";
import {TableMetadata} from "../metadata-builder/metadata/TableMetadata";
export class MysqlSchemaBuilder extends SchemaBuilder {

View File

@ -1,6 +1,6 @@
import {ColumnMetadata} from "../../metadata-builder/metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../../metadata-builder/metadata/ForeignKeyMetadata";
import {TableMetadata} from "../../metadata-builder/metadata/TableMetadata";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata-builder/metadata/ForeignKeyMetadata";
import {TableMetadata} from "../metadata-builder/metadata/TableMetadata";
export abstract class SchemaBuilder {

View File

@ -3,7 +3,7 @@ import {TableMetadata} from "../metadata-builder/metadata/TableMetadata";
import {ColumnMetadata} from "../metadata-builder/metadata/ColumnMetadata";
import {ForeignKeyMetadata} from "../metadata-builder/metadata/ForeignKeyMetadata";
import {EntityMetadata} from "../metadata-builder/metadata/EntityMetadata";
import {SchemaBuilder} from "../driver/schema-builder/SchemaBuilder";
import {SchemaBuilder} from "../schema-builder/SchemaBuilder";
/**
* Creates indexes based on the given metadata

View File

@ -14,6 +14,7 @@
"exclude": [
"build",
"node_modules",
"odmhelpers",
"typings/browser.d.ts",
"typings/browser"
]