diff --git a/CHANGELOG.md b/CHANGELOG.md index a229984370..463d6d4334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ feel free to ask us and community. * now relation id can be set directly to relation, e.g. `Post { @ManyToOne(type => Tag) tag: Tag|number }` with `post.tag = 1` usage. * now you can disable persistence on any relation by setting `@OneToMany(type => Post, post => tag, { persistence: false })`. This can dramatically improve entity save performance. * `loadAllRelationIds` method of `QueryBuilder` now accepts list of relation paths that needs to be loaded, also `disableMixedMap` option is now by default set to false, but you can enable it via new method parameter `options` -* lot of changes affect closure table pattern which is planned for fix in 0.3.0 * now `returning` and `output` statements of `InsertQueryBuilder` support array of columns as argument * now when many-to-many and one-to-many relation set to `null` all items from that relation are removed, just like it would be set to empty array * fixed issues with relation updation from one-to-one non-owner side @@ -53,6 +52,8 @@ Use `findOne(id)` method instead now. * `skipSync` in entity options has been renamed to `synchronize`. Now if it set to false schema synchronization for the entity will be disabled. By default its true. * now array initializations for relations are forbidden and ORM throws an error if there are entities with initialized relation arrays. +* `@ClosureEntity` decorator has been removed. Instead `@Entity` + `@Tree("closure-table")` must be used +* added support for nested set and materialized path tree hierarchy patterns ## 0.1.10 diff --git a/docs/decorator-reference.md b/docs/decorator-reference.md index 306334f336..5b68285f4f 100644 --- a/docs/decorator-reference.md +++ b/docs/decorator-reference.md @@ -749,6 +749,6 @@ Learn more about [custom entity repositories](working-with-entity-manager.md). ---- -Note: some decorators (like `@ClosureEntity`, `@SingleEntityChild`, `@ClassEntityChild`, `@DiscriminatorColumn`, etc.) aren't +Note: some decorators (like `@Tree`, `@ChildEntity`, etc.) aren't documented in this reference because they are treated as experimental at the moment. Expect to see their documentation in the future. \ No newline at end of file diff --git a/docs/entities.md b/docs/entities.md index 4bd725de4d..8ecceb8566 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -516,9 +516,10 @@ To learn more about closure table take a look at [this awesome presentation by B Example: ```typescript -import {ClosureEntity, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; +import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; -@ClosureEntity() +@Entity() +@Tree("closure-table") export class Category { @PrimaryGeneratedColumn() diff --git a/docs/repository-api.md b/docs/repository-api.md index fb27c02bf8..7b8cc66a9d 100644 --- a/docs/repository-api.md +++ b/docs/repository-api.md @@ -204,78 +204,7 @@ await repository.clear(); ## `TreeRepository` API -* `findTrees` - Gets complete tree for all roots in the table. - -```typescript -const treeCategories = await repository.findTrees(); -// returns root categories with sub categories inside -``` - -* `findRoots` - Roots are entities that have no ancestors. Finds them all. -Does not load children leafs. - -```typescript -const rootCategories = await repository.findRoots(); -// returns root categories without sub categories inside -``` - -* `findDescendants` - Gets all children (descendants) of the given entity. Returns them all in a flat array. - -```typescript -const childrens = await repository.findDescendants(parentCategory); -// returns all direct subcategories (without its nested categories) of a parentCategory -``` - -* `findDescendantsTree` - Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other. - -```typescript -const childrensTree = await repository.findDescendantsTree(parentCategory); -// returns all direct subcategories (with its nested categories) of a parentCategory -``` - -* `createDescendantsQueryBuilder` - Creates a query builder used to get descendants of the entities in a tree. - -```typescript -const childrens = await repository - .createDescendantsQueryBuilder("category", "categoryClosure", parentCategory) - .andWhere("category.type = 'secondary'") - .getMany(); -``` - -* `countDescendants` - Gets number of descendants of the entity. - -```typescript -const childrenCount = await repository.countDescendants(parentCategory); -``` - -* `findAncestors` - Gets all parent (ancestors) of the given entity. Returns them all in a flat array. - -```typescript -const parents = await repository.findAncestors(childCategory); -// returns all direct childCategory's parent categories (without "parent of parents") -``` - -* `findAncestorsTree` - Gets all parent (ancestors) of the given entity. Returns them in a tree - nested into each other. - -```typescript -const parentsTree = await repository.findAncestorsTree(childCategory); -// returns all direct childCategory's parent categories (with "parent of parents") -``` - -* `createAncestorsQueryBuilder` - Creates a query builder used to get ancestors of the entities in a tree. - -```typescript -const parents = await repository - .createAncestorsQueryBuilder("category", "categoryClosure", childCategory) - .andWhere("category.type = 'secondary'") - .getMany(); -``` - -* `countAncestors` - Gets the number of ancestors of the entity. - -```typescript -const parentsCount = await repository.countAncestors(childCategory); -``` +For `TreeRepository` API refer to [the Tree Entities documentation](./tree-entities.md#working-with-tree-entities). ## `MongoRepository` API diff --git a/docs/roadmap.md b/docs/roadmap.md index ab5e70c9ef..134cb45ed1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,12 +4,9 @@ See what amazing new features we are expecting to land in the next TypeORM versi ## Note on 1.0.0 release -We are planning to release a final stable `1.0.0` version somewhere in summer 2018. +We are planning to release a final stable `1.0.0` version in summer 2018. However TypeORM is already actively used in number of big production systems. -Main API is already very stable, there are only few issues currently we have in following areas: -`class and single table inheritance`, `naming strategy`, `subscribers`, `tree tables`. -All issues in those areas are planning to be fixed in next minor versions. -Your donations and contribution play a big role in achieving this goal. +Main API is already very stable. TypeORM follows a semantic versioning and until `1.0.0` breaking changes may appear in `0.x.x` versions, however since API is already quite stable we don't expect too much breaking changes. @@ -23,15 +20,11 @@ npm i typeorm@next ## 0.3.0 -- [ ] fix Oracle driver issues and make oracle stable and ready for production use - [ ] add `@Select` and `@Where` decorators - [ ] add `addSelectAndMap` functionality to `QueryBuilder` -- [ ] research NativeScript support - [ ] research internationalization features -- [ ] implement soft deletion - [ ] research ability to create one-to-many relations without inverse sides - [ ] research ability to create a single relation with multiple entities at once -- [ ] add more tree-table features: nested set and materialized path; more repository methods - [ ] cli: create database backup command - [ ] extend `query` method functionality - [ ] better support for entity schemas, support inheritance, add xml and yml formats support @@ -41,6 +34,10 @@ npm i typeorm@next ## 0.2.0 +- [ ] research NativeScript support +- [x] implement soft deletion +- [x] add more tree-table features: nested set and materialized path; more repository methods +- [ ] fix Oracle driver issues and make oracle stable and ready for production use - [ ] implement migrations generator for all drivers - [ ] create example how to use TypeORM in Electron apps - [ ] finish naming strategy implementation diff --git a/docs/tree-entities.md b/docs/tree-entities.md index 04fc125483..431455dd76 100644 --- a/docs/tree-entities.md +++ b/docs/tree-entities.md @@ -1,11 +1,15 @@ # Tree Entities TypeORM supports the Adjacency list and Closure table patterns for storing tree structures. +To learn more about hierarchy table take a look at [this awesome presentation by Bill Karwin](https://www.slideshare.net/billkarwin/models-for-hierarchical-data). * [Adjacency list](#adjacency-list) +* [Nested set](#nested-set) +* [Materialized Path (aka Path Enumeration)](#nested-set-aka-path-enumeration) * [Closure table](#closure-table) +* [Working with tree entities](#working-with-tree-entities) -### Adjacency list +## Adjacency list Adjacency list is a simple model with self-referencing. The benefit of this approach is simplicity, @@ -36,18 +40,18 @@ export class Category { ``` -### Closure table +## Nested set - -Closure table stores relations between parent and child in a separate table in a special way. -Its efficient in both reads and writes. -To learn more about closure table take a look at [this awesome presentation by Bill Karwin](https://www.slideshare.net/billkarwin/models-for-hierarchical-data). +Nested set is another pattern of storing tree structures in the database. +Its very efficient for reads, but bad for writes. +You cannot have multiple roots in nested set. Example: ```typescript -import {ClosureEntity, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; +import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; -@ClosureEntity() +@Entity() +@Tree("nested-set") export class Category { @PrimaryGeneratedColumn() @@ -56,16 +60,201 @@ export class Category { @Column() name: string; + @TreeChildren() + children: Category[]; + + @TreeParent() + parent: Category; +} +``` + +## Materialized Path (aka Path Enumeration) + +Materialized Path (also called Path Enumeration) is another pattern of storing tree structures in the database. +Its simple and effective. +Example: + +```typescript +import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; + +@Entity() +@Tree("materialized-path") +export class Category { + + @PrimaryGeneratedColumn() + id: number; + @Column() - description: string; + name: string; @TreeChildren() children: Category[]; @TreeParent() parent: Category; +} +``` - @TreeLevelColumn() - level: number; +## Closure table + +Closure table stores relations between parent and child in a separate table in a special way. +Its efficient for both reads and writes. +Example: + +```typescript +import {Entity, Tree, Column, PrimaryGeneratedColumn, TreeChildren, TreeParent, TreeLevelColumn} from "typeorm"; + +@Entity() +@Tree("closure-table") +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @TreeChildren() + children: Category[]; + + @TreeParent() + parent: Category; } ``` + +## Working with tree entities + +To make bind tree entities to each other its important to set to children entities their parent and save them, +for example: + +```typescript +const manager = getManager(); + +const a1 = new Category("a1"); +a1.name = "a1"; +await manager.save(a1); + +const a11 = new Category(); +a11.name = "a11"; +a11.parent = a1; +await manager.save(a11); + +const a12 = new Category(); +a12.name = "a12"; +a12.parent = a1; +await manager.save(a12); + +const a111 = new Category(); +a111.name = "a111"; +a111.parent = a11; +await manager.save(a111); + +const a112 = new Category(); +a112.name = "a112"; +a112.parent = a11; +await manager.save(a112); +``` + +To load such a tree use `TreeRepository`: + +```typescript +const manager = getManager(); +const trees = await manager.getTreeRepository(Category).findTrees(); +``` + +`trees` will be following: + +```json +[{ + "id": 1, + "name": "a1", + "children": [{ + "id": 2, + "name": "a11", + "children": [{ + "id": 4, + "name": "a111" + }, { + "id": 5, + "name": "a112" + }] + }, { + "id": 3, + "name": "a12" + }] +}] +``` + +There are other special methods to work with tree entities thought `TreeRepository`: + +* `findTrees` - Returns all trees in the database with all their children, children of children, etc. + +```typescript +const treeCategories = await repository.findTrees(); +// returns root categories with sub categories inside +``` + +* `findRoots` - Roots are entities that have no ancestors. Finds them all. +Does not load children leafs. + +```typescript +const rootCategories = await repository.findRoots(); +// returns root categories without sub categories inside +``` + +* `findDescendants` - Gets all children (descendants) of the given entity. Returns them all in a flat array. + +```typescript +const childrens = await repository.findDescendants(parentCategory); +// returns all direct subcategories (without its nested categories) of a parentCategory +``` + +* `findDescendantsTree` - Gets all children (descendants) of the given entity. Returns them in a tree - nested into each other. + +```typescript +const childrensTree = await repository.findDescendantsTree(parentCategory); +// returns all direct subcategories (with its nested categories) of a parentCategory +``` + +* `createDescendantsQueryBuilder` - Creates a query builder used to get descendants of the entities in a tree. + +```typescript +const childrens = await repository + .createDescendantsQueryBuilder("category", "categoryClosure", parentCategory) + .andWhere("category.type = 'secondary'") + .getMany(); +``` + +* `countDescendants` - Gets number of descendants of the entity. + +```typescript +const childrenCount = await repository.countDescendants(parentCategory); +``` + +* `findAncestors` - Gets all parent (ancestors) of the given entity. Returns them all in a flat array. + +```typescript +const parents = await repository.findAncestors(childCategory); +// returns all direct childCategory's parent categories (without "parent of parents") +``` + +* `findAncestorsTree` - Gets all parent (ancestors) of the given entity. Returns them in a tree - nested into each other. + +```typescript +const parentsTree = await repository.findAncestorsTree(childCategory); +// returns all direct childCategory's parent categories (with "parent of parents") +``` + +* `createAncestorsQueryBuilder` - Creates a query builder used to get ancestors of the entities in a tree. + +```typescript +const parents = await repository + .createAncestorsQueryBuilder("category", "categoryClosure", childCategory) + .andWhere("category.type = 'secondary'") + .getMany(); +``` + +* `countAncestors` - Gets the number of ancestors of the entity. + +```typescript +const parentsCount = await repository.countAncestors(childCategory); \ No newline at end of file diff --git a/docs/working-with-repository.md b/docs/working-with-repository.md index 869ad91c0d..23b752d4c0 100644 --- a/docs/working-with-repository.md +++ b/docs/working-with-repository.md @@ -19,6 +19,6 @@ await userRepository.save(user); There are 3 types of repositories: * `Repository` - Regular repository for any entity * `TreeRepository` - Repository, extensions of `Repository` used for tree-entities -(like entities marked with `@ClosureEntity` decorator). +(like entities marked with `@Tree` decorator). Has special methods to work with tree structures. * `MongoRepository` - Repository with special functions used only with MongoDB. diff --git a/extra/typeorm-class-transformer-shim.js b/extra/typeorm-class-transformer-shim.js index 70aed6094a..e86e0bbf3c 100644 --- a/extra/typeorm-class-transformer-shim.js +++ b/extra/typeorm-class-transformer-shim.js @@ -40,12 +40,6 @@ exports.Column = Column; } exports.CreateDateColumn = CreateDateColumn; -/* export */ function DiscriminatorColumn(discriminatorOptions) { - return function (object, propertyName) { - }; -} -exports.DiscriminatorColumn = DiscriminatorColumn; - /* export */ function ObjectIdColumn(typeOrOptions, options) { return function (object, propertyName) { @@ -185,35 +179,11 @@ exports.RelationId = RelationId; // entities -/* export */ function AbstractEntity() { - return function (object) { - }; -} -exports.AbstractEntity = AbstractEntity; - -/* export */ function ClassEntityChild(tableName, options) { - return function (object) { - }; -} -exports.ClassEntityChild = ClassEntityChild; - -/* export */ function ClosureEntity(name, options) { +/* export */ function ChildEntity(tableName, options) { return function (object) { }; } -exports.ClosureEntity = ClosureEntity; - -/* export */ function EmbeddableEntity() { - return function (object) { - }; -} -exports.EmbeddableEntity = EmbeddableEntity; - -/* export */ function SingleEntityChild() { - return function (object) { - }; -} -exports.SingleEntityChild = SingleEntityChild; +exports.ChildEntity = ChildEntity; /* export */ function Entity(name, options) { return function (object) { @@ -227,52 +197,27 @@ exports.Entity = Entity; } exports.TableInheritance = TableInheritance; -// tables (deprecated) - -/* export */ function AbstractTable() { - return function (object) { - }; -} -exports.AbstractTable = AbstractTable; - -/* export */ function ClassTableChild(tableName, options) { - return function (object) { - }; -} -exports.ClassTableChild = ClassTableChild; - -/* export */ function ClosureTable(name, options) { - return function (object) { - }; -} -exports.ClosureTable = ClosureTable; - -/* export */ function EmbeddableTable() { - return function (object) { - }; -} -exports.EmbeddableTable = EmbeddableTable; +// tree -/* export */ function SingleTableChild() { +/* export */ function Tree(name, options) { return function (object) { }; } -exports.SingleTableChild = SingleTableChild; +exports.Tree = Tree; -/* export */ function Table(name, options) { - return function (object) { +/* export */ function TreeChildren(options) { + return function (object, propertyName) { + class_transformer_1.Type(typeFunction)(object, propertyName); }; } -exports.Table = Table; - -// tree +exports.TreeChildren = TreeChildren; -/* export */ function TreeChildren(options) { +/* export */ function TreeChildrenCount(options) { return function (object, propertyName) { class_transformer_1.Type(typeFunction)(object, propertyName); }; } -exports.TreeChildren = TreeChildren; +exports.TreeChildrenCount = TreeChildrenCount; /* export */ function TreeLevelColumn() { return function (object, propertyName) { @@ -289,11 +234,11 @@ exports.TreeParent = TreeParent; // other -/* export */ function DiscriminatorValue(options) { +/* export */ function Generated(options) { return function (object, propertyName) { }; } -exports.DiscriminatorValue = DiscriminatorValue; +exports.Generated = Generated; /* export */ function Index() { return function (object, propertyName) { diff --git a/extra/typeorm-model-shim.js b/extra/typeorm-model-shim.js index 0b2f4c53b4..c869dabc19 100644 --- a/extra/typeorm-model-shim.js +++ b/extra/typeorm-model-shim.js @@ -35,12 +35,6 @@ exports.Column = Column; } exports.CreateDateColumn = CreateDateColumn; -/* export */ function DiscriminatorColumn(discriminatorOptions) { - return function (object, propertyName) { - }; -} -exports.DiscriminatorColumn = DiscriminatorColumn; - /* export */ function ObjectIdColumn(columnOptions) { return function (object, propertyName) { }; @@ -73,35 +67,11 @@ exports.VersionColumn = VersionColumn; // entities -/* export */ function AbstractEntity() { - return function (object) { - }; -} -exports.AbstractEntity = AbstractEntity; - -/* export */ function ClassEntityChild(tableName, options) { - return function (object) { - }; -} -exports.ClassEntityChild = ClassEntityChild; - -/* export */ function ClosureEntity(name, options) { +/* export */ function ChildEntity(tableName, options) { return function (object) { }; } -exports.ClosureEntity = ClosureEntity; - -/* export */ function EmbeddableEntity() { - return function (object) { - }; -} -exports.EmbeddableEntity = EmbeddableEntity; - -/* export */ function SingleEntityChild() { - return function (object) { - }; -} -exports.SingleEntityChild = SingleEntityChild; +exports.ChildEntity = ChildEntity; /* export */ function Entity(name, options) { return function (object) { @@ -215,51 +185,25 @@ exports.RelationCount = RelationCount; } exports.RelationId = RelationId; -// tables (deprecated) - -/* export */ function AbstractTable() { - return function (object) { - }; -} -exports.AbstractTable = AbstractTable; - -/* export */ function ClassTableChild(tableName, options) { - return function (object) { - }; -} -exports.ClassTableChild = ClassTableChild; - -/* export */ function ClosureTable(name, options) { - return function (object) { - }; -} -exports.ClosureTable = ClosureTable; - -/* export */ function EmbeddableTable() { - return function (object) { - }; -} -exports.EmbeddableTable = EmbeddableTable; +// tree -/* export */ function SingleTableChild() { +/* export */ function Tree(name, options) { return function (object) { }; } -exports.SingleTableChild = SingleTableChild; +exports.Tree = Tree; -/* export */ function Table(name, options) { - return function (object) { +/* export */ function TreeChildren(options) { + return function (object, propertyName) { }; } -exports.Table = Table; - -// tree +exports.TreeChildren = TreeChildren; -/* export */ function TreeChildren(options) { +/* export */ function TreeChildrenCount(options) { return function (object, propertyName) { }; } -exports.TreeChildren = TreeChildren; +exports.TreeChildrenCount = TreeChildrenCount; /* export */ function TreeLevelColumn() { return function (object, propertyName) { @@ -275,11 +219,11 @@ exports.TreeParent = TreeParent; // other -/* export */ function DiscriminatorValue(options) { +/* export */ function Generated(options) { return function (object, propertyName) { }; } -exports.DiscriminatorValue = DiscriminatorValue; +exports.Generated = Generated; /* export */ function Index(options) { return function (object, propertyName) { diff --git a/sample/sample22-closure-table/entity/Category.ts b/sample/sample22-closure-table/entity/Category.ts index dbc0ee1b8d..ce3e41622f 100644 --- a/sample/sample22-closure-table/entity/Category.ts +++ b/sample/sample22-closure-table/entity/Category.ts @@ -1,10 +1,12 @@ import {Column, PrimaryGeneratedColumn} from "../../../src/index"; import {TreeLevelColumn} from "../../../src/decorator/tree/TreeLevelColumn"; -import {ClosureEntity} from "../../../src/decorator/entity/ClosureEntity"; import {TreeParent} from "../../../src/decorator/tree/TreeParent"; import {TreeChildren} from "../../../src/decorator/tree/TreeChildren"; +import {Tree} from "../../../src/decorator/tree/Tree"; +import {Entity} from "../../../src/decorator/entity/Entity"; -@ClosureEntity("sample22_category") +@Entity("sample22_category") +@Tree("closure-table") export class Category { @PrimaryGeneratedColumn() @@ -23,7 +25,7 @@ export class Category { level: number; // todo: - // @RelationsCountColumn() + // @TreeChildrenCount() // categoriesCount: number; } \ No newline at end of file diff --git a/src/connection/Connection.ts b/src/connection/Connection.ts index aa07aa357e..ef1bceaa2c 100644 --- a/src/connection/Connection.ts +++ b/src/connection/Connection.ts @@ -315,7 +315,7 @@ export class Connection { /** * Gets tree repository for the given entity class or name. - * Only tree-type entities can have a TreeRepository, like ones decorated with @ClosureEntity decorator. + * Only tree-type entities can have a TreeRepository, like ones decorated with @Tree decorator. */ getTreeRepository(target: ObjectType|string): TreeRepository { return this.manager.getTreeRepository(target); diff --git a/src/decorator/entity/ClosureEntity.ts b/src/decorator/entity/ClosureEntity.ts deleted file mode 100644 index ede415b771..0000000000 --- a/src/decorator/entity/ClosureEntity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {getMetadataArgsStorage} from "../../index"; -import {TableMetadataArgs} from "../../metadata-args/TableMetadataArgs"; -import {EntityOptions} from "../options/EntityOptions"; - -/** - * Used on a entities that stores its children in a tree using closure design pattern. - */ -export function ClosureEntity(name?: string, options?: EntityOptions) { - return function (target: Function) { - const args: TableMetadataArgs = { - target: target, - name: name, - type: "closure", - orderBy: options && options.orderBy ? options.orderBy : undefined, - synchronize: options && options.synchronize === false ? false : true - }; - getMetadataArgsStorage().tables.push(args); - }; -} diff --git a/src/decorator/tree/Tree.ts b/src/decorator/tree/Tree.ts new file mode 100644 index 0000000000..6132b31fb4 --- /dev/null +++ b/src/decorator/tree/Tree.ts @@ -0,0 +1,16 @@ +import {getMetadataArgsStorage} from "../../index"; +import {TreeMetadataArgs} from "../../metadata-args/TreeMetadataArgs"; +import {TreeType} from "../../metadata/types/TreeTypes"; + +/** + * Marks entity to work like a tree. + */ +export function Tree(type: TreeType): Function { + return function (target: Function) { + const args: TreeMetadataArgs = { + target: target, + type: type + }; + getMetadataArgsStorage().trees.push(args); + }; +} diff --git a/src/driver/Driver.ts b/src/driver/Driver.ts index 671d864c4a..0b53f7b820 100644 --- a/src/driver/Driver.ts +++ b/src/driver/Driver.ts @@ -92,6 +92,8 @@ export interface Driver { /** * Escapes a table name, column name or an alias. + * + * todo: probably escape should be able to handle dots in the names and automatically escape them */ escape(name: string): string; diff --git a/src/error/RepositoryNotTreeError.ts b/src/error/RepositoryNotTreeError.ts index 643d6fc009..e9eba5ff5a 100644 --- a/src/error/RepositoryNotTreeError.ts +++ b/src/error/RepositoryNotTreeError.ts @@ -7,7 +7,7 @@ export class RepositoryNotTreeError extends Error { constructor(entityClass: Function|string) { super(); const targetName = typeof entityClass === "function" && ( entityClass).name ? ( entityClass).name : entityClass; - this.message = `Repository of the "${targetName}" class is not a TreeRepository. Try to use @ClosureEntity decorator instead of @Entity.`; + this.message = `Repository of the "${targetName}" class is not a TreeRepository. Try to apply @Tree decorator on your entity.`; this.stack = new Error().stack; } diff --git a/src/index.ts b/src/index.ts index 2e8c94e540..913b025579 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,7 +55,6 @@ export * from "./decorator/relations/OneToOne"; export * from "./decorator/relations/RelationCount"; export * from "./decorator/relations/RelationId"; export * from "./decorator/entity/Entity"; -export * from "./decorator/entity/ClosureEntity"; export * from "./decorator/entity/ChildEntity"; export * from "./decorator/entity/TableInheritance"; export * from "./decorator/transaction/Transaction"; @@ -64,6 +63,7 @@ export * from "./decorator/transaction/TransactionRepository"; export * from "./decorator/tree/TreeLevelColumn"; export * from "./decorator/tree/TreeParent"; export * from "./decorator/tree/TreeChildren"; +export * from "./decorator/tree/Tree"; export * from "./decorator/Index"; export * from "./decorator/Generated"; export * from "./decorator/EntityRepository"; diff --git a/src/metadata-args/MetadataArgsStorage.ts b/src/metadata-args/MetadataArgsStorage.ts index 1c05a0c082..5009b8a691 100644 --- a/src/metadata-args/MetadataArgsStorage.ts +++ b/src/metadata-args/MetadataArgsStorage.ts @@ -17,6 +17,7 @@ import {TransactionEntityMetadataArgs} from "./TransactionEntityMetadataArgs"; import {TransactionRepositoryMetadataArgs} from "./TransactionRepositoryMetadataArgs"; import {MetadataUtils} from "../metadata-builder/MetadataUtils"; import {GeneratedMetadataArgs} from "./GeneratedMetadataArgs"; +import {TreeMetadataArgs} from "./TreeMetadataArgs"; /** * Storage all metadatas args of all available types: tables, columns, subscribers, relations, etc. @@ -30,6 +31,7 @@ export class MetadataArgsStorage { // ------------------------------------------------------------------------- readonly tables: TableMetadataArgs[] = []; + readonly trees: TreeMetadataArgs[] = []; readonly entityRepositories: EntityRepositoryMetadataArgs[] = []; readonly transactionEntityManagers: TransactionEntityMetadataArgs[] = []; readonly transactionRepositories: TransactionRepositoryMetadataArgs[] = []; @@ -72,6 +74,12 @@ export class MetadataArgsStorage { }); } + findTree(target: (Function|string)|(Function|string)[]): TreeMetadataArgs|undefined { + return this.trees.find(tree => { + return (target instanceof Array ? target.indexOf(tree.target) !== -1 : tree.target === target); + }); + } + filterRelations(target: Function|string): RelationMetadataArgs[]; filterRelations(target: (Function|string)[]): RelationMetadataArgs[]; filterRelations(target: (Function|string)|(Function|string)[]): RelationMetadataArgs[] { diff --git a/src/metadata-args/TreeMetadataArgs.ts b/src/metadata-args/TreeMetadataArgs.ts new file mode 100644 index 0000000000..021cd48bda --- /dev/null +++ b/src/metadata-args/TreeMetadataArgs.ts @@ -0,0 +1,18 @@ +import {TreeType} from "../metadata/types/TreeTypes"; + +/** + * Stores metadata collected for Tree entities. + */ +export interface TreeMetadataArgs { + + /** + * Entity to which tree is applied. + */ + target: Function|string; + + /** + * Tree type. + */ + type: TreeType; + +} diff --git a/src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts b/src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts index 307b4c8e6f..66064556f7 100644 --- a/src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts +++ b/src/metadata-builder/ClosureJunctionEntityMetadataBuilder.ts @@ -42,10 +42,12 @@ export class ClosureJunctionEntityMetadataBuilder { entityMetadata.ownColumns.push(new ColumnMetadata({ connection: this.connection, entityMetadata: entityMetadata, + closureType: "ancestor", + referencedColumn: primaryColumn, args: { target: "", mode: "virtual", - propertyName: "ancestor", // todo: naming strategy + propertyName: primaryColumn.propertyName + "_ancestor", // todo: naming strategy options: { length: primaryColumn.length, type: primaryColumn.type, @@ -55,10 +57,12 @@ export class ClosureJunctionEntityMetadataBuilder { entityMetadata.ownColumns.push(new ColumnMetadata({ connection: this.connection, entityMetadata: entityMetadata, + closureType: "descendant", + referencedColumn: primaryColumn, args: { target: "", mode: "virtual", - propertyName: "descendant", + propertyName: primaryColumn.propertyName + "_descendant", options: { length: primaryColumn.length, type: primaryColumn.type, diff --git a/src/metadata-builder/EntityMetadataBuilder.ts b/src/metadata-builder/EntityMetadataBuilder.ts index ad2f54122c..1941557e98 100644 --- a/src/metadata-builder/EntityMetadataBuilder.ts +++ b/src/metadata-builder/EntityMetadataBuilder.ts @@ -136,7 +136,7 @@ export class EntityMetadataBuilder { // generate closure junction tables for all closure tables entityMetadatas - .filter(metadata => metadata.isClosure) + .filter(metadata => metadata.treeType === "closure-table") .forEach(entityMetadata => { const closureJunctionEntityMetadata = this.closureJunctionEntityMetadataBuilder.build(entityMetadata); entityMetadata.closureJunctionTable = closureJunctionEntityMetadata; @@ -211,6 +211,7 @@ export class EntityMetadataBuilder { : [tableArgs.target]; // todo: implement later here inheritance for string-targets const tableInheritance = this.metadataArgsStorage.findInheritanceType(tableArgs.target); + const tableTree = this.metadataArgsStorage.findTree(tableArgs.target); // if single table inheritance used, we need to copy all children columns in to parent table let singleTableChildrenTargets: any[]; @@ -227,6 +228,7 @@ export class EntityMetadataBuilder { connection: this.connection, args: tableArgs, inheritanceTree: inheritanceTree, + tableTree: tableTree, inheritancePattern: tableInheritance ? tableInheritance.pattern : undefined }); } @@ -280,7 +282,7 @@ export class EntityMetadataBuilder { mode: "virtual", propertyName: discriminatorColumnName, options: entityInheritance.column || { - name: "type", + name: discriminatorColumnName, type: "varchar", nullable: false } @@ -303,6 +305,60 @@ export class EntityMetadataBuilder { } } + // check if tree is used then we need to add extra columns for specific tree types + if (entityMetadata.treeType === "materialized-path") { + entityMetadata.ownColumns.push(new ColumnMetadata({ + connection: this.connection, + entityMetadata: entityMetadata, + materializedPath: true, + args: { + target: entityMetadata.target, + mode: "virtual", + propertyName: "mpath", + options: /*tree.column || */ { + name: "mpath", + type: "varchar", + nullable: false, + default: "" + } + } + })); + + } else if (entityMetadata.treeType === "nested-set") { + entityMetadata.ownColumns.push(new ColumnMetadata({ + connection: this.connection, + entityMetadata: entityMetadata, + nestedSetLeft: true, + args: { + target: entityMetadata.target, + mode: "virtual", + propertyName: "nsleft", + options: /*tree.column || */ { + name: "nsleft", + type: "integer", + nullable: false, + default: 1 + } + } + })); + entityMetadata.ownColumns.push(new ColumnMetadata({ + connection: this.connection, + entityMetadata: entityMetadata, + nestedSetRight: true, + args: { + target: entityMetadata.target, + mode: "virtual", + propertyName: "nsright", + options: /*tree.column || */ { + name: "nsright", + type: "integer", + nullable: false, + default: 2 + } + } + })); + } + entityMetadata.ownRelations = this.metadataArgsStorage.filterRelations(entityMetadata.inheritanceTree).map(args => { // for single table children we reuse relations created for their parents @@ -403,6 +459,8 @@ export class EntityMetadataBuilder { entityMetadata.indices = entityMetadata.embeddeds.reduce((columns, embedded) => columns.concat(embedded.indicesFromTree), entityMetadata.ownIndices); entityMetadata.primaryColumns = entityMetadata.columns.filter(column => column.isPrimary); entityMetadata.nonVirtualColumns = entityMetadata.columns.filter(column => !column.isVirtual); + entityMetadata.ancestorColumns = entityMetadata.columns.filter(column => column.closureType === "ancestor"); + entityMetadata.descendantColumns = entityMetadata.columns.filter(column => column.closureType === "descendant"); entityMetadata.hasMultiplePrimaryKeys = entityMetadata.primaryColumns.length > 1; entityMetadata.generatedColumns = entityMetadata.columns.filter(column => column.isGenerated || column.isObjectId); entityMetadata.hasUUIDGeneratedColumns = entityMetadata.columns.filter(column => column.isGenerated || column.generationStrategy === "uuid").length > 0; @@ -411,6 +469,9 @@ export class EntityMetadataBuilder { entityMetadata.versionColumn = entityMetadata.columns.find(column => column.isVersion); entityMetadata.discriminatorColumn = entityMetadata.columns.find(column => column.isDiscriminator); entityMetadata.treeLevelColumn = entityMetadata.columns.find(column => column.isTreeLevel); + entityMetadata.nestedSetLeftColumn = entityMetadata.columns.find(column => column.isNestedSetLeft); + entityMetadata.nestedSetRightColumn = entityMetadata.columns.find(column => column.isNestedSetRight); + entityMetadata.materializedPathColumn = entityMetadata.columns.find(column => column.isMaterializedPath); entityMetadata.objectIdColumn = entityMetadata.columns.find(column => column.isObjectId); entityMetadata.foreignKeys.forEach(foreignKey => foreignKey.build(this.connection.namingStrategy)); entityMetadata.propertiesMap = entityMetadata.createPropertiesMap(); diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index eb96cd19ec..5437881209 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -215,6 +215,30 @@ export class ColumnMetadata { */ transformer?: ValueTransformer; + /** + * Column type in the case if this column is in the closure table. + * Column can be ancestor or descendant in the closure tables. + */ + closureType?: "ancestor"|"descendant"; + + /** + * Indicates if this column is nested set's left column. + * Used only in tree entities with nested-set type. + */ + isNestedSetLeft: boolean = false; + + /** + * Indicates if this column is nested set's right column. + * Used only in tree entities with nested-set type. + */ + isNestedSetRight: boolean = false; + + /** + * Indicates if this column is materialized path's path column. + * Used only in tree entities with materialized path type. + */ + isMaterializedPath: boolean = false; + // --------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------- @@ -224,7 +248,11 @@ export class ColumnMetadata { entityMetadata: EntityMetadata, embeddedMetadata?: EmbeddedMetadata, referencedColumn?: ColumnMetadata, - args: ColumnMetadataArgs + args: ColumnMetadataArgs, + closureType?: "ancestor"|"descendant", + nestedSetLeft?: boolean, + nestedSetRight?: boolean, + materializedPath?: boolean, }) { this.entityMetadata = options.entityMetadata; this.embeddedMetadata = options.embeddedMetadata!; @@ -303,6 +331,14 @@ export class ColumnMetadata { } if (this.isVersion) this.type = options.connection.driver.mappedDataTypes.version; + if (options.closureType) + this.closureType = options.closureType; + if (options.nestedSetLeft) + this.isNestedSetLeft = options.nestedSetLeft; + if (options.nestedSetRight) + this.isNestedSetRight = options.nestedSetRight; + if (options.materializedPath) + this.isMaterializedPath = options.materializedPath; } // --------------------------------------------------------------------- diff --git a/src/metadata/EntityMetadata.ts b/src/metadata/EntityMetadata.ts index 7389b11bae..f0750ede41 100644 --- a/src/metadata/EntityMetadata.ts +++ b/src/metadata/EntityMetadata.ts @@ -17,6 +17,8 @@ import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver"; import {PostgresConnectionOptions} from "../driver/postgres/PostgresConnectionOptions"; import {SqlServerConnectionOptions} from "../driver/sqlserver/SqlServerConnectionOptions"; import {CannotCreateEntityIdMapError} from "../error/CannotCreateEntityIdMapError"; +import {TreeType} from "./types/TreeTypes"; +import {TreeMetadataArgs} from "../metadata-args/TreeMetadataArgs"; /** * Contains all entity metadata. @@ -167,10 +169,9 @@ export class EntityMetadata { isJunction: boolean = false; /** - * Checks if this table is a closure table. - * Closure table is one of the tree-specific tables that supports closure database pattern. + * Indicates if this entity is a tree, what type of tree it is. */ - isClosure: boolean = false; + treeType?: TreeType; /** * Checks if this table is a junction table of the closure table. @@ -204,6 +205,16 @@ export class EntityMetadata { */ columns: ColumnMetadata[] = []; + /** + * Ancestor columns used only in closure junction tables. + */ + ancestorColumns: ColumnMetadata[] = []; + + /** + * Descendant columns used only in closure junction tables. + */ + descendantColumns: ColumnMetadata[] = []; + /** * All columns except for virtual columns. */ @@ -256,6 +267,24 @@ export class EntityMetadata { */ treeLevelColumn?: ColumnMetadata; + /** + * Nested set's left value column. + * Used only in tree entities with nested set pattern applied. + */ + nestedSetLeftColumn?: ColumnMetadata; + + /** + * Nested set's right value column. + * Used only in tree entities with nested set pattern applied. + */ + nestedSetRightColumn?: ColumnMetadata; + + /** + * Materialized path column. + * Used only in tree entities with materialized path pattern applied. + */ + materializedPathColumn?: ColumnMetadata; + /** * Gets the primary columns. */ @@ -423,12 +452,14 @@ export class EntityMetadata { connection: Connection, inheritanceTree?: Function[], inheritancePattern?: "STI"/*|"CTI"*/, + tableTree?: TreeMetadataArgs, parentClosureEntityMetadata?: EntityMetadata, args: TableMetadataArgs }) { this.connection = options.connection; this.inheritanceTree = options.inheritanceTree || []; this.inheritancePattern = options.inheritancePattern; + this.treeType = options.tableTree ? options.tableTree.type : undefined; this.parentClosureEntityMetadata = options.parentClosureEntityMetadata!; this.tableMetadataArgs = options.args; this.target = this.tableMetadataArgs.target; @@ -701,7 +732,6 @@ export class EntityMetadata { this.isJunction = this.tableMetadataArgs.type === "closure-junction" || this.tableMetadataArgs.type === "junction"; this.isClosureJunction = this.tableMetadataArgs.type === "closure-junction"; - this.isClosure = this.tableMetadataArgs.type === "closure"; } /** diff --git a/src/metadata/EntityMetadataUtils.js b/src/metadata/EntityMetadataUtils.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/metadata/types/TreeTypes.ts b/src/metadata/types/TreeTypes.ts new file mode 100644 index 0000000000..7a9c0471a7 --- /dev/null +++ b/src/metadata/types/TreeTypes.ts @@ -0,0 +1,4 @@ +/** + * Tree type. + */ +export type TreeType = "adjacency-list"|"closure-table"|"nested-set"|"materialized-path"; diff --git a/src/persistence/SubjectExecutor.ts b/src/persistence/SubjectExecutor.ts index c80f767bbf..46b3e1a5bf 100644 --- a/src/persistence/SubjectExecutor.ts +++ b/src/persistence/SubjectExecutor.ts @@ -12,6 +12,10 @@ import {ObjectLiteral} from "../common/ObjectLiteral"; import {SaveOptions} from "../repository/SaveOptions"; import {RemoveOptions} from "../repository/RemoveOptions"; import {BroadcasterResult} from "../subscriber/BroadcasterResult"; +import {OracleDriver} from "../driver/oracle/OracleDriver"; +import {NestedSetSubjectExecutor} from "./tree/NestedSetSubjectExecutor"; +import {ClosureSubjectExecutor} from "./tree/ClosureSubjectExecutor"; +import {MaterializedPathSubjectExecutor} from "./tree/MaterializedPathSubjectExecutor"; /** * Executes all database operations (inserts, updated, deletes) that must be executed @@ -221,7 +225,9 @@ export class SubjectExecutor { }); } else { subjects.forEach(subject => { - if (subject.changeMaps.length === 0) { + if (subject.changeMaps.length === 0 || + subject.metadata.treeType || + this.queryRunner.connection.driver instanceof OracleDriver) { singleInsertSubjects.push(subject); } else { @@ -268,22 +274,34 @@ export class SubjectExecutor { // insert subjects which must be inserted in separate requests (all default values) if (singleInsertSubjects.length > 0) { - await Promise.all(singleInsertSubjects.map(subject => { - const updatedEntity = {}; // important to have because query builder sets inserted values into it - return this.queryRunner + await Promise.all(singleInsertSubjects.map(async subject => { + subject.insertedValueSet = subject.createValueSetAndPopChangeMap(); // important to have because query builder sets inserted values into it + + // for nested set we execute additional queries + if (subject.metadata.treeType === "nested-set") + await new NestedSetSubjectExecutor(this.queryRunner).insert(subject); + + await this.queryRunner .manager .createQueryBuilder() .insert() .into(subject.metadata.target) - .values(updatedEntity) + .values(subject.insertedValueSet) .updateEntity(this.options && this.options.reload === false ? false : true) .callListeners(false) .execute() .then(insertResult => { subject.identifier = insertResult.identifiers[0]; subject.generatedMap = insertResult.generatedMaps[0]; - subject.insertedValueSet = updatedEntity; }); + + // for tree tables we execute additional queries + if (subject.metadata.treeType === "closure-table") { + await new ClosureSubjectExecutor(this.queryRunner).insert(subject); + + } else if (subject.metadata.treeType === "materialized-path") { + await new MaterializedPathSubjectExecutor(this.queryRunner).insert(subject); + } })); } } diff --git a/src/persistence/tree/ClosureSubjectExecutor.ts b/src/persistence/tree/ClosureSubjectExecutor.ts new file mode 100644 index 0000000000..8cf8bdbc38 --- /dev/null +++ b/src/persistence/tree/ClosureSubjectExecutor.ts @@ -0,0 +1,76 @@ +import {Subject} from "../Subject"; +import {QueryRunner} from "../../query-runner/QueryRunner"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; + +/** + * Executes subject operations for closure entities. + */ +export class ClosureSubjectExecutor { + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(protected queryRunner: QueryRunner) { + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Executes operations when subject is being inserted. + */ + async insert(subject: Subject): Promise { + + // create values to be inserted into the closure junction + const closureJunctionInsertMap: ObjectLiteral = {}; + subject.metadata.closureJunctionTable.ancestorColumns.forEach(column => { + closureJunctionInsertMap[column.databaseName] = subject.identifier; + }); + subject.metadata.closureJunctionTable.descendantColumns.forEach(column => { + closureJunctionInsertMap[column.databaseName] = subject.identifier; + }); + + // insert values into the closure junction table + await this.queryRunner + .manager + .createQueryBuilder() + .insert() + .into(subject.metadata.closureJunctionTable.tablePath) + .values(closureJunctionInsertMap) + .updateEntity(false) + .callListeners(false) + .execute(); + + const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!); + if (parent) { + const escape = (alias: string) => this.queryRunner.connection.driver.escape(alias); + const tableName = escape(subject.metadata.closureJunctionTable.tablePath); // todo: make sure to properly escape table path, not just a table name + const ancestorColumnNames = subject.metadata.closureJunctionTable.ancestorColumns.map(column => { + return escape(column.databaseName); + }); + const descendantColumnNames = subject.metadata.closureJunctionTable.descendantColumns.map(column => { + return escape(column.databaseName); + }); + const firstQueryParameters: any[] = []; + const childEntityIdValues = subject.metadata.primaryColumns.map(column => column.getEntityValue(subject.insertedValueSet!)); + const childEntityIds1 = subject.metadata.primaryColumns.map((column, index) => { + firstQueryParameters.push(childEntityIdValues[index]); + return this.queryRunner.connection.driver.createParameter("child_entity_" + column.databaseName, firstQueryParameters.length - 1); + }); + const whereCondition = subject.metadata.primaryColumns.map(column => { + const columnName = escape(column.databaseName + "_descendant"); + firstQueryParameters.push(column.getEntityValue(parent)); + const parameterName = this.queryRunner.connection.driver.createParameter("parent_entity_" + column.databaseName, firstQueryParameters.length - 1); + return columnName + " = " + parameterName; + }).join(", "); + await this.queryRunner.query( + `INSERT INTO ${tableName} (${[...ancestorColumnNames, ...descendantColumnNames].join(", ")}) ` + + `SELECT ${ancestorColumnNames.join(", ")}, ${childEntityIds1.join(", ")} FROM ${tableName} WHERE ${whereCondition}`, + firstQueryParameters + ); + } + } + +} \ No newline at end of file diff --git a/src/persistence/tree/MaterializedPathSubjectExecutor.ts b/src/persistence/tree/MaterializedPathSubjectExecutor.ts new file mode 100644 index 0000000000..42ad07d85e --- /dev/null +++ b/src/persistence/tree/MaterializedPathSubjectExecutor.ts @@ -0,0 +1,52 @@ +import {Subject} from "../Subject"; +import {QueryRunner} from "../../query-runner/QueryRunner"; + +/** + * Executes subject operations for materialized-path tree entities. + */ +export class MaterializedPathSubjectExecutor { + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(protected queryRunner: QueryRunner) { + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Executes operations when subject is being inserted. + */ + async insert(subject: Subject): Promise { + const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!); + const parentId = subject.metadata.getEntityIdMap(parent); + + let parentPath: string = ""; + if (parentId) { + parentPath = await this.queryRunner.manager + .createQueryBuilder() + .select(subject.metadata.targetName + "." + subject.metadata.materializedPathColumn!.propertyPath, "path") + .from(subject.metadata.target, subject.metadata.targetName) + .whereInIds(parentId) + .getRawOne() + .then(result => result ? result["path"] : undefined); + } + + const insertedEntityId = subject.metadata.treeParentRelation!.joinColumns.map(joinColumn => { + return joinColumn.referencedColumn!.getEntityValue(subject.insertedValueSet!); + }).join("_"); + + await this.queryRunner.manager + .createQueryBuilder() + .update(subject.metadata.target) + .set({ + [subject.metadata.materializedPathColumn!.propertyPath]: parentPath + insertedEntityId + "." + }) + .where(subject.identifier!) + .execute(); + } + +} \ No newline at end of file diff --git a/src/persistence/tree/NestedSetSubjectExecutor.ts b/src/persistence/tree/NestedSetSubjectExecutor.ts new file mode 100644 index 0000000000..71f0c38b9e --- /dev/null +++ b/src/persistence/tree/NestedSetSubjectExecutor.ts @@ -0,0 +1,63 @@ +import {Subject} from "../Subject"; +import {QueryRunner} from "../../query-runner/QueryRunner"; +import {OrmUtils} from "../../util/OrmUtils"; + +/** + * Executes subject operations for nested set tree entities. + */ +export class NestedSetSubjectExecutor { + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(protected queryRunner: QueryRunner) { + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Executes operations when subject is being inserted. + */ + async insert(subject: Subject): Promise { + const escape = (alias: string) => this.queryRunner.connection.driver.escape(alias); + const tableName = escape(subject.metadata.tablePath); + const leftColumnName = escape(subject.metadata.nestedSetLeftColumn!.databaseName); + const rightColumnName = escape(subject.metadata.nestedSetRightColumn!.databaseName); + const parent = subject.metadata.treeParentRelation!.getEntityValue(subject.entity!); + const parentId = subject.metadata.getEntityIdMap(parent); + + let parentNsRight: number|undefined = undefined; + if (parentId) { + parentNsRight = await this.queryRunner.manager + .createQueryBuilder() + .select(subject.metadata.targetName + "." + subject.metadata.nestedSetRightColumn!.propertyPath, "right") + .from(subject.metadata.target, subject.metadata.targetName) + .whereInIds(parentId) + .getRawOne() + .then(result => result ? result["right"] : undefined); + } + + if (parentNsRight !== undefined) { + await this.queryRunner.query(`UPDATE ${tableName} SET ` + + `${leftColumnName} = CASE WHEN ${leftColumnName} > ${parentNsRight} THEN ${leftColumnName} + 2 ELSE ${leftColumnName} END,` + + `${rightColumnName} = ${rightColumnName} + 2 ` + + `WHERE ${rightColumnName} >= ${parentNsRight}`); + + OrmUtils.mergeDeep( + subject.insertedValueSet, + subject.metadata.nestedSetLeftColumn!.createValueMap(parentNsRight), + subject.metadata.nestedSetRightColumn!.createValueMap(parentNsRight + 1), + ); + } else { + OrmUtils.mergeDeep( + subject.insertedValueSet, + subject.metadata.nestedSetLeftColumn!.createValueMap(1), + subject.metadata.nestedSetRightColumn!.createValueMap(2), + ); + } + } + +} \ No newline at end of file diff --git a/src/query-builder/InsertQueryBuilder.ts b/src/query-builder/InsertQueryBuilder.ts index e957e097f4..e6e790b47f 100644 --- a/src/query-builder/InsertQueryBuilder.ts +++ b/src/query-builder/InsertQueryBuilder.ts @@ -352,6 +352,18 @@ export class InsertQueryBuilder extends QueryBuilder { if (column.isVersion) { expression += "1"; + // } else if (column.isNestedSetLeft) { + // const tableName = this.connection.driver.escape(column.entityMetadata.tablePath); + // const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName); + // const subQuery = `(SELECT c.max + 1 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`; + // expression += subQuery; + // + // } else if (column.isNestedSetRight) { + // const tableName = this.connection.driver.escape(column.entityMetadata.tablePath); + // const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName); + // const subQuery = `(SELECT c.max + 2 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`; + // expression += subQuery; + } else if (column.isDiscriminator) { this.expressionMap.nativeParameters["discriminator_value"] = this.expressionMap.mainAlias!.metadata.discriminatorValue; expression += this.connection.driver.createParameter("discriminator_value", parametersCount); diff --git a/src/repository/RepositoryFactory.ts b/src/repository/RepositoryFactory.ts index 80bf74a489..45568bfce2 100644 --- a/src/repository/RepositoryFactory.ts +++ b/src/repository/RepositoryFactory.ts @@ -20,7 +20,7 @@ export class RepositoryFactory { */ create(manager: EntityManager, metadata: EntityMetadata, queryRunner?: QueryRunner): Repository { - if (metadata.isClosure) { + if (metadata.treeType) { // NOTE: dynamic access to protected properties. We need this to prevent unwanted properties in those classes to be exposed, // however we need these properties for internal work of the class const repository = new TreeRepository(); diff --git a/src/repository/TreeRepository.ts b/src/repository/TreeRepository.ts index a9885b6f15..b758485b62 100644 --- a/src/repository/TreeRepository.ts +++ b/src/repository/TreeRepository.ts @@ -1,5 +1,7 @@ import {Repository} from "./Repository"; import {SelectQueryBuilder} from "../query-builder/SelectQueryBuilder"; +import {ObjectLiteral} from "../common/ObjectLiteral"; +import {AbstractSqliteDriver} from "../driver/sqlite-abstract/AbstractSqliteDriver"; /** * Repository with additional functions to work with trees. @@ -20,10 +22,7 @@ export class TreeRepository extends Repository { */ async findTrees(): Promise { const roots = await this.findRoots(); - await Promise.all(roots.map(async root => { - await this.findDescendantsTree(root); - })); - + await Promise.all(roots.map(root => this.findDescendantsTree(root))); return roots; } @@ -83,11 +82,59 @@ export class TreeRepository extends Repository { // create shortcuts for better readability const escape = (alias: string) => this.manager.connection.driver.escape(alias); - const joinCondition = `${escape(alias)}.${escape(this.metadata.primaryColumns[0].databaseName)}=${escape(closureTableAlias)}.${escape("descendant")}`; - return this.createQueryBuilder(alias) - .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition) - .where(`${escape(closureTableAlias)}.${escape("ancestor")}=${this.metadata.getEntityIdMap(entity)![this.metadata.primaryColumns[0].propertyName]}`); + if (this.metadata.treeType === "closure-table") { + + const joinCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => { + return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath); + }).join(" AND "); + + const parameters: ObjectLiteral = {}; + const whereCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => { + parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity); + return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName; + }).join(" AND "); + + return this + .createQueryBuilder(alias) + .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition) + .where(whereCondition) + .setParameters(parameters); + + } else if (this.metadata.treeType === "nested-set") { + + const whereCondition = alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " + + "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND joined." + this.metadata.nestedSetRightColumn!.propertyPath; + const parameters: ObjectLiteral = {}; + const joinCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => { + const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_"); + parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity); + return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName; + }).join(" AND "); + return this + .createQueryBuilder(alias) + .innerJoin(this.metadata.targetName, "joined", whereCondition) + .where(joinCondition, parameters); + + } else if (this.metadata.treeType === "materialized-path") { + return this + .createQueryBuilder(alias) + .where(qb => { + const subQuery = qb.subQuery() + .select(this.metadata.materializedPathColumn!.propertyPath, "path") + .from(this.metadata.target, this.metadata.targetName) + .whereInIds(this.metadata.getEntityIdMap(entity)); + + qb.setNativeParameters(subQuery.expressionMap.nativeParameters); + if (this.manager.connection.driver instanceof AbstractSqliteDriver) { + return this.metadata.materializedPathColumn!.propertyPath + " LIKE " + subQuery.getQuery() + " || '%'"; + } else { + return this.metadata.materializedPathColumn!.propertyPath + " LIKE CONCAT(" + subQuery.getQuery() + ", '%')"; + } + }); + } + + throw new Error(`Supported only in tree entities`); } /** @@ -129,13 +176,63 @@ export class TreeRepository extends Repository { createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder { // create shortcuts for better readability - const escapeAlias = (alias: string) => this.manager.connection.driver.escape(alias); - const escapeColumn = (column: string) => this.manager.connection.driver.escape(column); + const escape = (alias: string) => this.manager.connection.driver.escape(alias); + + if (this.metadata.treeType === "closure-table") { + const joinCondition = this.metadata.closureJunctionTable.ancestorColumns.map(column => { + return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = " + escape(alias) + "." + escape(column.referencedColumn!.propertyPath); + }).join(" AND "); + + const parameters: ObjectLiteral = {}; + const whereCondition = this.metadata.closureJunctionTable.descendantColumns.map(column => { + parameters[column.referencedColumn!.propertyName] = column.referencedColumn!.getEntityValue(entity); + return escape(closureTableAlias) + "." + escape(column.propertyPath) + " = :" + column.referencedColumn!.propertyName; + }).join(" AND "); + + return this + .createQueryBuilder(alias) + .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition) + .where(whereCondition) + .setParameters(parameters); + + } else if (this.metadata.treeType === "nested-set") { + + const joinCondition = "joined." + this.metadata.nestedSetLeftColumn!.propertyPath + " BETWEEN " + + alias + "." + this.metadata.nestedSetLeftColumn!.propertyPath + " AND " + alias + "." + this.metadata.nestedSetRightColumn!.propertyPath; + const parameters: ObjectLiteral = {}; + const whereCondition = this.metadata.treeParentRelation!.joinColumns.map(joinColumn => { + const parameterName = joinColumn.referencedColumn!.propertyPath.replace(".", "_"); + parameters[parameterName] = joinColumn.referencedColumn!.getEntityValue(entity); + return "joined." + joinColumn.referencedColumn!.propertyPath + " = :" + parameterName; + }).join(" AND "); + + return this + .createQueryBuilder(alias) + .innerJoin(this.metadata.targetName, "joined", joinCondition) + .where(whereCondition, parameters); + + + } else if (this.metadata.treeType === "materialized-path") { + // example: SELECT * FROM category WHERE (SELECT mpath FROM `category` WHERE id = 2) LIKE CONCAT(mpath, '%'); + return this + .createQueryBuilder(alias) + .where(qb => { + const subQuery = qb.subQuery() + .select(this.metadata.materializedPathColumn!.propertyPath, "path") + .from(this.metadata.target, this.metadata.targetName) + .whereInIds(this.metadata.getEntityIdMap(entity)); + + qb.setNativeParameters(subQuery.expressionMap.nativeParameters); + if (this.manager.connection.driver instanceof AbstractSqliteDriver) { + return subQuery.getQuery() + " LIKE " + this.metadata.materializedPathColumn!.propertyPath + " || '%'"; + + } else { + return subQuery.getQuery() + " LIKE CONCAT(" + this.metadata.materializedPathColumn!.propertyPath + ", '%')"; + } + }); + } - const joinCondition = `${escapeAlias(alias)}.${escapeColumn(this.metadata.primaryColumns[0].databaseName)}=${escapeAlias(closureTableAlias)}.${escapeColumn("ancestor")}`; - return this.createQueryBuilder(alias) - .innerJoin(this.metadata.closureJunctionTable.tableName, closureTableAlias, joinCondition) - .where(`${escapeAlias(closureTableAlias)}.${escapeColumn("descendant")}=${this.metadata.getEntityIdMap(entity)![this.metadata.primaryColumns[0].propertyName]}`); + throw new Error(`Supported only in tree entities`); } /** diff --git a/test/functional/connection/entity/Category.ts b/test/functional/connection/entity/Category.ts index df32f81ba2..92cd1843be 100644 --- a/test/functional/connection/entity/Category.ts +++ b/test/functional/connection/entity/Category.ts @@ -1,11 +1,13 @@ -import {ClosureEntity} from "../../../../src/decorator/entity/ClosureEntity"; import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../src/decorator/columns/Column"; import {TreeParent} from "../../../../src/decorator/tree/TreeParent"; import {TreeChildren} from "../../../../src/decorator/tree/TreeChildren"; import {TreeLevelColumn} from "../../../../src/decorator/tree/TreeLevelColumn"; +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {Tree} from "../../../../src/decorator/tree/Tree"; -@ClosureEntity("CaTeGoRy") +@Entity("CaTeGoRy") +@Tree("closure-table") export class Category { @PrimaryGeneratedColumn() diff --git a/test/functional/tree-tables/closure-table/closure-table.ts b/test/functional/tree-tables/closure-table/closure-table.ts index 3856d7ad0a..a464631fda 100644 --- a/test/functional/tree-tables/closure-table/closure-table.ts +++ b/test/functional/tree-tables/closure-table/closure-table.ts @@ -3,21 +3,55 @@ import {Category} from "./entity/Category"; import {Connection} from "../../../../src/connection/Connection"; import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; -// todo: uncomment test once closure tables functionality is back -describe.skip("closure-table", () => { +describe("tree tables > closure-table", () => { let connections: Connection[]; before(async () => connections = await createTestingConnections({ - entities: [Category], + entities: [Category] })); beforeEach(() => reloadTestingDatabases(connections)); after(() => closeTestingConnections(connections)); - it("should work correctly when saving using parent category", () => Promise.all(connections.map(async connection => { + it("attach should work properly", () => Promise.all(connections.map(async connection => { const categoryRepository = connection.getTreeRepository(Category); const a1 = new Category(); a1.name = "a1"; + await categoryRepository.save(a1); + + const a11 = new Category(); + a11.name = "a11"; + a11.parentCategory = a1; + await categoryRepository.save(a11); + + const a12 = new Category(); + a12.name = "a12"; + a12.parentCategory = a1; + await categoryRepository.save(a12); + + const rootCategories = await categoryRepository.findRoots(); + rootCategories.should.be.eql([{ + id: 1, + name: "a1" + }]); + + const a11Parent = await categoryRepository.findAncestors(a11); + a11Parent.length.should.be.equal(2); + a11Parent.should.include({ id: 1, name: "a1" }); + a11Parent.should.include({ id: 2, name: "a11" }); + + const a1Children = await categoryRepository.findDescendants(a1); + a1Children.length.should.be.equal(3); + a1Children.should.include({ id: 1, name: "a1" }); + a1Children.should.include({ id: 2, name: "a11" }); + a1Children.should.include({ id: 3, name: "a12" }); + }))); + + it.skip("should work correctly when saving using parent category", () => Promise.all(connections.map(async connection => { + // await categoryRepository.attach(a1, a11); + + /*const a1 = new Category(); + a1.name = "a1"; const b1 = new Category(); b1.name = "b1"; @@ -79,11 +113,11 @@ describe.skip("closure-table", () => { level: 2, childCategories: [] }] - }); + });*/ }))); - it("should work correctly when saving using children categories", () => Promise.all(connections.map(async connection => { + it.skip("should work correctly when saving using children categories", () => Promise.all(connections.map(async connection => { const categoryRepository = connection.getTreeRepository(Category); const a1 = new Category(); @@ -153,7 +187,7 @@ describe.skip("closure-table", () => { }))); - it("should be able to retrieve the whole tree", () => Promise.all(connections.map(async connection => { + it.skip("should be able to retrieve the whole tree", () => Promise.all(connections.map(async connection => { const categoryRepository = connection.getTreeRepository(Category); const a1 = new Category(); diff --git a/test/functional/tree-tables/closure-table/entity/Category.ts b/test/functional/tree-tables/closure-table/entity/Category.ts index cd45fa5d0f..5dc4d3178d 100644 --- a/test/functional/tree-tables/closure-table/entity/Category.ts +++ b/test/functional/tree-tables/closure-table/entity/Category.ts @@ -1,11 +1,12 @@ -import {ClosureEntity} from "../../../../../src/decorator/entity/ClosureEntity"; import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../../src/decorator/columns/Column"; import {TreeParent} from "../../../../../src/decorator/tree/TreeParent"; import {TreeChildren} from "../../../../../src/decorator/tree/TreeChildren"; -import {TreeLevelColumn} from "../../../../../src/decorator/tree/TreeLevelColumn"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Tree} from "../../../../../src/decorator/tree/Tree"; -@ClosureEntity() +@Entity() +@Tree("closure-table") export class Category { @PrimaryGeneratedColumn() @@ -20,7 +21,7 @@ export class Category { @TreeChildren({ cascade: true }) childCategories: Category[]; - @TreeLevelColumn() - level: number; + // @TreeLevelColumn() + // level: number; } \ No newline at end of file diff --git a/test/functional/tree-tables/materialized-path/entity/Category.ts b/test/functional/tree-tables/materialized-path/entity/Category.ts new file mode 100644 index 0000000000..6013a6bbbe --- /dev/null +++ b/test/functional/tree-tables/materialized-path/entity/Category.ts @@ -0,0 +1,27 @@ +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {TreeParent} from "../../../../../src/decorator/tree/TreeParent"; +import {TreeChildren} from "../../../../../src/decorator/tree/TreeChildren"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Tree} from "../../../../../src/decorator/tree/Tree"; + +@Entity() +@Tree("materialized-path") +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @TreeParent({ cascade: true }) + parentCategory: Category; + + @TreeChildren({ cascade: true }) + childCategories: Category[]; + + // @TreeLevelColumn() + // level: number; + +} \ No newline at end of file diff --git a/test/functional/tree-tables/materialized-path/materialized-path.ts b/test/functional/tree-tables/materialized-path/materialized-path.ts new file mode 100644 index 0000000000..bb12c2525d --- /dev/null +++ b/test/functional/tree-tables/materialized-path/materialized-path.ts @@ -0,0 +1,56 @@ +import "reflect-metadata"; +import {Category} from "./entity/Category"; +import {Connection} from "../../../../src/connection/Connection"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; + +describe("tree tables > materialized-path", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [Category], + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("attach should work properly", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + await categoryRepository.save(a1); + + const a11 = new Category(); + a11.name = "a11"; + a11.parentCategory = a1; + await categoryRepository.save(a11); + + const a111 = new Category(); + a111.name = "a111"; + a111.parentCategory = a11; + await categoryRepository.save(a111); + + const a12 = new Category(); + a12.name = "a12"; + a12.parentCategory = a1; + await categoryRepository.save(a12); + + const rootCategories = await categoryRepository.findRoots(); + rootCategories.should.be.eql([{ + id: 1, + name: "a1" + }]); + + const a11Parent = await categoryRepository.findAncestors(a11); + a11Parent.length.should.be.equal(2); + a11Parent.should.contain({ id: 1, name: "a1" }); + a11Parent.should.contain({ id: 2, name: "a11" }); + + const a1Children = await categoryRepository.findDescendants(a1); + a1Children.length.should.be.equal(4); + a1Children.should.contain({ id: 1, name: "a1" }); + a1Children.should.contain({ id: 2, name: "a11" }); + a1Children.should.contain({ id: 3, name: "a111" }); + a1Children.should.contain({ id: 4, name: "a12" }); + }))); + +}); diff --git a/test/functional/tree-tables/nested-set/entity/Category.ts b/test/functional/tree-tables/nested-set/entity/Category.ts new file mode 100644 index 0000000000..65f0bc0355 --- /dev/null +++ b/test/functional/tree-tables/nested-set/entity/Category.ts @@ -0,0 +1,27 @@ +import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"; +import {Column} from "../../../../../src/decorator/columns/Column"; +import {TreeParent} from "../../../../../src/decorator/tree/TreeParent"; +import {TreeChildren} from "../../../../../src/decorator/tree/TreeChildren"; +import {Entity} from "../../../../../src/decorator/entity/Entity"; +import {Tree} from "../../../../../src/decorator/tree/Tree"; + +@Entity() +@Tree("nested-set") +export class Category { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @TreeParent({ cascade: true }) + parentCategory: Category; + + @TreeChildren({ cascade: true }) + childCategories: Category[]; + + // @TreeLevelColumn() + // level: number; + +} \ No newline at end of file diff --git a/test/functional/tree-tables/nested-set/nested-set.ts b/test/functional/tree-tables/nested-set/nested-set.ts new file mode 100644 index 0000000000..0d21b66cbf --- /dev/null +++ b/test/functional/tree-tables/nested-set/nested-set.ts @@ -0,0 +1,56 @@ +import "reflect-metadata"; +import {Category} from "./entity/Category"; +import {Connection} from "../../../../src/connection/Connection"; +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils"; + +describe("tree tables > nested-set", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [Category] + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("attach should work properly", () => Promise.all(connections.map(async connection => { + const categoryRepository = connection.getTreeRepository(Category); + + const a1 = new Category(); + a1.name = "a1"; + await categoryRepository.save(a1); + + const a11 = new Category(); + a11.name = "a11"; + a11.parentCategory = a1; + await categoryRepository.save(a11); + + const a111 = new Category(); + a111.name = "a111"; + a111.parentCategory = a11; + await categoryRepository.save(a111); + + const a12 = new Category(); + a12.name = "a12"; + a12.parentCategory = a1; + await categoryRepository.save(a12); + + const rootCategories = await categoryRepository.findRoots(); + rootCategories.should.be.eql([{ + id: 1, + name: "a1" + }]); + + const a11Parent = await categoryRepository.findAncestors(a11); + a11Parent.length.should.be.equal(2); + a11Parent.should.contain({ id: 1, name: "a1" }); + a11Parent.should.contain({ id: 2, name: "a11" }); + + const a1Children = await categoryRepository.findDescendants(a1); + a1Children.length.should.be.equal(4); + a1Children.should.contain({ id: 1, name: "a1" }); + a1Children.should.contain({ id: 2, name: "a11" }); + a1Children.should.contain({ id: 3, name: "a111" }); + a1Children.should.contain({ id: 4, name: "a12" }); + }))); + +}); diff --git a/test/github-issues/904/entity/Category.ts b/test/github-issues/904/entity/Category.ts index 629e62e098..2e865722b4 100644 --- a/test/github-issues/904/entity/Category.ts +++ b/test/github-issues/904/entity/Category.ts @@ -1,12 +1,12 @@ -import {ClosureEntity} from "../../../../src/decorator/entity/ClosureEntity"; import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn"; import {Column} from "../../../../src/decorator/columns/Column"; import {TreeParent} from "../../../../src/decorator/tree/TreeParent"; import {TreeChildren} from "../../../../src/decorator/tree/TreeChildren"; +import {Entity} from "../../../../src/decorator/entity/Entity"; +import {Tree} from "../../../../src/decorator/tree/Tree"; -// @Entity() -// @Tree("closure") -@ClosureEntity() +@Entity("sample22_category") +@Tree("closure-table") export class Category { @PrimaryGeneratedColumn()