implemented custom repositories support

This commit is contained in:
Umed Khudoiberdiev 2017-01-13 18:03:51 +05:00
parent 82249a4523
commit 8884789d31
18 changed files with 489 additions and 9 deletions

View File

@ -5,7 +5,9 @@
* all table decorators are renamed to `Entity` (`Table` => `Entity`, `AbstractTable` => `AbstractEntity`,
`ClassTableChild` => `ClassEntityChild`, `ClosureTable` => `ClosureEntity`, `EmbeddableTable` => `EmbeddableEntity`,
`SingleTableChild` => `SingleEntityChild`). This change is required because upcoming versions of orm will work
not only with tables, but also with documents and other database-specific "tables".
not only with tables, but also with documents and other database-specific "tables".
Previous decorator names are deprecated and will be removed in the future.
* added custom repositories support. Example in samples directory.
# 0.0.6 (latest)

View File

@ -0,0 +1,55 @@
import "reflect-metadata";
import {ConnectionOptions, createConnection} from "../../src/index";
import {Post} from "./entity/Post";
import {Author} from "./entity/Author";
import {MigrationExecutor} from "../../src/migration/MigrationExecutor";
import {PostRepository} from "./repository/PostRepository";
import {AuthorRepository} from "./repository/AuthorRepository";
const options: ConnectionOptions = {
driver: {
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "test"
},
autoSchemaSync: true,
logging: {
logQueries: true,
},
entities: [Post, Author],
};
createConnection(options).then(async connection => {
const post = connection
.getCustomRepository(PostRepository)
.create();
post.title = "Hello Repositories!";
await connection
.entityManager
.getCustomRepository(PostRepository)
.persist(post);
const loadedPost = await connection
.entityManager
.getCustomRepository(PostRepository)
.findMyPost();
console.log("Post persisted! Loaded post: ", loadedPost);
const author = await connection
.getCustomRepository(AuthorRepository)
.createAndSave("Umed", "Khudoiberdiev");
const loadedAuthor = await connection
.getCustomRepository(AuthorRepository)
.findMyAuthor();
console.log("Author persisted! Loaded author: ", loadedAuthor);
}).catch(error => console.log("Error: ", error));

View File

@ -0,0 +1,15 @@
import {PrimaryGeneratedColumn, Column, Entity} from "../../../src/index";
@Entity()
export class Author {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
}

View File

@ -0,0 +1,18 @@
import {PrimaryGeneratedColumn, Column, Entity, ManyToOne} from "../../../src/index";
import {Author} from "./Author";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(type => Author, {
cascadeInsert: true
})
author: Author;
}

View File

@ -0,0 +1,25 @@
import {EntityRepository} from "../../../src/decorator/EntityRepository";
import {AbstractRepository} from "../../../src/repository/AbstractRepository";
import {Author} from "../entity/Author";
/**
* Second type of custom repository - extends abstract repository (also can not extend anything).
*/
@EntityRepository(Author)
export class AuthorRepository extends AbstractRepository<Author> {
createAndSave(firstName: string, lastName: string) {
const author = new Author();
author.firstName = firstName;
author.lastName = lastName;
return this.entityManager.persist(author);
}
findMyAuthor() {
return this
.createQueryBuilder("author")
.getOne();
}
}

View File

@ -0,0 +1,15 @@
import {Repository} from "../../../src/repository/Repository";
import {Post} from "../entity/Post";
import {EntityRepository} from "../../../src/decorator/EntityRepository";
/**
* First type of custom repository - extends standard repository.
*/
@EntityRepository(Post)
export class PostRepository extends Repository<Post> {
findMyPost() {
return this.findOne();
}
}

View File

