added closure tables basic support

This commit is contained in:
Umed Khudoiberdiev 2016-05-10 21:45:34 +05:00
parent 887cedaeb3
commit 76341fd9ec
43 changed files with 1151 additions and 167 deletions

View File

@ -1,5 +1,5 @@
import * as _ from "lodash";
import {NamingStrategyInterface} from "../../../src/naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../../../src/naming-strategy/NamingStrategyInterface";
import {NamingStrategy} from "../../../src/decorator/NamingStrategy";
@NamingStrategy("custom_strategy")
@ -52,4 +52,8 @@ export class CustomNamingStrategy implements NamingStrategyInterface {
return column1 === column2 ? column1 + "_2" : column1;
}
closureJunctionTableName(tableName: string): string {
return tableName + "_closure";
}
}

View File

@ -0,0 +1,119 @@
import {createConnection, CreateConnectionOptions} from "../../src/typeorm";
import {Category} from "./entity/Category";
const options: CreateConnectionOptions = {
driver: "mysql",
connection: {
host: "192.168.99.100",
port: 3306,
username: "root",
password: "admin",
database: "test",
autoSchemaCreate: true,
logging: {
logOnlyFailedQueries: true,
logFailedQueryError: true
}
},
entities: [Category]
};
createConnection(options).then(connection => {
let categoryRepository = connection.getTreeRepository(Category);
let childChildCategory1 = new Category();
childChildCategory1.name = "Child #1 of Child #1 of Category #1";
let childChildCategory2 = new Category();
childChildCategory2.name = "Child #1 of Child #2 of Category #1";
let childCategory1 = new Category();
childCategory1.name = "Child #1 of Category #1";
childCategory1.childCategories = [childChildCategory1];
let childCategory2 = new Category();
childCategory2.name = "Child #2 of Category #1";
childCategory2.childCategories = [childChildCategory2];
let category1 = new Category();
category1.name = "Category #1";
category1.childCategories = [childCategory1, childCategory2];
return categoryRepository
.persist(category1)
.then(category => {
console.log("Categories has been saved. Lets now load it and all its descendants:");
return categoryRepository.findDescendants(category1);
})
.then(categories => {
console.log(categories);
console.log("Descendants has been loaded. Now lets get them in a tree:");
return categoryRepository.findDescendantsTree(category1);
})
.then(categories => {
console.log(categories);
console.log("Descendants in a tree has been loaded. Now lets get a count of the descendants:");
return categoryRepository.countDescendants(category1);
})
.then(count => {
console.log(count);
console.log("Descendants count has been loaded. Lets now load all ancestors of the childChildCategory1:");
return categoryRepository.findAncestors(childChildCategory1);
})
.then(categories => {
console.log(categories);
console.log("Ancestors has been loaded. Now lets get them in a tree:");
return categoryRepository.findAncestorsTree(childChildCategory1);
})
.then(categories => {
console.log(categories);
console.log("Ancestors in a tree has been loaded. Now lets get a count of the ancestors:");
return categoryRepository.countAncestors(childChildCategory1);
})
.then(count => {
console.log(count);
console.log("Ancestors count has been loaded. Now lets get a all roots (categories without parents):");
return categoryRepository.findRoots();
})
.then(categories => {
console.log(categories);
})
.catch(error => console.log(error.stack));
/*
this way it does not work:
let category1 = new Category();
category1.name = "Category #1";
// category1.childCategories = [];
let childCategory1 = new Category();
childCategory1.name = "Child #1 of Category #1";
childCategory1.parentCategory = category1;
let childCategory2 = new Category();
childCategory2.name = "Child #2 of Category #1";
childCategory2.parentCategory = category1;
let childChildCategory1 = new Category();
childChildCategory1.name = "Child #1 of Child #1 of Category #1";
childChildCategory1.parentCategory = childCategory1;
let childChildCategory2 = new Category();
childChildCategory2.name = "Child #1 of Child #2 of Category #1";
childChildCategory2.parentCategory = childCategory2;
return categoryRepository
.persist(childChildCategory1)
.then(category => {
return categoryRepository.persist(childChildCategory2);
})
.then(category => {
console.log("Categories has been saved. Lets load them now.");
})
.catch(error => console.log(error.stack));
*/
}, error => console.log("Cannot connect: ", error));

View File

@ -0,0 +1,29 @@
import {PrimaryColumn, Column} from "../../../src/columns";
import {TreeLevelColumn} from "../../../src/decorator/tree/TreeLevelColumn";
import {ClosureTable} from "../../../src/decorator/tables/ClosureTable";
import {TreeParent} from "../../../src/decorator/tree/TreeParent";
import {TreeChildren} from "../../../src/decorator/tree/TreeChildren";
@ClosureTable("sample22_category")
export class Category {
@PrimaryColumn("int", { generated: true })
id: number;
@Column()
name: string;
@TreeParent()
parentCategory: Category;
@TreeChildren({ cascadeAll: true })
childCategories: Category[];
@TreeLevelColumn()
level: number;
// todo:
// @RelationsCountColumn()
// categoriesCount: number;
}

View File

@ -2,3 +2,5 @@ export * from "./decorator/columns/Column";
export * from "./decorator/columns/PrimaryColumn";
export * from "./decorator/columns/CreateDateColumn";
export * from "./decorator/columns/UpdateDateColumn";
export * from "./decorator/columns/RelationsCountColumn";

View File

@ -7,7 +7,7 @@ import {EntityMetadata} from "../metadata/EntityMetadata";
import {SchemaCreator} from "../schema-creator/SchemaCreator";
import {ConstructorFunction} from "../common/ConstructorFunction";
import {EntityListenerMetadata} from "../metadata/EntityListenerMetadata";
import {EntityManager} from "../repository/EntityManager";
import {EntityManager} from "../entity-manager/EntityManager";
import {importClassesFromDirectories} from "../util/DirectoryExportedClassesLoader";
import {defaultMetadataStorage, getContainer} from "../typeorm";
import {EntityMetadataBuilder} from "../metadata-storage/EntityMetadataBuilder";
@ -19,7 +19,9 @@ import {CannotImportAlreadyConnectedError} from "./error/CannotImportAlreadyConn
import {CannotCloseNotConnectedError} from "./error/CannotCloseNotConnectedError";
import {CannotConnectAlreadyConnectedError} from "./error/CannotConnectAlreadyConnectedError";
import {ReactiveRepository} from "../repository/ReactiveRepository";
import {ReactiveEntityManager} from "../repository/ReactiveEntityManager";
import {ReactiveEntityManager} from "../entity-manager/ReactiveEntityManager";
import {TreeRepository} from "../repository/TreeRepository";
import {ReactiveTreeRepository} from "../repository/ReactiveTreeRepository";
/**
* Temporary type to store and link both repository and its metadata.
@ -254,7 +256,25 @@ export class Connection {
}
/**
* Gets repository for the given entity class.
* Gets tree repository for the given entity class.
*/
getTreeRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): TreeRepository<Entity> {
const repository = this.getRepository(entityClass);
if (!this.isConnected)
throw new NoConnectionForRepositoryError(this.name);
const metadata = this.entityMetadatas.findByTarget(entityClass);
const repoMeta = this.repositoryAndMetadatas.find(repoMeta => repoMeta.metadata === metadata);
if (!repoMeta)
throw new RepositoryNotFoundError(this.name, entityClass);
if (!repoMeta.metadata.table.isClosure)
throw new Error(`Cannot get a tree repository. ${repoMeta.metadata.name} is not a tree table. Try to use @ClosureTable decorator instead of @Table.`);
return <TreeRepository<Entity>> repoMeta.repository;
}
/**
* Gets reactive repository for the given entity class.
*/
getReactiveRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): ReactiveRepository<Entity> {
if (!this.isConnected)
@ -268,6 +288,23 @@ export class Connection {
return repoMeta.reactiveRepository;
}
/**
* Gets reactive tree repository for the given entity class.
*/
getReactiveTreeRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): ReactiveTreeRepository<Entity> {
if (!this.isConnected)
throw new NoConnectionForRepositoryError(this.name);
const metadata = this.entityMetadatas.findByTarget(entityClass);
const repoMeta = this.repositoryAndMetadatas.find(repoMeta => repoMeta.metadata === metadata);
if (!repoMeta)
throw new RepositoryNotFoundError(this.name, entityClass);
if (!repoMeta.metadata.table.isClosure)
throw new Error(`Cannot get a tree repository. ${repoMeta.metadata.name} is not a tree table. Try to use @ClosureTable decorator instead of @Table.`);
return <ReactiveTreeRepository<Entity>> repoMeta.reactiveRepository;
}
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
@ -321,12 +358,21 @@ export class Connection {
* Creates a temporary object RepositoryAndMetadata to store relation between repository and metadata.
*/
private createRepoMeta(metadata: EntityMetadata): RepositoryAndMetadata {
const repository = new Repository<any>(this, this.entityMetadatas, metadata);
return {
metadata: metadata,
repository: repository,
reactiveRepository: new ReactiveRepository(repository)
};
if (metadata.table.isClosure) {
const repository = new TreeRepository<any>(this, this.entityMetadatas, metadata);
return {
metadata: metadata,
repository: repository,
reactiveRepository: new ReactiveTreeRepository(repository)
};
} else {
const repository = new Repository<any>(this, this.entityMetadatas, metadata);
return {
metadata: metadata,
repository: repository,
reactiveRepository: new ReactiveRepository(repository)
};
}
}
}

