mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
added basic support for tree tables - closure tables, nested set, materialized path
This commit is contained in:
parent
2fd585f901
commit
42d6c88031
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
@ -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.
|
||||
|
||||
@ -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() {
|
||||
/* export */ function ChildEntity(tableName, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.AbstractEntity = AbstractEntity;
|
||||
|
||||
/* export */ function ClassEntityChild(tableName, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.ClassEntityChild = ClassEntityChild;
|
||||
|
||||
/* export */ function ClosureEntity(name, 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,46 +197,14 @@ 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;
|
||||
|
||||
/* export */ function SingleTableChild() {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.SingleTableChild = SingleTableChild;
|
||||
|
||||
/* export */ function Table(name, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.Table = Table;
|
||||
|
||||
// tree
|
||||
|
||||
/* export */ function Tree(name, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.Tree = Tree;
|
||||
|
||||
/* export */ function TreeChildren(options) {
|
||||
return function (object, propertyName) {
|
||||
class_transformer_1.Type(typeFunction)(object, propertyName);
|
||||
@ -274,6 +212,13 @@ exports.Table = Table;
|
||||
}
|
||||
exports.TreeChildren = TreeChildren;
|
||||
|
||||
/* export */ function TreeChildrenCount(options) {
|
||||
return function (object, propertyName) {
|
||||
class_transformer_1.Type(typeFunction)(object, propertyName);
|
||||
};
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -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() {
|
||||
/* export */ function ChildEntity(tableName, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.AbstractEntity = AbstractEntity;
|
||||
|
||||
/* export */ function ClassEntityChild(tableName, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.ClassEntityChild = ClassEntityChild;
|
||||
|
||||
/* export */ function ClosureEntity(name, 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,52 +185,26 @@ 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;
|
||||
|
||||
/* export */ function SingleTableChild() {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.SingleTableChild = SingleTableChild;
|
||||
|
||||
/* export */ function Table(name, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.Table = Table;
|
||||
|
||||
// tree
|
||||
|
||||
/* export */ function Tree(name, options) {
|
||||
return function (object) {
|
||||
};
|
||||
}
|
||||
exports.Tree = Tree;
|
||||
|
||||
/* export */ function TreeChildren(options) {
|
||||
return function (object, propertyName) {
|
||||
};
|
||||
}
|
||||
exports.TreeChildren = TreeChildren;
|
||||
|
||||
/* export */ function TreeChildrenCount(options) {
|
||||
return function (object, propertyName) {
|
||||
};
|
||||
}
|
||||
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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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<Entity>(target: ObjectType<Entity>|string): TreeRepository<Entity> {
|
||||
return this.manager.getTreeRepository(target);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
16
src/decorator/tree/Tree.ts
Normal file
16
src/decorator/tree/Tree.ts
Normal file
@ -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);
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ export class RepositoryNotTreeError extends Error {
|
||||
constructor(entityClass: Function|string) {
|
||||
super();
|
||||
const targetName = typeof entityClass === "function" && (<any> entityClass).name ? (<any> 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;
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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[] {
|
||||
|
||||
18
src/metadata-args/TreeMetadataArgs.ts
Normal file
18
src/metadata-args/TreeMetadataArgs.ts
Normal file
@ -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;
|
||||
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
4
src/metadata/types/TreeTypes.ts
Normal file
4
src/metadata/types/TreeTypes.ts
Normal file
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Tree type.
|
||||
*/
|
||||
export type TreeType = "adjacency-list"|"closure-table"|"nested-set"|"materialized-path";
|
||||
@ -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);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
76
src/persistence/tree/ClosureSubjectExecutor.ts
Normal file
76
src/persistence/tree/ClosureSubjectExecutor.ts
Normal file
@ -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<void> {
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
52
src/persistence/tree/MaterializedPathSubjectExecutor.ts
Normal file
52
src/persistence/tree/MaterializedPathSubjectExecutor.ts
Normal file
@ -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<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
63
src/persistence/tree/NestedSetSubjectExecutor.ts
Normal file
63
src/persistence/tree/NestedSetSubjectExecutor.ts
Normal file
@ -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<void> {
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -352,6 +352,18 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
|
||||
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);
|
||||
|
||||
@ -20,7 +20,7 @@ export class RepositoryFactory {
|
||||
*/
|
||||
create(manager: EntityManager, metadata: EntityMetadata, queryRunner?: QueryRunner): Repository<any> {
|
||||
|
||||
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<any>();
|
||||
|
||||
@ -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<Entity> extends Repository<Entity> {
|
||||
*/
|
||||
async findTrees(): Promise<Entity[]> {
|
||||
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<Entity> extends Repository<Entity> {
|
||||
// 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<Entity> extends Repository<Entity> {
|
||||
createAncestorsQueryBuilder(alias: string, closureTableAlias: string, entity: Entity): SelectQueryBuilder<Entity> {
|
||||
|
||||
// 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);
|
||||
|
||||
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]}`);
|
||||
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 + ", '%')";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Supported only in tree entities`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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" });
|
||||
})));
|
||||
|
||||
});
|
||||
27
test/functional/tree-tables/nested-set/entity/Category.ts
Normal file
27
test/functional/tree-tables/nested-set/entity/Category.ts
Normal file
@ -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;
|
||||
|
||||
}
|
||||
56
test/functional/tree-tables/nested-set/nested-set.ts
Normal file
56
test/functional/tree-tables/nested-set/nested-set.ts
Normal file
@ -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" });
|
||||
})));
|
||||
|
||||
});
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user