@ -32,6 +32,10 @@ import {MigrationInterface} from "../migration/MigrationInterface";
import {MigrationExecutor} from "../migration/MigrationExecutor";
import {CannotRunMigrationNotConnectedError} from "./error/CannotRunMigrationNotConnectedError";
import {PlatformTools} from "../platform/PlatformTools";
import {AbstractRepository} from "../repository/AbstractRepository";
import {CustomRepositoryNotFoundError} from "../repository/error/CustomRepositoryNotFoundError";
import {CustomRepositoryReusedError} from "../repository/error/CustomRepositoryReusedError";
import {CustomRepositoryCannotInheritRepositoryError} from "../repository/error/CustomRepositoryCannotInheritRepositoryError";
/**
* Connection is a single database connection to a specific database of a database management system.
@ -82,6 +86,11 @@ export class Connection {
*/
private readonly repositoryAggregators: RepositoryAggregator[] = [];
/**
* Stores all entity repository instances.
*/
private readonly entityRepositories: Object[] = [];
/**
* Entity listeners that are registered for this connection.
*/
@ -427,6 +436,13 @@ export class Connection {
*/
getTreeRepository<Entity>(entityClass: ObjectType<Entity>): TreeRepository<Entity>;
/**
* Gets tree repository for the given entity class.
* Only tree-type entities can have a TreeRepository,
* like ones decorated with @ClosureTable decorator.
*/
getTreeRepository<Entity>(entityClassOrName: ObjectType<Entity>|string): TreeRepository<Entity>;
/**
* Gets tree repository for the given entity class.
* Only tree-type entities can have a TreeRepository,
@ -497,6 +513,73 @@ export class Connection {
return [];
}
/**
* Gets custom entity repository marked with @EntityRepository decorator.
*/
getCustomRepository<T>(customRepository: ObjectType<T>): T {
const entityRepositoryMetadataArgs = getMetadataArgsStorage().entityRepositories.toArray().find(repository => {
return repository.target === (customRepository instanceof Function ? customRepository : (customRepository as any).constructor);
});
if (!entityRepositoryMetadataArgs)
throw new CustomRepositoryNotFoundError(customRepository);
let entityRepositoryInstance: any = this.entityRepositories.find(entityRepository => entityRepository.constructor === customRepository);
if (!entityRepositoryInstance) {
if (entityRepositoryMetadataArgs.useContainer) {
entityRepositoryInstance = getFromContainer(entityRepositoryMetadataArgs.target);
// if we get custom entity repository from container then there is a risk that it already was used
// in some different connection. If it was used there then we check it and throw an exception
// because we cant override its connection there again
if (entityRepositoryInstance instanceof AbstractRepository || entityRepositoryInstance instanceof Repository) {
// 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
if ((entityRepositoryInstance as any)["connection"] && (entityRepositoryInstance as any)["connection"] !== this)
throw new CustomRepositoryReusedError(customRepository);
}
} else {
entityRepositoryInstance = new (entityRepositoryMetadataArgs.target as any)();
}
if (entityRepositoryInstance instanceof AbstractRepository) {
// 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
if (!(entityRepositoryInstance as any)["connection"])
(entityRepositoryInstance as any)["connection"] = this;
}
if (entityRepositoryInstance instanceof Repository) {
if (!entityRepositoryMetadataArgs.entity)
throw new CustomRepositoryCannotInheritRepositoryError(customRepository);
// 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
(entityRepositoryInstance as any)["connection"] = this;
(entityRepositoryInstance as any)["metadata"] = this.getMetadata(entityRepositoryMetadataArgs.entity);
}
// register entity repository
this.entityRepositories.push(entityRepositoryInstance);
}
return entityRepositoryInstance;
}
/**
* Gets custom repository's managed entity.
* If given custom repository does not manage any entity then undefined will be returned.
*/
getCustomRepositoryTarget<T>(customRepository: any): Function|string|undefined {
const entityRepositoryMetadataArgs = getMetadataArgsStorage().entityRepositories.toArray().find(repository => {
return repository.target === (customRepository instanceof Function ? customRepository : (customRepository as any).constructor);
});
if (!entityRepositoryMetadataArgs)
throw new CustomRepositoryNotFoundError(customRepository);
return entityRepositoryMetadataArgs.entity;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
@ -630,4 +713,5 @@ export class Connection {
protected createLazyRelationsWrapper() {
return new LazyRelationsWrapper(this);
}
}

View File

@ -0,0 +1,34 @@
import {getMetadataArgsStorage} from "../index";
import {EntityRepositoryMetadataArgs} from "../metadata-args/EntityRepositoryMetadataArgs";
/**
* Used to declare a class as a custom repository.
* Custom repository can either manage some specific entity, either just be generic.
* Custom repository can extend AbstractRepository or regular Repository or TreeRepository.
*/
export function EntityRepository(entity?: Function, options?: { useContainer?: boolean }): Function;
/**
* Used to declare a class as a custom repository.
* Custom repository can either manage some specific entity, either just be generic.
* Custom repository can extend AbstractRepository or regular Repository or TreeRepository.
*/
export function EntityRepository(options?: { useContainer?: boolean }): Function;
/**
* Used to declare a class as a custom repository.
* Custom repository can either manage some specific entity, either just be generic.
* Custom repository can extend AbstractRepository or regular Repository or TreeRepository.
*/
export function EntityRepository(entityOrOptions?: Function|{ useContainer?: boolean }, maybeOptions?: { useContainer?: boolean }): Function {
const entity = entityOrOptions instanceof Function ? entityOrOptions as Function : undefined;
const options = entityOrOptions instanceof Function ? maybeOptions : entityOrOptions as { useContainer?: boolean };
return function (target: Function) {
const args: EntityRepositoryMetadataArgs = {
target: target,
entity: entity,
useContainer: !!(options && options.useContainer)
};
getMetadataArgsStorage().entityRepositories.add(args);
};
}

View File

@ -140,6 +140,13 @@ export abstract class BaseEntityManager {
return this.connection.getSpecificRepository<Entity>(entityClassOrName as any);
}
/**
* Gets custom entity repository marked with @EntityRepository decorator.
*/
getCustomRepository<T>(customRepository: ObjectType<T>): T {
return this.connection.getCustomRepository<T>(customRepository);
}
/**
* Checks if entity has an id.
*/

View File

@ -0,0 +1,22 @@
/**
* Arguments for EntityRepositoryMetadata class, helps to construct an EntityRepositoryMetadata object.
*/
export interface EntityRepositoryMetadataArgs {
/**
* Constructor of the custom entity repository.
*/
readonly target: Function;
/**
* Entity managed by this custom repository.
*/
readonly entity?: Function|string;
/**
* Indicates if entity repository will be retrieved from the service container.
* Note: this may cause problems if you are sharing entity repositories between using multiple connections.
*/
readonly useContainer: boolean;
}

View File

@ -14,6 +14,7 @@ import {EntitySubscriberMetadataArgs} from "./EntitySubscriberMetadataArgs";
import {RelationIdMetadataArgs} from "./RelationIdMetadataArgs";
import {InheritanceMetadataArgs} from "./InheritanceMetadataArgs";
import {DiscriminatorValueMetadataArgs} from "./DiscriminatorValueMetadataArgs";
import {EntityRepositoryMetadataArgs} from "./EntityRepositoryMetadataArgs";
/**
* Storage all metadatas of all available types: tables, fields, subscribers, relations, etc.
@ -31,6 +32,7 @@ export class MetadataArgsStorage {
// -------------------------------------------------------------------------
readonly tables = new TargetMetadataArgsCollection<TableMetadataArgs>();
readonly entityRepositories = new TargetMetadataArgsCollection<EntityRepositoryMetadataArgs>();
readonly namingStrategies = new TargetMetadataArgsCollection<NamingStrategyMetadataArgs>();
readonly entitySubscribers = new TargetMetadataArgsCollection<EntitySubscriberMetadataArgs>();
readonly indices = new TargetMetadataArgsCollection<IndexMetadataArgs>();

View File

@ -0,0 +1,120 @@
import {Connection} from "../connection/Connection";
import {QueryBuilder} from "../query-builder/QueryBuilder";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {EntityManager} from "../entity-manager/EntityManager";
import {Repository} from "./Repository";
import {TreeRepository} from "./TreeRepository";
import {SpecificRepository} from "./SpecificRepository";
import {ObjectType} from "../common/ObjectType";
import {CustomRepositoryDoesNotHaveEntityError} from "./error/CustomRepositoryDoesNotHaveEntityError";
/**
* Provides abstract class for custom repositories that do not inherit from original orm Repository.
* Contains all most-necessary methods to simplify code in the custom repository.
* All methods are protected thus not exposed and it allows to create encapsulated custom repository.
*
* @experimental
*/
export class AbstractRepository<Entity extends ObjectLiteral> {
// -------------------------------------------------------------------------
// Protected Methods Set Dynamically
// -------------------------------------------------------------------------
/**
* Connection used by this repository.
*/
protected connection: Connection;
// -------------------------------------------------------------------------
// Protected Accessors
// -------------------------------------------------------------------------
/**
* Gets entity manager that allows to perform repository operations with any entity.
*/
protected get entityManager(): EntityManager {
return this.connection.entityManager;
}
/**
* Gets the original ORM repository for the entity that is managed by this repository.
* If current repository does not manage any entity, then exception will be thrown.
*/
protected get repository(): Repository<Entity> {
const target = this.connection.getCustomRepositoryTarget(this as any);
if (!target)
throw new CustomRepositoryDoesNotHaveEntityError(this.constructor);
return this.connection.getRepository<Entity>(target);
}
/**
* Gets the original ORM tree repository for the entity that is managed by this repository.
* If current repository does not manage any entity, then exception will be thrown.
*/
protected get treeRepository(): TreeRepository<Entity> {
const target = this.connection.getCustomRepositoryTarget(this as any);
if (!target)
throw new CustomRepositoryDoesNotHaveEntityError(this.constructor);
return this.connection.getTreeRepository<Entity>(target);
}
/**
* Gets the original ORM specific repository for the entity that is managed by this repository.
* If current repository does not manage any entity, then exception will be thrown.
*/
protected get specificRepository(): SpecificRepository<Entity> {
const target = this.connection.getCustomRepositoryTarget(this as any);
if (!target)
throw new CustomRepositoryDoesNotHaveEntityError(this.constructor);
return this.connection.getSpecificRepository<Entity>(target);
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Creates a new query builder for the repository's entity that can be used to build a sql query.
* If current repository does not manage any entity, then exception will be thrown.
*/
protected createQueryBuilder(alias: string): QueryBuilder<Entity> {
const target = this.connection.getCustomRepositoryTarget(this.constructor);
if (!target)
throw new CustomRepositoryDoesNotHaveEntityError(this.constructor);
return this.connection.getRepository(target).createQueryBuilder(alias);
}
/**
* Creates a new query builder for the given entity that can be used to build a sql query.
*/
protected createQueryBuilderFor<T>(entity: ObjectType<T>, alias: string): QueryBuilder<T> {
return this.getRepositoryFor(entity).createQueryBuilder(alias);
}
/**
* Gets the original ORM repository for the given entity class.
*/
protected getRepositoryFor<T>(entity: ObjectType<T>): Repository<T> {
return this.entityManager.getRepository(entity);
}
/**
* Gets the original ORM tree repository for the given entity class.
*/
protected getTreeRepositoryFor<T>(entity: ObjectType<T>): TreeRepository<T> {
return this.entityManager.getTreeRepository(entity);
}
/**
* Gets the original ORM specific repository for the given entity class.
*/
protected getSpecificRepositoryFor<T>(entity: ObjectType<T>): SpecificRepository<T> {
return this.entityManager.getSpecificRepository(entity);
}
}

View File

@ -16,13 +16,23 @@ import {SubjectBuilder} from "../persistence/SubjectBuilder";
export class Repository<Entity extends ObjectLiteral> {
// -------------------------------------------------------------------------
// Constructor
// Protected Methods Set Dynamically
// -------------------------------------------------------------------------
constructor(protected connection: Connection,
protected metadata: EntityMetadata,
protected queryRunnerProvider?: QueryRunnerProvider) {
}
/**
* Connection used by this repository.
*/
protected connection: Connection;
/**
* Entity metadata of the entity current repository manages.
*/
protected metadata: EntityMetadata;
/**
* Query runner provider used for this repository.
*/
protected queryRunnerProvider?: QueryRunnerProvider;
// -------------------------------------------------------------------------
// Public Methods
@ -317,7 +327,14 @@ export class Repository<Entity extends ObjectLiteral> {
async transaction(runInTransaction: (repository: Repository<Entity>) => Promise<any>|any): Promise<any> {
const queryRunnerProvider = this.queryRunnerProvider || new QueryRunnerProvider(this.connection.driver, true);
const queryRunner = await queryRunnerProvider.provide();
const transactionRepository = new Repository<Entity>(this.connection, this.metadata, queryRunnerProvider);
// 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 transactionRepository = new Repository<any>();
(transactionRepository as any)["connection"] = this.connection;
(transactionRepository as any)["metadata"] = this.metadata;
(transactionRepository as any)["queryRunnerProvider"] = queryRunnerProvider;
// todo: same code in the repository factory. probably better to use repository factory here too
try {
await queryRunner.beginTransaction();

View File

@ -18,14 +18,28 @@ export class RepositoryFactory {
* Creates a regular repository.
*/
createRepository(connection: Connection, metadata: EntityMetadata, queryRunnerProvider?: QueryRunnerProvider): Repository<any> {
return new Repository<any>(connection, metadata, queryRunnerProvider);
// 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 Repository<any>();
(repository as any)["connection"] = connection;
(repository as any)["metadata"] = metadata;
(repository as any)["queryRunnerProvider"] = queryRunnerProvider;
return repository;
}
/**
* Creates a tree repository.
*/
createTreeRepository(connection: Connection, metadata: EntityMetadata, queryRunnerProvider?: QueryRunnerProvider): TreeRepository<any> {
return new TreeRepository<any>(connection, metadata, queryRunnerProvider);
// 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>();
(repository as any)["connection"] = connection;
(repository as any)["metadata"] = metadata;
(repository as any)["queryRunnerProvider"] = queryRunnerProvider;
return repository;
}
/**

View File

@ -0,0 +1,12 @@
/**
* Thrown if custom repository inherits Repository class however entity is not set in @EntityRepository decorator.
*/
export class CustomRepositoryCannotInheritRepositoryError extends Error {
name = "CustomRepositoryCannotInheritRepositoryError";
constructor(repository: any) {
super(`Custom entity repository ${repository instanceof Function ? repository.name : repository.constructor.name} ` +
` cannot inherit Repository class without entity being set in the @EntityRepository decorator.`);
}
}

View File

@ -0,0 +1,12 @@
/**
* Thrown if custom repositories that extend AbstractRepository classes does not have managed entity.
*/
export class CustomRepositoryDoesNotHaveEntityError extends Error {
name = "CustomRepositoryDoesNotHaveEntityError";
constructor(repository: any) {
super(`Custom repository ${repository instanceof Function ? repository.name : repository.constructor.name} does not have managed entity. ` +
`Did you forget to specify entity for it @EntityRepository(MyEntity)? `);
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown if custom repository was not found.
*/
export class CustomRepositoryNotFoundError extends Error {
name = "CustomRepositoryNotFoundError";
constructor(repository: any) {
super();
this.message = `Custom repository ${repository instanceof Function ? repository.name : repository.constructor.name } was not found. ` +
`Did you forgot to put @EntityRepository decorator on it?`;
}
}

View File

@ -0,0 +1,13 @@
/**
* Thrown if same custom repository instance is reused between different connections.
*/
export class CustomRepositoryReusedError extends Error {
name = "CustomRepositoryReusedError";
constructor(repository: any) {
super(`Custom entity repository ${repository instanceof Function ? repository.name : repository.constructor.name} ` +
`was already used in the different connection. You can't share entity repositories between different connections ` +
`when useContainer is set to true for the entity repository.`);
}
}