View File

@ -24,7 +24,7 @@ export function CreateDateColumn(options?: ColumnOptions): Function {
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isCreateDate: true,
mode: "createDate",
options: options
}));
};

View File

@ -0,0 +1,17 @@
import {defaultMetadataStorage} from "../../typeorm";
import {RelationsCountMetadata} from "../../metadata/RelationsCountMetadata";
/**
* Holds a number of children in the closure table of the column.
*/
export function RelationsCountColumn<T>(relation: string|((object: T) => any)): Function {
return function (object: Object, propertyName: string) {
// todo: need to check if property type is number?
// const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// create and register a new column metadata
defaultMetadataStorage().relationCountMetadatas.add(new RelationsCountMetadata(object.constructor, propertyName, relation));
};
}

View File

@ -24,7 +24,7 @@ export function UpdateDateColumn(options?: ColumnOptions): Function {
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isUpdateDate: true,
mode: "updateDate",
options: options
}));
};

View File

@ -26,7 +26,7 @@ export function VersionColumn(options?: ColumnOptions): Function {
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
isVersion: true,
mode: "version",
options: options
}));
};

View File

@ -6,6 +6,6 @@ import {defaultMetadataStorage} from "../../typeorm";
*/
export function AbstractTable() {
return function (cls: Function) {
defaultMetadataStorage().tableMetadatas.add(new TableMetadata(cls, true));
defaultMetadataStorage().tableMetadatas.add(new TableMetadata(cls, undefined, "abstract"));
};
}

View File

@ -0,0 +1,12 @@
import {defaultMetadataStorage} from "../../typeorm";
import {TableMetadata} from "../../metadata/TableMetadata";
/**
* This decorator is used to mark classes that will be a tables. Database schema will be created for all classes
* decorated with it, and Repository can be retrieved and used for it.
*/
export function ClosureTable(name?: string) {
return function (cls: Function) {
defaultMetadataStorage().tableMetadatas.add(new TableMetadata(cls, name, "closure"));
};
}

View File

@ -0,0 +1,27 @@
import {defaultMetadataStorage} from "../../typeorm";
import {RelationOptions} from "../../metadata/options/RelationOptions";
import {RelationMetadata} from "../../metadata/RelationMetadata";
import {RelationTypes} from "../../metadata/types/RelationTypes";
/**
* Marks a specific property of the class as a children of the tree.
*/
export function TreeChildren(options?: RelationOptions): Function {
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
// add one-to-many relation for this
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
isTreeChildren: true,
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.ONE_TO_MANY,
type: () => object.constructor,
options: options
}));
};
}

View File

@ -0,0 +1,30 @@
import {defaultMetadataStorage} from "../../typeorm";
import {ColumnTypes} from "../../metadata/types/ColumnTypes";
import {ColumnOptions} from "../../metadata/options/ColumnOptions";
import {ColumnMetadata} from "../../metadata/ColumnMetadata";
/**
* Creates a "level"/"length" column to the table that holds a closure table.
*/
export function TreeLevelColumn(): Function {
return function (object: Object, propertyName: string) {
const reflectedType = ColumnTypes.typeToString(Reflect.getMetadata("design:type", object, propertyName));
// if column options are not given then create a new empty options
const options: ColumnOptions = {};
// implicitly set a type, because this column's type cannot be anything else except number
options.type = ColumnTypes.INTEGER;
// create and register a new column metadata
defaultMetadataStorage().columnMetadatas.add(new ColumnMetadata({
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
mode: "treeLevel",
options: options
}));
};
}

View File

@ -0,0 +1,25 @@
import {defaultMetadataStorage} from "../../typeorm";
import {RelationOptions} from "../../metadata/options/RelationOptions";
import {RelationMetadata} from "../../metadata/RelationMetadata";
import {RelationTypes} from "../../metadata/types/RelationTypes";
/**
* Marks a specific property of the class as a parent of the tree.
*/
export function TreeParent(options?: RelationOptions): Function {
return function (object: Object, propertyName: string) {
if (!options) options = {} as RelationOptions;
const reflectedType = Reflect.getMetadata("design:type", object, propertyName);
defaultMetadataStorage().relationMetadatas.add(new RelationMetadata({
isTreeParent: true,
target: object.constructor,
propertyName: propertyName,
propertyType: reflectedType,
relationType: RelationTypes.MANY_TO_ONE,
type: () => object.constructor,
options: options
}));
};
}

View File

@ -98,5 +98,10 @@ export interface Driver {
* Escapes given value.
*/
escape(value: any): any;
/**
* Inserts new values into closure table.
*/
insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise<number>;
}

View File

@ -304,4 +304,26 @@ export class MysqlDriver extends BaseDriver implements Driver {
return this.mysqlConnection.escape(value);
}
/**
* Inserts rows into closure table.
*/
insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise<number> {
let sql = "";
if (hasLevel) {
sql = `INSERT INTO ${tableName}(ancestor, descendant, level) ` +
`SELECT ancestor, ${newEntityId}, level + 1 FROM ${tableName} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}, 1`;
} else {
sql = `INSERT INTO ${tableName}(ancestor, descendant) ` +
`SELECT ancestor, ${newEntityId} FROM ${tableName} WHERE descendant = ${parentId} ` +
`UNION ALL SELECT ${newEntityId}, ${newEntityId}`;
}
return this.query(sql).then(() => {
return this.query(`SELECT MAX(level) as level FROM ${tableName} WHERE descendant = ${parentId}`);
}).then((results: any) => {
return results && results[0] && results[0]["level"] ? parseInt(results[0]["level"]) + 1 : 1;
});
}
}

View File

