mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added closure tables basic support
This commit is contained in:
parent
887cedaeb3
commit
76341fd9ec
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
119
sample/sample22-closure-table/app.ts
Normal file
119
sample/sample22-closure-table/app.ts
Normal 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));
|
||||
29
sample/sample22-closure-table/entity/Category.ts
Normal file
29
sample/sample22-closure-table/entity/Category.ts
Normal 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;
|
||||
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -24,7 +24,7 @@ export function CreateDateColumn(options?: ColumnOptions): Function {
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
propertyType: reflectedType,
|
||||
isCreateDate: true,
|
||||
mode: "createDate",
|
||||
options: options
|
||||
}));
|
||||
};
|
||||
|
||||
17
src/decorator/columns/RelationsCountColumn.ts
Normal file
17
src/decorator/columns/RelationsCountColumn.ts
Normal 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));
|
||||
};
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ export function UpdateDateColumn(options?: ColumnOptions): Function {
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
propertyType: reflectedType,
|
||||
isUpdateDate: true,
|
||||
mode: "updateDate",
|
||||
options: options
|
||||
}));
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ export function VersionColumn(options?: ColumnOptions): Function {
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
propertyType: reflectedType,
|
||||
isVersion: true,
|
||||
mode: "version",
|
||||
options: options
|
||||
}));
|
||||
};
|
||||
|
||||
@ -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"));
|
||||
};
|
||||
}
|
||||
12
src/decorator/tables/ClosureTable.ts
Normal file
12
src/decorator/tables/ClosureTable.ts
Normal 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"));
|
||||
};
|
||||
}
|
||||
27
src/decorator/tree/TreeChildren.ts
Normal file
27
src/decorator/tree/TreeChildren.ts
Normal 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
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
30
src/decorator/tree/TreeLevelColumn.ts
Normal file
30
src/decorator/tree/TreeLevelColumn.ts
Normal 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
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
25
src/decorator/tree/TreeParent.ts
Normal file
25
src/decorator/tree/TreeParent.ts
Normal 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
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
25
src/metadata/RelationsCountMetadata.ts
Normal file
25
src/metadata/RelationsCountMetadata.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
* @internal
|
||||
*/
|
||||
export class InsertOperation {
|
||||
|
||||
public treeLevel: number;
|
||||
|
||||
constructor(public entity: any,
|
||||
public entityId?: number,
|
||||
public date = new Date()) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -13,7 +13,7 @@ export class ReactiveRepository<Entity> {
|
||||
// Constructor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
constructor(private repository: Repository<Entity>) {
|
||||
constructor(protected repository: Repository<Entity>) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
95
src/repository/ReactiveTreeRepository.ts
Normal file
95
src/repository/ReactiveTreeRepository.ts
Normal 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);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
157
src/repository/TreeRepository.ts
Normal file
157
src/repository/TreeRepository.ts
Normal 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
2
src/trees.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./decorator/tree/TreeLevelColumn";
|
||||
export * from "./decorator/tree/TreeParent";
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user