@ -1,9 +1,10 @@
import {Connection} from "../connection/Connection";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {FindOptions} from "./FindOptions";
import {Repository} from "./Repository";
import {FindOptions} from "../repository/FindOptions";
import {Repository} from "../repository/Repository";
import {ConstructorFunction} from "../common/ConstructorFunction";
import {ReactiveRepository} from "./ReactiveRepository";
import {ReactiveRepository} from "../repository/ReactiveRepository";
import {TreeRepository} from "../repository/TreeRepository";
/**
* Entity manager supposed to work with any entity, automatically find its repository and call its method, whatever
@ -29,6 +30,13 @@ export class EntityManager {
return this.connection.getRepository(entityClass);
}
/**
* Gets a tree repository of the given entity.
*/
getTreeRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): TreeRepository<Entity> {
return this.connection.getTreeRepository(entityClass);
}
/**
* Gets reactive repository of the given entity.
*/
@ -230,4 +238,68 @@ export class EntityManager {
.then(() => runInTransactionResult);
}
/**
* Roots are entities that have no ancestors. Finds them all.
*/
findRoots<Entity>(entityClass: ConstructorFunction<Entity>|Function): Promise<Entity[]> {
return this.getTreeRepository(entityClass).findRoots();
}
/**
* Creates a query builder used to get descendants of the entities in a tree.
*/
createDescendantsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getTreeRepository(entityClass).createDescendantsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
*/
findDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity[]> {
return this.getTreeRepository(entityClass).findDescendants(entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
findDescendantsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity> {
return this.getTreeRepository(entityClass).findDescendantsTree(entity);
}
/**
* Gets number of descendants of the entity.
*/
countDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<number> {
return this.getTreeRepository(entityClass).countDescendants(entity);
}
/**
* Creates a query builder used to get ancestors of the entities in the tree.
*/
createAncestorsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getTreeRepository(entityClass).createAncestorsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
*/
findAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity[]> {
return this.getTreeRepository(entityClass).findAncestors(entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity> {
return this.getTreeRepository(entityClass).findAncestorsTree(entity);
}
/**
* Gets number of ancestors of the entity.
*/
countAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<number> {
return this.getTreeRepository(entityClass).countAncestors(entity);
}
}

View File

@ -1,10 +1,11 @@
import {Connection} from "../connection/Connection";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {FindOptions} from "./FindOptions";
import {Repository} from "./Repository";
import {FindOptions} from "../repository/FindOptions";
import {Repository} from "../repository/Repository";
import {ConstructorFunction} from "../common/ConstructorFunction";
import {ReactiveRepository} from "./ReactiveRepository";
import {ReactiveRepository} from "../repository/ReactiveRepository";
import * as Rx from "rxjs/Rx";
import {ReactiveTreeRepository} from "../repository/ReactiveTreeRepository";
/**
* Entity manager supposed to work with any entity, automatically find its repository and call its method, whatever
@ -36,6 +37,13 @@ export class ReactiveEntityManager {
getReactiveRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): ReactiveRepository<Entity> {
return this.connection.getReactiveRepository(entityClass);
}
/**
* Gets reactive tree repository of the given entity.
*/
getReactiveTreeRepository<Entity>(entityClass: ConstructorFunction<Entity>|Function): ReactiveTreeRepository<Entity> {
return this.connection.getReactiveTreeRepository(entityClass);
}
/**
* Checks if entity has an id.
@ -233,4 +241,67 @@ export class ReactiveEntityManager {
.then(() => runInTransactionResult));
}
/**
* Roots are entities that have no ancestors. Finds them all.
*/
findRoots<Entity>(entityClass: ConstructorFunction<Entity>|Function): Promise<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findRoots();
}
/**
* Creates a query builder used to get descendants of the entities in a tree.
*/
createDescendantsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getReactiveTreeRepository(entityClass).createDescendantsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
*/
findDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findDescendants(entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
findDescendantsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity> {
return this.getReactiveTreeRepository(entityClass).findDescendantsTree(entity);
}
/**
* Gets number of descendants of the entity.
*/
countDescendants<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<number> {
return this.getReactiveTreeRepository(entityClass).countDescendants(entity);
}
/**
* Creates a query builder used to get ancestors of the entities in the tree.
*/
createAncestorsQueryBuilder<Entity>(entityClass: ConstructorFunction<Entity>|Function, alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.getReactiveTreeRepository(entityClass).createAncestorsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
*/
findAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity[]> {
return this.getReactiveTreeRepository(entityClass).findAncestors(entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<Entity> {
return this.getReactiveTreeRepository(entityClass).findAncestorsTree(entity);
}
/**
* Gets number of ancestors of the entity.
*/
countAncestors<Entity>(entityClass: ConstructorFunction<Entity>|Function, entity: Entity): Promise<number> {
return this.getReactiveTreeRepository(entityClass).countAncestors(entity);
}
}

View File

@ -1,26 +1,18 @@
import {MetadataStorage} from "./MetadataStorage";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {ColumnOptions} from "../metadata/options/ColumnOptions";
import {ForeignKeyMetadata} from "../metadata/ForeignKeyMetadata";
import {JunctionTableMetadata} from "../metadata/JunctionTableMetadata";
import {defaultMetadataStorage} from "../typeorm";
import {UsingJoinTableIsNotAllowedError} from "./error/UsingJoinTableIsNotAllowedError";
import {UsingJoinTableOnlyOnOneSideAllowedError} from "./error/UsingJoinTableOnlyOnOneSideAllowedError";
import {UsingJoinColumnIsNotAllowedError} from "./error/UsingJoinColumnIsNotAllowedError";
import {UsingJoinColumnOnlyOnOneSideAllowedError} from "./error/UsingJoinColumnOnlyOnOneSideAllowedError";
import {MissingJoinColumnError} from "./error/MissingJoinColumnError";
import {MissingJoinTableError} from "./error/MissingJoinTableError";
import {EntityMetadataValidator} from "./EntityMetadataValidator";
import {IndexMetadata} from "../metadata/IndexMetadata";
import {CompositeIndexMetadata} from "../metadata/CompositeIndexMetadata";
import {PropertyMetadataCollection} from "../metadata/collection/PropertyMetadataCollection";
import {TargetMetadataCollection} from "../metadata/collection/TargetMetadataCollection";
import {JoinTableMetadata} from "../metadata/JoinTableMetadata";
import {JoinTableOptions} from "../metadata/options/JoinTableOptions";
import {JoinColumnMetadata} from "../metadata/JoinColumnMetadata";
import {JoinColumnOptions} from "../metadata/options/JoinColumnOptions";
import {TableMetadata} from "../metadata/TableMetadata";
import {ColumnTypes} from "../metadata/types/ColumnTypes";
import {defaultMetadataStorage} from "../typeorm";
/**
* Aggregates all metadata: table, column, relation into one collection grouped by tables for a given set of classes.
@ -88,6 +80,10 @@ export class EntityMetadataBuilder {
// merge indices and composite indices because simple indices actually are compose indices with only one column
this.mergeIndicesAndCompositeIndices(mergedMetadata.indexMetadatas, mergedMetadata.compositeIndexMetadatas);
// todo: check if multiple tree parent metadatas in validator
// todo: tree decorators can be used only on closure table (validation)
// todo: throw error if parent tree metadata was not specified in a closure table
// create a new entity metadata
const entityMetadata = new EntityMetadata(
this.namingStrategy,
@ -175,11 +171,58 @@ export class EntityMetadataBuilder {
});
});
// generate closure tables
const closureJunctionEntityMetadatas: EntityMetadata[] = [];
entityMetadatas
.filter(metadata => metadata.table.isClosure)
.forEach(metadata => {
const closureTableName = this.namingStrategy.closureJunctionTableName(metadata.table.name);
const closureJunctionTableMetadata = new TableMetadata(undefined, closureTableName, "closureJunction");
const columns = [
new ColumnMetadata({
propertyType: metadata.primaryColumn.type,
options: {
length: metadata.primaryColumn.length,
type: metadata.primaryColumn.type,
name: "ancestor"
}
}),
new ColumnMetadata({
propertyType: metadata.primaryColumn.type,
options: {
length: metadata.primaryColumn.length,
type: metadata.primaryColumn.type,
name: "descendant"
}
})
];
if (metadata.treeLevelColumn) {
columns.push(new ColumnMetadata({
propertyType: ColumnTypes.INTEGER,
options: {
type: ColumnTypes.INTEGER,
name: "level"
}
}));
}
const closureJunctionEntityMetadata = new EntityMetadata(this.namingStrategy, closureJunctionTableMetadata, columns, [], []);
closureJunctionEntityMetadata.foreignKeys.push(
new ForeignKeyMetadata(closureJunctionTableMetadata, [columns[0]], metadata.table, [metadata.primaryColumn]),
new ForeignKeyMetadata(closureJunctionTableMetadata, [columns[1]], metadata.table, [metadata.primaryColumn])
);
closureJunctionEntityMetadatas.push(closureJunctionEntityMetadata);
metadata.closureJunctionTable = closureJunctionEntityMetadata;
});
// generate junction tables with its columns and foreign keys
const junctionEntityMetadatas: EntityMetadata[] = [];
entityMetadatas.forEach(metadata => {
metadata.ownerManyToManyRelations.map(relation => {
const tableMetadata = new JunctionTableMetadata(relation.joinTable.name);
const tableMetadata = new TableMetadata(undefined, relation.joinTable.name, "junction");
const column1 = relation.joinTable.referencedColumn;
const column2 = relation.joinTable.inverseReferencedColumn;
@ -215,7 +258,9 @@ export class EntityMetadataBuilder {
});
});
return entityMetadatas.concat(junctionEntityMetadatas);
return entityMetadatas
.concat(junctionEntityMetadatas)
.concat(closureJunctionEntityMetadatas);
}
}

View File

@ -10,6 +10,8 @@ import {JoinColumnMetadata} from "../metadata/JoinColumnMetadata";
import {JoinTableMetadata} from "../metadata/JoinTableMetadata";
import {TargetMetadataCollection} from "../metadata/collection/TargetMetadataCollection";
import {PropertyMetadataCollection} from "../metadata/collection/PropertyMetadataCollection";
import {PropertyMetadata} from "../metadata/PropertyMetadata";
import {RelationsCountMetadata} from "../metadata/RelationsCountMetadata";
/**
* Storage all metadatas of all available types: tables, fields, subscribers, relations, etc.
@ -36,41 +38,13 @@ export class MetadataStorage {
readonly joinTableMetadatas = new PropertyMetadataCollection<JoinTableMetadata>();
readonly indexMetadatas = new PropertyMetadataCollection<IndexMetadata>();
readonly entityListenerMetadatas = new PropertyMetadataCollection<EntityListenerMetadata>();
readonly relationCountMetadatas = new PropertyMetadataCollection<RelationsCountMetadata>();
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(tableMetadatas?: TargetMetadataCollection<TableMetadata>,
namingStrategyMetadatas?: TargetMetadataCollection<NamingStrategyMetadata>,
eventSubscriberMetadatas?: TargetMetadataCollection<EventSubscriberMetadata>,
compositeIndexMetadatas?: TargetMetadataCollection<CompositeIndexMetadata>,
columnMetadatas?: PropertyMetadataCollection<ColumnMetadata>,
relationMetadatas?: PropertyMetadataCollection<RelationMetadata>,
joinColumnMetadatas?: PropertyMetadataCollection<JoinColumnMetadata>,
joinTableMetadatas?: PropertyMetadataCollection<JoinTableMetadata>,
indexMetadatas?: PropertyMetadataCollection<IndexMetadata>,
entityListenerMetadatas?: PropertyMetadataCollection<EntityListenerMetadata>) {
if (tableMetadatas)
this.tableMetadatas = tableMetadatas;
if (namingStrategyMetadatas)
this.namingStrategyMetadatas = namingStrategyMetadatas;
if (eventSubscriberMetadatas)
this.eventSubscriberMetadatas = eventSubscriberMetadatas;
if (compositeIndexMetadatas)
this.compositeIndexMetadatas = compositeIndexMetadatas;
if (columnMetadatas)
this.columnMetadatas = columnMetadatas;
if (relationMetadatas)
this.relationMetadatas = relationMetadatas;
if (joinColumnMetadatas)
this.joinColumnMetadatas = joinColumnMetadatas;
if (joinTableMetadatas)
this.joinTableMetadatas = joinTableMetadatas;
if (indexMetadatas)
this.indexMetadatas = indexMetadatas;
if (entityListenerMetadatas)
this.entityListenerMetadatas = entityListenerMetadatas;
constructor() {
}
// -------------------------------------------------------------------------
@ -91,6 +65,7 @@ export class MetadataStorage {
const joinTableMetadatas = this.joinTableMetadatas.filterByClass(tableMetadata.target);
const indexMetadatas = this.indexMetadatas.filterByClass(tableMetadata.target);
const entityListenerMetadatas = this.entityListenerMetadatas.filterByClass(tableMetadata.target);
const relationCountMetadatas = this.relationCountMetadatas.filterByClass(tableMetadata.target);
allTableMetadatas
.filter(metadata => tableMetadata.isInherited(metadata))
@ -103,6 +78,7 @@ export class MetadataStorage {
joinTableMetadatas.push(...metadatasFromAbstract.joinTableMetadatas.filterRepeatedMetadatas(joinTableMetadatas));
indexMetadatas.push(...metadatasFromAbstract.indexMetadatas.filterRepeatedMetadatas(indexMetadatas));
entityListenerMetadatas.push(...metadatasFromAbstract.entityListenerMetadatas.filterRepeatedMetadatas(entityListenerMetadatas));
relationCountMetadatas.push(...metadatasFromAbstract.relationCountMetadatas.filterRepeatedMetadatas(relationCountMetadatas));
});
return {
@ -112,7 +88,8 @@ export class MetadataStorage {
joinColumnMetadatas: joinColumnMetadatas,
joinTableMetadatas: joinTableMetadatas,
indexMetadatas: indexMetadatas,
entityListenerMetadatas: entityListenerMetadatas
entityListenerMetadatas: entityListenerMetadatas,
relationCountMetadatas: relationCountMetadatas
};
}

View File

@ -1,7 +1,9 @@
import {PropertyMetadata} from "./PropertyMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {ColumnType} from "./types/ColumnTypes";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {ColumnMetadataArgs} from "./args/ColumnMetadataArgs";
import {ColumnType} from "./types/ColumnTypes";
export type ColumnMode = "regular"|"createDate"|"updateDate"|"version"|"treeChildrenCount"|"treeLevel";
/**
* This metadata contains all information about class's column.
@ -31,6 +33,11 @@ export class ColumnMetadata extends PropertyMetadata {
*/
readonly type: ColumnType;
/**
* The mode of the column.
*/
readonly mode: ColumnMode;
/**
* Maximum length in the database.
*/
@ -57,22 +64,7 @@ export class ColumnMetadata extends PropertyMetadata {
readonly isNullable = false;
/**
* Indicates if column will contain a created date or not.
*/
readonly isCreateDate = false;
/**
* Indicates if column will contain an updated date or not.
*/
readonly isUpdateDate = false;
/**
* Indicates if column will contain a version.
*/
readonly isVersion = false;
/**
* Indicates if column will contain an updated date or not.
* Indicates if column is virtual. Virtual columns are not mapped to the entity.
*/
readonly isVirtual = false;
@ -126,12 +118,8 @@ export class ColumnMetadata extends PropertyMetadata {
if (args.isPrimaryKey)
this.isPrimary = args.isPrimaryKey;
if (args.isCreateDate)
this.isCreateDate = args.isCreateDate;
if (args.isUpdateDate)
this.isUpdateDate = args.isUpdateDate;
if (args.isVersion)
this.isVersion = args.isVersion;
if (args.mode)
this.mode = args.mode;
if (args.isVirtual)
this.isVirtual = args.isVirtual;
if (args.propertyType)
@ -176,5 +164,17 @@ export class ColumnMetadata extends PropertyMetadata {
return this.namingStrategy ? this.namingStrategy.columnName(this.propertyName) : this.propertyName;
}
get isUpdateDate() {
return this.mode === "updateDate";
}
get isCreateDate() {
return this.mode === "createDate";
}
get isVersion() {
return this.mode === "version";
}
}

View File

@ -1,5 +1,5 @@
import {TargetMetadata} from "./TargetMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "./EntityMetadata";
import {CompositeIndexOptions} from "./options/CompositeIndexOptions";

View File

@ -4,13 +4,20 @@ import {RelationMetadata} from "./RelationMetadata";
import {CompositeIndexMetadata} from "./CompositeIndexMetadata";
import {RelationTypes} from "./types/RelationTypes";
import {ForeignKeyMetadata} from "./ForeignKeyMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {PropertyMetadata} from "./PropertyMetadata";
/**
* Contains all entity metadata.
*/
export class EntityMetadata {
// -------------------------------------------------------------------------
// Properties
// -------------------------------------------------------------------------
closureJunctionTable: EntityMetadata;
// -------------------------------------------------------------------------
// Readonly Properties
// -------------------------------------------------------------------------
@ -51,6 +58,9 @@ export class EntityMetadata {
// -------------------------------------------------------------------------
get name(): string {
if (!this.table || !this.table.target)
throw new Error("No table target set to the entity metadata.");
return (<any> this.table.target).name;
}
@ -91,17 +101,25 @@ export class EntityMetadata {
}
get createDateColumn(): ColumnMetadata {
return this.columns.find(column => column.isCreateDate);
return this.columns.find(column => column.mode === "createDate");
}
get updateDateColumn(): ColumnMetadata {
return this.columns.find(column => column.isUpdateDate);
return this.columns.find(column => column.mode === "updateDate");
}
get versionColumn(): ColumnMetadata {
return this.columns.find(column => column.isVersion);
return this.columns.find(column => column.mode === "version");
}
get treeChildrenCountColumn(): ColumnMetadata {
return this.columns.find(column => column.mode === "treeChildrenCount");
}
get treeLevelColumn(): ColumnMetadata {
return this.columns.find(column => column.mode === "treeLevel");
}
get hasPrimaryKey(): boolean {
return !!this.primaryColumn;
}
@ -191,5 +209,13 @@ export class EntityMetadata {
hasRelationWithManyWithName(name: string): boolean {
return !!this.findRelationWithManyWithDbName(name);
}
get treeParentRelation() {
return this.relations.find(relation => relation.isTreeParent);
}
get treeChildrenRelation() {
return this.relations.find(relation => relation.isTreeChildren);
}
}

View File

@ -1,5 +1,5 @@
import {PropertyMetadata} from "./PropertyMetadata";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
/**
* This metadata interface contains all information about some index on a field.

View File

@ -1,6 +1,6 @@
import {PropertyMetadata} from "./PropertyMetadata";
import {JoinColumnOptions} from "./options/JoinColumnOptions";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {RelationMetadata} from "./RelationMetadata";
import {ColumnMetadata} from "./ColumnMetadata";

View File

@ -1,16 +0,0 @@
import {TableMetadata} from "./TableMetadata";
/**
* This metadata interface contains all information about junction table.
*/
export class JunctionTableMetadata extends TableMetadata {
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(name: string) {
super(undefined, name);
}
}

View File

@ -3,7 +3,7 @@ import {TargetMetadata} from "./TargetMetadata";
/**
* This represents metadata of some object's property.
*/
export abstract class PropertyMetadata extends TargetMetadata {
export class PropertyMetadata extends TargetMetadata {
// ---------------------------------------------------------------------
// Readonly Properties

View File

@ -1,12 +1,11 @@
import {PropertyMetadata} from "./PropertyMetadata";
import {RelationTypes, RelationType} from "./types/RelationTypes";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategy";
import {NamingStrategyInterface} from "../naming-strategy/NamingStrategyInterface";
import {EntityMetadata} from "./EntityMetadata";
import {OnDeleteType} from "./ForeignKeyMetadata";
import {JoinTableMetadata} from "./JoinTableMetadata";
import {JoinColumnMetadata} from "./JoinColumnMetadata";
import {RelationMetadataArgs} from "./args/RelationMetadataArgs";
import {ColumnMetadata} from "./ColumnMetadata";
/**
* Function that returns a type of the field. Returned value must be a class used on the relation.
@ -62,6 +61,16 @@ export class RelationMetadata extends PropertyMetadata {
// Readonly Properties
// ---------------------------------------------------------------------
/**
* Indicates if this is a parent (can be only many-to-one relation) relation in the tree tables.
*/
readonly isTreeParent: boolean = false;
/**
* Indicates if this is a children (can be only one-to-many relation) relation in the tree tables.
*/
readonly isTreeChildren: boolean = false;
/**
* Relation type.
*/
@ -128,8 +137,9 @@ export class RelationMetadata extends PropertyMetadata {
constructor(args: RelationMetadataArgs) {
super(args.target, args.propertyName);
this.relationType = args.relationType;
this._inverseSideProperty = args.inverseSideProperty;
if (args.inverseSideProperty)
this._inverseSideProperty = args.inverseSideProperty;
if (args.options.name)
this._name = args.options.name;
if (args.propertyType)
@ -146,6 +156,10 @@ export class RelationMetadata extends PropertyMetadata {
this.isNullable = args.options.nullable;
if (args.options.onDelete)
this.onDelete = args.options.onDelete;
if (args.isTreeParent)
this.isTreeParent = true;
if (args.isTreeChildren)
this.isTreeChildren = true;
if (!this._type)
this._type = args.type;
@ -178,11 +192,23 @@ export class RelationMetadata extends PropertyMetadata {
}
get inverseSideProperty(): string {
return this.computeInverseSide(this._inverseSideProperty);
if (this._inverseSideProperty) {
return this.computeInverseSide(this._inverseSideProperty);
} else if (this.isTreeParent) {
return this.entityMetadata.treeChildrenRelation.propertyName;
} else if (this.isTreeChildren) {
return this.entityMetadata.treeParentRelation.propertyName;
}
return "";
}
get inverseRelation(): RelationMetadata {
return this.inverseEntityMetadata.findRelationWithPropertyName(this.computeInverseSide(this._inverseSideProperty));
return this.inverseEntityMetadata.findRelationWithPropertyName(this.inverseSideProperty);
}
get isOneToOne(): boolean {

View File

@ -0,0 +1,25 @@
import {PropertyMetadata} from "./PropertyMetadata";
/**
*/
export class RelationsCountMetadata extends PropertyMetadata {
// ---------------------------------------------------------------------
// Readonly Properties
// ---------------------------------------------------------------------
/**
* The real reflected property type.
*/
readonly relation: string|((object: any) => any);
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(target: Function, propertyName: string, relation: string|((object: any) => any)) {
super(target, propertyName);
this.relation = relation;
}
}

View File

@ -1,6 +1,11 @@
import {TargetMetadata} from "./TargetMetadata";
import {EntityMetadata} from "./EntityMetadata";
/**
* Table type.
*/
export type TableType = "regular"|"abstract"|"junction"|"closure"|"closureJunction";
/**
* This metadata interface contains all information about specific table.
*/
@ -16,43 +21,57 @@ export class TableMetadata extends TargetMetadata {
entityMetadata: EntityMetadata;
// ---------------------------------------------------------------------
// Readonly Properties
// Private Properties
// ---------------------------------------------------------------------
/**
* Indicates if this table is abstract or not. Regular tables can inherit columns from abstract tables.
*/
readonly isAbstract = false;
// ---------------------------------------------------------------------
// Private Properties
// ---------------------------------------------------------------------
private readonly tableType: TableType;
/**
* Table name in the database.
*/
private _name: string;
private readonly _name: string;
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(target?: Function, name?: string);
constructor(target: Function, isAbstract: boolean);
constructor(target: Function, nameOrIsAbstract?: string|boolean, maybeIsAbstract?: boolean) {
constructor(target?: Function, name?: string, type: TableType = "regular") {
super(target);
if (typeof nameOrIsAbstract === "string")
this._name = nameOrIsAbstract;
if (typeof nameOrIsAbstract === "boolean")
this.isAbstract = nameOrIsAbstract;
if (typeof maybeIsAbstract === "boolean")
this.isAbstract = maybeIsAbstract;
if (name)
this._name = name;
if (type)
this.tableType = type;
}
// ---------------------------------------------------------------------
// Getters
// Accessors
// ---------------------------------------------------------------------
/**
* Checks if this table is abstract.
*/
get isAbstract() {
return this.tableType === "abstract";
}
/**
* Checks if this table is regular (non abstract and non closure).
*/
get isRegular() {
return this.tableType === "regular";
}
/**
* Checks if this table is a closure table.
*/
get isClosure() {
return this.tableType === "closure";
}
/**
* Table name in the database.
*/
@ -63,6 +82,10 @@ export class TableMetadata extends TargetMetadata {
return this.entityMetadata.namingStrategy.tableName((<any>this.target).name);
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
/**
* Checks if this table is inherited from another table.
*/

View File

@ -1,4 +1,5 @@
import {ColumnOptions} from "../options/ColumnOptions";
import {ColumnMode} from "../ColumnMetadata";
/**
* Constructor arguments for ColumnMetadata class.
@ -25,25 +26,15 @@ export interface ColumnMetadataArgs {
*/
isPrimaryKey?: boolean;
/**
* Indicates if this column is create date column or not.
*/
isCreateDate?: boolean;
/**
* Indicates if this column is update date column or not.
*/
isUpdateDate?: boolean;
/**
* Indicates if this column is virtual or not.
*/
isVirtual?: boolean;
/**
* Indicates if this column is version column.
* Column mode.
*/
isVersion?: boolean;
mode?: ColumnMode;
/**
* Indicates if this column is order id column.

View File

@ -36,11 +36,21 @@ export interface RelationMetadataArgs {
/**
* Inverse side of the relation.
*/
inverseSideProperty: PropertyTypeInFunction<any>;
inverseSideProperty?: PropertyTypeInFunction<any>;
/**
* Additional relation options.
*/
options: RelationOptions;
/**
* Indicates if this is a parent (can be only many-to-one relation) relation in the tree tables.
*/
isTreeParent?: boolean;
/**
* Indicates if this is a children (can be only one-to-many relation) relation in the tree tables.
*/
isTreeChildren?: boolean;
}

View File

@ -1,4 +1,4 @@
import {NamingStrategyInterface} from "./NamingStrategy";
import {NamingStrategyInterface} from "./NamingStrategyInterface";
import * as _ from "lodash";
/**
@ -52,5 +52,9 @@ export class DefaultNamingStrategy implements NamingStrategyInterface {
const column2 = secondTableName + "_" + secondColumnName;
return column1 === column2 ? column1 + "_2" : column1;
}
closureJunctionTableName(tableName: string): string {
return tableName + "_closure";
}
}

View File

@ -49,4 +49,9 @@ export interface NamingStrategyInterface {
*/
joinTableInverseColumnName(tableName: string, columnName: string, secondTableName: string, secondColumnName: string): string;
/**
* Gets the name for the closure junction table.
*/
closureJunctionTableName(tableName: string): string;
}

View File

@ -37,6 +37,8 @@ export class PersistOperationExecutor {
.then(() => this.broadcastBeforeEvents(persistOperation))
.then(() => this.driver.beginTransaction())
.then(() => this.executeInsertOperations(persistOperation))
.then(() => this.executeInsertClosureTableOperations(persistOperation))
.then(() => this.executeUpdateTreeLevelOperations(persistOperation))
.then(() => this.executeInsertJunctionsOperations(persistOperation))
.then(() => this.executeRemoveJunctionsOperations(persistOperation))
.then(() => this.executeUpdateRelationsOperations(persistOperation))
@ -59,10 +61,6 @@ export class PersistOperationExecutor {
*/
private broadcastBeforeEvents(persistOperation: PersistOperation) {
/*console.log("persistOperation.allPersistedEntities: ", persistOperation.allPersistedEntities);
console.log("inserts", persistOperation.inserts);
console.log("updates", persistOperation.updates);*/
const insertEvents = persistOperation.inserts.map(insertOperation => {
const persistedEntityWithId = persistOperation.allPersistedEntities.find(e => e.entity === insertOperation.entity);
return this.broadcaster.broadcastBeforeInsertEvent(persistedEntityWithId.entity);
@ -117,6 +115,33 @@ export class PersistOperationExecutor {
}));
}
/**
* Executes insert operations for closure tables.
*/
private executeInsertClosureTableOperations(persistOperation: PersistOperation) {
const promises = persistOperation.inserts
.filter(operation => {
const metadata = this.entityMetadatas.findByTarget(operation.entity.constructor);
return metadata.table.isClosure;
})
.map(operation => {
const relationsUpdateMap = this.findUpdateOperationForEntity(persistOperation.updatesByRelations, persistOperation.inserts, operation.entity);
return this.insertIntoClosureTable(operation, relationsUpdateMap).then(level => {
operation.treeLevel = level;
});
});
return Promise.all(promises);
}
/**
* Executes update tree level operations in inserted entities right after data into closure table inserted.
*/
private executeUpdateTreeLevelOperations(persistOperation: PersistOperation) {
return Promise.all(persistOperation.inserts.map(operation => {
return this.updateTreeLevel(operation);
}));
}
/**
* Executes insert junction operations.
*/
@ -140,7 +165,7 @@ export class PersistOperationExecutor {
*/
private executeUpdateRelationsOperations(persistOperation: PersistOperation) {
return Promise.all(persistOperation.updatesByRelations.map(updateByRelation => {
this.updateByRelation(updateByRelation, persistOperation.inserts);
return this.updateByRelation(updateByRelation, persistOperation.inserts);
}));
}
@ -198,6 +223,14 @@ export class PersistOperationExecutor {
insertOperation.entity[metadata.createDateColumn.propertyName] = insertOperation.date;
if (metadata.versionColumn)
insertOperation.entity[metadata.versionColumn.propertyName]++;
if (metadata.treeLevelColumn) {
// const parentEntity = insertOperation.entity[metadata.treeParentMetadata.propertyName];
// const parentLevel = parentEntity ? (parentEntity[metadata.treeLevelColumn.name] || 0) : 0;
insertOperation.entity[metadata.treeLevelColumn.propertyName] = insertOperation.treeLevel;
}
if (metadata.treeChildrenCountColumn) {
insertOperation.entity[metadata.treeChildrenCountColumn.propertyName] = 0;
}
});
persistOperation.updates.forEach(updateOperation => {
const metadata = this.entityMetadatas.findByTarget(updateOperation.entity.constructor);
@ -224,6 +257,27 @@ export class PersistOperationExecutor {
});
}
private findUpdateOperationForEntity(operations: UpdateByRelationOperation[], insertOperations: InsertOperation[], target: any): { [key: string]: any } {
let updateMap: { [key: string]: any } = {};
operations
.forEach(operation => { // duplication with updateByRelation method
const relatedInsertOperation = insertOperations.find(o => o.entity === operation.targetEntity);
const idInInserts = relatedInsertOperation ? relatedInsertOperation.entityId : null;
if (operation.updatedRelation.isOneToMany) {
const metadata = this.entityMetadatas.findByTarget(operation.insertOperation.entity.constructor);
if (operation.insertOperation.entity === target)
updateMap[operation.updatedRelation.inverseRelation.name] = operation.targetEntity[metadata.primaryColumn.propertyName] || idInInserts;
} else {
if (operation.targetEntity === target)
updateMap[operation.updatedRelation.name] = operation.insertOperation.entityId;
}
});
return updateMap;
}
private updateByRelation(operation: UpdateByRelationOperation, insertOperations: InsertOperation[]) {
let tableName: string, relationName: string, relationId: any, idColumn: string, id: any;
const relatedInsertOperation = insertOperations.find(o => o.entity === operation.targetEntity);
@ -325,9 +379,58 @@ export class PersistOperationExecutor {
allValues.push(this.driver.preparePersistentValue(1, metadata.versionColumn));
}
if (metadata.treeLevelColumn) {
const parentEntity = entity[metadata.treeParentRelation.propertyName];
const parentLevel = parentEntity ? (parentEntity[metadata.treeLevelColumn.name] || 0) : 0;
allColumns.push(metadata.treeLevelColumn.name);
allValues.push(parentLevel + 1);
}
if (metadata.treeChildrenCountColumn) {
allColumns.push(metadata.treeChildrenCountColumn.name);
allValues.push(0);
}
return this.driver.insert(metadata.table.name, this.zipObject(allColumns, allValues));
}
private insertIntoClosureTable(operation: InsertOperation, updateMap: { [key: string]: any }) {
const entity = operation.entity;
const metadata = this.entityMetadatas.findByTarget(entity.constructor);
const parentEntity = entity[metadata.treeParentRelation.propertyName];
const hasLevel = !!metadata.treeLevelColumn;
let parentEntityId: any = 0;
if (parentEntity && parentEntity[metadata.primaryColumn.name]) {
parentEntityId = parentEntity[metadata.primaryColumn.name];
} else if (updateMap && updateMap[metadata.treeParentRelation.propertyName]) { // todo: name or propertyName: depend how update will be implemented. or even find relation of this treeParent and use its name?
parentEntityId = updateMap[metadata.treeParentRelation.propertyName];
}
return this.driver.insertIntoClosureTable(metadata.closureJunctionTable.table.name, operation.entityId, parentEntityId, hasLevel)
/*.then(() => {
// we also need to update children count in parent
if (parentEntity && parentEntityId) {
const values = { [metadata.treeChildrenCountColumn.name]: parentEntity[metadata.treeChildrenCountColumn.name] + 1 };
return this.driver.update(metadata.table.name, values, { [metadata.primaryColumn.name]: parentEntityId });
}
return;
})*/;
}
private updateTreeLevel(operation: InsertOperation) {
const metadata = this.entityMetadatas.findByTarget(operation.entity.constructor);
if (metadata.treeLevelColumn && operation.treeLevel) {
const values = { [metadata.treeLevelColumn.name]: operation.treeLevel };
return this.driver.update(metadata.table.name, values, { [metadata.primaryColumn.name]: operation.entityId });
}
return Promise.resolve();
}
private insertJunctions(junctionOperation: JunctionInsertOperation, insertOperations: InsertOperation[]) {
const junctionMetadata = junctionOperation.metadata;
const metadata1 = this.entityMetadatas.findByTarget(junctionOperation.entity1.constructor);

View File

@ -2,6 +2,9 @@
* @internal
*/
export class InsertOperation {
public treeLevel: number;
constructor(public entity: any,
public entityId?: number,
public date = new Date()) {

View File

@ -15,6 +15,7 @@ export interface Join {
type: "LEFT"|"INNER";
conditionType: "ON"|"WITH";
condition: string;
tableName: string;
}
export class QueryBuilder<Entity> {
@ -175,17 +176,20 @@ export class QueryBuilder<Entity> {
join(joinType: "INNER"|"LEFT", entityOrProperty: Function|string, alias: string, conditionType: "ON"|"WITH", condition: string, parameters?: { [key: string]: any }): this;
join(joinType: "INNER"|"LEFT", entityOrProperty: Function|string, alias: string, conditionType: "ON"|"WITH" = "ON", condition: string = "", parameters?: { [key: string]: any }): this {
let tableName = "";
const aliasObj = new Alias(alias);
this.aliasMap.addAlias(aliasObj);
if (entityOrProperty instanceof Function) {
aliasObj.target = entityOrProperty;
} else if (typeof entityOrProperty === "string") {
} else if (typeof entityOrProperty === "string" && entityOrProperty.indexOf(".") !== -1) {
aliasObj.parentAliasName = entityOrProperty.split(".")[0];
aliasObj.parentPropertyName = entityOrProperty.split(".")[1];
} else if (typeof entityOrProperty === "string") {
tableName = entityOrProperty;
}
const join: Join = { type: joinType, alias: aliasObj, conditionType: conditionType, condition: condition };
const join: Join = { type: joinType, alias: aliasObj, tableName: tableName, conditionType: conditionType, condition: condition };
this.joins.push(join);
if (parameters) this.addParameters(parameters);
return this;
@ -306,10 +310,18 @@ export class QueryBuilder<Entity> {
getSingleScalarResult<T>(): Promise<T> {
return this.getScalarResults().then(results => results[0]);
}
getResults(): Promise<Entity[]> {
return this.getResultsAndScalarResults().then(results => {
return results.entities;
});
}
getResultsAndScalarResults(): Promise<{ entities: Entity[], scalarResults: any[] }> {
const mainAlias = this.aliasMap.mainAlias.name;
let scalarResults: any[];
if (this.firstResult || this.maxResults) {
const metadata = this.entityMetadatas.findByTarget(this.fromEntity.alias.target);
let idsQuery = `SELECT DISTINCT(distinctAlias.${mainAlias}_${metadata.primaryColumn.name}) as ids`;
@ -325,6 +337,7 @@ export class QueryBuilder<Entity> {
return this.driver
.query<any[]>(idsQuery)
.then((results: any[]) => {
scalarResults = results;
const ids = results.map(result => result["ids"]).join(", ");
if (ids.length === 0)
return Promise.resolve([]);
@ -335,17 +348,32 @@ export class QueryBuilder<Entity> {
})
.then(results => this.rawResultsToEntities(results))
.then(results => this.addLazyProperties(results))
.then(results => this.broadcaster.broadcastLoadEventsForAll(results).then(() => results));
.then(results => this.broadcaster.broadcastLoadEventsForAll(results).then(() => results))
.then(results => {
return {
entities: results,
scalarResults: scalarResults
};
});
} else {
return this.driver
.query<any[]>(this.getSql())
.then(results => this.rawResultsToEntities(results))
.then(results => {
scalarResults = results;
return this.rawResultsToEntities(results);
})
.then(results => this.addLazyProperties(results))
.then(results => {
return this.broadcaster
.broadcastLoadEventsForAll(results)
.then(() => results);
})
.then(results => {
return {
entities: results,
scalarResults: scalarResults
};
});
}
}
@ -399,7 +427,7 @@ export class QueryBuilder<Entity> {
}
this.joins.forEach(join => {
const property = join.alias.target || (join.alias.parentAliasName + "." + join.alias.parentPropertyName);
const property = join.tableName || join.alias.target || (join.alias.parentAliasName + "." + join.alias.parentPropertyName);
qb.join(join.type, property, join.alias.name, join.conditionType, join.condition);
});
@ -574,8 +602,7 @@ export class QueryBuilder<Entity> {
protected createJoinExpression() {
return this.joins.map(join => {
const joinType = join.type; // === "INNER" ? "INNER" : "LEFT";
const joinMetadata = this.aliasMap.getEntityMetadataByAlias(join.alias);
const joinTableName = joinMetadata.table.name;
const joinTableName = join.tableName ? join.tableName : this.aliasMap.getEntityMetadataByAlias(join.alias).table.name;
const parentAlias = join.alias.parentAliasName;
if (!parentAlias) {
return " " + joinType + " JOIN " + joinTableName + " " + join.alias.name + " " + join.conditionType + " " + join.condition;

View File

@ -13,7 +13,7 @@ export class ReactiveRepository<Entity> {
// Constructor
// -------------------------------------------------------------------------
constructor(private repository: Repository<Entity>) {
constructor(protected repository: Repository<Entity>) {
}

View File

@ -0,0 +1,95 @@
import {ReactiveRepository} from "./ReactiveRepository";
import {TreeRepository} from "./TreeRepository";
import {QueryBuilder} from "../query-builder/QueryBuilder";
/**
* Tree repository is supposed to work with your entity objects. Find entities, insert, update, delete, etc.
* This version of TreeRepository is using rxjs library and Observables instead of promises.
*/
export class ReactiveTreeRepository<Entity> extends ReactiveRepository<Entity> {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(protected repository: TreeRepository<Entity>) {
super(repository);
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Roots are entities that have no ancestors. Finds them all.
*/
findRoots(): Promise<Entity[]> {
return this.repository.findRoots();
}
/**
* Creates a query builder used to get descendants of the entities in a tree.
*/
createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.repository.createDescendantsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
*/
findDescendants(entity: Entity): Promise<Entity[]> {
return this.repository.findDescendants(entity);
}
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
findDescendantsTree(entity: Entity): Promise<Entity> {
return this.repository.findDescendantsTree(entity);
}
/**
* Gets number of descendants of the entity.
*/
countDescendants(entity: Entity): Promise<number> {
return this.repository.countDescendants(entity);
}
/**
* Creates a query builder used to get ancestors of the entities in the tree.
*/
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
return this.repository.createAncestorsQueryBuilder(alias, closureTableAlias, entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
*/
findAncestors(entity: Entity): Promise<Entity[]> {
return this.repository.findAncestors(entity);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree(entity: Entity): Promise<Entity> {
return this.repository.findAncestorsTree(entity);
}
/**
* Gets number of ancestors of the entity.
*/
countAncestors(entity: Entity): Promise<number> {
return this.repository.countAncestors(entity);
}
/**
* Moves entity to the children of then given entity.
*
move(entity: Entity, to: Entity): Promise<void> {
return this.repository.move(entity, to);
}
*/
}

View File

@ -31,9 +31,9 @@ export class Repository<Entity> {
// Constructor
// -------------------------------------------------------------------------
constructor(private connection: Connection,
private entityMetadatas: EntityMetadataCollection,
private metadata: EntityMetadata) {
constructor(protected connection: Connection,
protected entityMetadatas: EntityMetadataCollection,
protected metadata: EntityMetadata) {
this.driver = connection.driver;
this.broadcaster = new Broadcaster(entityMetadatas, connection.eventSubscribers, connection.entityListeners); // todo: inject broadcaster from connection
this.persistOperationExecutor = new PersistOperationExecutor(connection.driver, entityMetadatas, this.broadcaster);

View File

@ -0,0 +1,157 @@
import {Repository} from "./Repository";
import {QueryBuilder} from "../query-builder/QueryBuilder";
/**
* Repository with additional functions to work with trees.
*/
export class TreeRepository<Entity> extends Repository<Entity> {
// todo: implement moving
// todo: implement removing
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Roots are entities that have no ancestors. Finds them all.
*/
findRoots(): Promise<Entity[]> {
const parentPropertyName = this.metadata.treeParentRelation.propertyName;
return this.createQueryBuilder("treeEntity")
.where(`treeEntity.${parentPropertyName} IS NULL`)
.getResults();
}
/**
* Creates a query builder used to get descendants of the entities in a tree.
*/
createDescendantsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
const joinCondition = `${alias}.${this.metadata.primaryColumn.name}=${closureTableAlias}.descendant`;
return this.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.table.name, closureTableAlias, "ON", joinCondition)
.where(`${closureTableAlias}.ancestor=${this.metadata.getEntityId(entity)}`);
}
/**
* Gets all children (descendants) of the given entity. Returns them all in a flat array.
*/
findDescendants(entity: Entity): Promise<Entity[]> {
return this
.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
.getResults();
}
/**
* Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other.
*/
findDescendantsTree(entity: Entity): Promise<Entity> {
// todo: throw exception if there is no column of this relation?
return this
.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
.getResultsAndScalarResults()
.then(entitiesAndScalars => {
const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.scalarResults);
this.buildChildrenEntityTree(entity, entitiesAndScalars.entities, relationMaps);
return entity;
});
}
/**
* Gets number of descendants of the entity.
*/
countDescendants(entity: Entity): Promise<number> {
return this
.createDescendantsQueryBuilder("treeEntity", "treeClosure", entity)
.getCount();
}
/**
* Creates a query builder used to get ancestors of the entities in the tree.
*/
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): QueryBuilder<Entity> {
const joinCondition = `${alias}.${this.metadata.primaryColumn.name}=${closureTableAlias}.ancestor`;
return this.createQueryBuilder(alias)
.innerJoin(this.metadata.closureJunctionTable.table.name, closureTableAlias, "ON", joinCondition)
.where(`${closureTableAlias}.descendant=${this.metadata.getEntityId(entity)}`);
}
/**
* Gets all parents (ancestors) of the given entity. Returns them all in a flat array.
*/
findAncestors(entity: Entity): Promise<Entity[]> {
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getResults();
}
/**
* Gets all parents (ancestors) of the given entity. Returns them in a tree - nested into each other.
*/
findAncestorsTree(entity: Entity): Promise<Entity> {
// todo: throw exception if there is no column of this relation?
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getResultsAndScalarResults()
.then(entitiesAndScalars => {
const relationMaps = this.createRelationMaps("treeEntity", entitiesAndScalars.scalarResults);
this.buildParentEntityTree(entity, entitiesAndScalars.entities, relationMaps);
return entity;
});
}
/**
* Gets number of ancestors of the entity.
*/
countAncestors(entity: Entity): Promise<number> {
return this
.createAncestorsQueryBuilder("treeEntity", "treeClosure", entity)
.getCount();
}
/**
* Moves entity to the children of then given entity.
*
move(entity: Entity, to: Entity): Promise<void> {
return Promise.resolve();
}
*/
// -------------------------------------------------------------------------
// Private Methods
// -------------------------------------------------------------------------
private createRelationMaps(alias: string, scalarResults: any[]): { id: any, parentId: any }[] {
return scalarResults.map(scalarResult => {
return {
id: scalarResult[alias + "_" + this.metadata.primaryColumn.name],
parentId: scalarResult[alias + "_" + this.metadata.treeParentRelation.name]
};
});
}
private buildChildrenEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
const childProperty = this.metadata.treeChildrenRelation.propertyName;
const parentEntityId = entity[this.metadata.primaryColumn.propertyName];
const childRelationMaps = relationMaps.filter(relationMap => relationMap.parentId === parentEntityId);
const childIds = childRelationMaps.map(relationMap => relationMap.id);
entity[childProperty] = entities.filter(entity => childIds.indexOf(entity[this.metadata.primaryColumn.propertyName]) !== -1);
entity[childProperty].forEach((childEntity: any) => {
this.buildChildrenEntityTree(childEntity, entities, relationMaps);
});
}
private buildParentEntityTree(entity: any, entities: any[], relationMaps: { id: any, parentId: any }[]): void {
const parentProperty = this.metadata.treeParentRelation.propertyName;
const entityId = entity[this.metadata.primaryColumn.propertyName];
const parentRelationMap = relationMaps.find(relationMap => relationMap.id === entityId);
if (parentRelationMap) {
const parentEntity = entities.find(entity => entity[this.metadata.primaryColumn.propertyName] === parentRelationMap.parentId);
if (parentEntity) {
entity[parentProperty] = parentEntity;
this.buildParentEntityTree(entity[parentProperty], entities, relationMaps);
}
}
}
}

2
src/trees.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./decorator/tree/TreeLevelColumn";
export * from "./decorator/tree/TreeParent";

View File

@ -92,7 +92,7 @@ export {CreateConnectionOptions} from "./connection-manager/CreateConnectionOpti
export {Driver} from "./driver/Driver";
export {MysqlDriver} from "./driver/MysqlDriver";
export {QueryBuilder} from "./query-builder/QueryBuilder";
export {EntityManager} from "./repository/EntityManager";
export {EntityManager} from "./entity-manager/EntityManager";
export {Repository} from "./repository/Repository";
export {FindOptions} from "./repository/FindOptions";
export {InsertEvent} from "./subscriber/event/InsertEvent";