From e84e6ff4c00f2e99c938e276cce7c41277c1244d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 4 Mar 2020 10:39:02 -0800 Subject: [PATCH 1/5] feat(repository): add functions to define repository classes --- .../define-repository-class.unit.ts | 166 +++++++++++++++ .../repository/src/define-repository-class.ts | 199 ++++++++++++++++++ packages/repository/src/index.ts | 1 + 3 files changed, 366 insertions(+) create mode 100644 packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts create mode 100644 packages/repository/src/define-repository-class.ts diff --git a/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts new file mode 100644 index 000000000000..a3a8adf63c58 --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts @@ -0,0 +1,166 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/context'; +import {expect} from '@loopback/testlab'; +import { + AndClause, + AnyObject, + Condition, + Count, + CrudRepository, + DataObject, + DefaultCrudRepository, + DefaultKeyValueRepository, + defineCrudRepositoryClass, + defineEntityCrudRepositoryClass, + defineKeyValueRepositoryClass, + Entity, + Filter, + juggler, + model, + Model, + OrClause, + property, +} from '../../..'; + +describe('RepositoryClass builder', () => { + describe('defineCrudRepositoryClass', () => { + it('should generate CRUD repository class', async () => { + const AddressRepository = defineCrudRepositoryClass( + Address, + DummyCrudRepository, + ); + // `CrudRepository.prototype.find` is inherited + expect(AddressRepository.prototype.find).to.be.a.Function(); + // `DummyCrudRepository.prototype.findByTitle` is inherited + expect(AddressRepository.prototype.findByTitle).to.be.a.Function(); + expect(AddressRepository.name).to.equal('AddressRepository'); + expect(Object.getPrototypeOf(AddressRepository)).to.equal( + DummyCrudRepository, + ); + }); + }); + + describe('defineEntityCrudRepositoryClass', () => { + it('should generate entity CRUD repository class', async () => { + const ProductRepository = defineEntityCrudRepositoryClass(Product); + + expect(ProductRepository.name).to.equal('ProductRepository'); + expect(ProductRepository.prototype.find).to.be.a.Function(); + expect(ProductRepository.prototype.findById).to.be.a.Function(); + expect(Object.getPrototypeOf(ProductRepository)).to.equal( + DefaultCrudRepository, + ); + }); + }); + + describe('defineEntityCrudRepositoryClass with custom base class', () => { + it('should generate entity CRUD repository class', async () => { + class BaseProductRepository extends DefaultCrudRepository< + Product, + number + > { + async findByName(name: string): Promise { + return this.find({where: {name}}); + } + } + + const ProductRepository = defineEntityCrudRepositoryClass( + Product, + BaseProductRepository, + ); + + expect(ProductRepository.name).to.equal('ProductRepository'); + expect(ProductRepository.prototype.find).to.be.a.Function(); + expect(ProductRepository.prototype.findById).to.be.a.Function(); + expect(Object.getPrototypeOf(ProductRepository)).to.equal( + BaseProductRepository, + ); + }); + }); + + describe('defineEntityKeyValueRepositoryClass', () => { + it('should generate key value repository class', async () => { + const ProductRepository = defineKeyValueRepositoryClass(Product); + + expect(ProductRepository.name).to.equal('ProductRepository'); + expect(ProductRepository.prototype.get).to.be.a.Function(); + expect(Object.getPrototypeOf(ProductRepository)).to.equal( + DefaultKeyValueRepository, + ); + }); + }); + + @model() + class Product extends Entity { + @property({id: true}) + id: number; + + @property() + name: string; + } + + @model() + class Address extends Model { + @property() + street: string; + + @property() + city: string; + + @property() + state: string; + } + + class DummyCrudRepository implements CrudRepository { + constructor( + private modelCtor: Constructor, + private dataSource: juggler.DataSource, + ) {} + create( + dataObject: DataObject, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + createAll( + dataObjects: DataObject[], + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + find( + filter?: Filter | undefined, + options?: AnyObject | undefined, + ): Promise<(M & {})[]> { + throw new Error('Method not implemented.'); + } + updateAll( + dataObject: DataObject, + where?: Condition | AndClause | OrClause | undefined, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + deleteAll( + where?: Condition | AndClause | OrClause | undefined, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + count( + where?: Condition | AndClause | OrClause | undefined, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + + // An extra method to verify it's available for the defined repo class + findByTitle(title: string): Promise { + throw new Error('Method not implemented.'); + } + } +}); diff --git a/packages/repository/src/define-repository-class.ts b/packages/repository/src/define-repository-class.ts new file mode 100644 index 000000000000..3dde4d7ded79 --- /dev/null +++ b/packages/repository/src/define-repository-class.ts @@ -0,0 +1,199 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/context'; +import assert from 'assert'; +import {Entity, Model} from './model'; +import { + CrudRepository, + DefaultCrudRepository, + DefaultKeyValueRepository, + EntityCrudRepository, + juggler, + KeyValueRepository, + Repository, +} from './repositories'; + +/** + * Signature for CrudRepository classes + * + * @typeParam M - Model class + * @typeParam R - Repository class/interface + */ +export interface CrudRepositoryClass< + M extends Model, + R extends CrudRepository +> { + /** + * The constructor for the generated repository class + * @param dataSource - DataSource object + */ + new (dataSource: juggler.DataSource): R; + prototype: R; +} + +/** + * Signature for EntityCrudRepository classes + * + * @typeParam E - An entity class + * @typeParam IdType - ID type for the entity + * @typeParam Relations - Relations for the entity + */ +export interface EntityCrudRepositoryClass< + E extends Entity, + IdType, + Relations extends object +> extends CrudRepositoryClass> {} + +/** + * Signature for repository classes that can be used as the base class for + * `define*` functions + * + * @typeParam M - Model class + * @typeParam R - Repository class/interface + */ +export interface BaseRepositoryClass> { + /** + * The constructor for the generated repository class + * @param modelClass - Model class + * @param dataSource - DataSource object + */ + new ( + // Make model class conditional of Model or Entity + modelClass: (M extends Entity ? typeof Entity : typeof Model) & { + prototype: M; + }, + dataSource: juggler.DataSource, + ): R; + prototype: R; +} +/** + * Create (define) a CRUD repository class for the given model. + * + * @example + * + * ```ts + * const AddressRepository = defineCrudRepositoryClass(Address); + * ``` + * + * @param modelClass - A model class such as `Address`. + * + * @typeParam M - Model class + * @typeParam R - CRUD Repository class/interface + */ +export function defineCrudRepositoryClass< + M extends Model, + R extends CrudRepository +>( + modelClass: Constructor & {prototype: M}, + baseRepositoryClass: BaseRepositoryClass, +): CrudRepositoryClass { + const repoName = modelClass.name + 'Repository'; + const defineNamedRepo = new Function( + 'ModelCtor', + 'BaseRepository', + `return class ${repoName} extends BaseRepository { + constructor(dataSource) { + super(ModelCtor, dataSource); + } + };`, + ); + + const repo = defineNamedRepo(modelClass, baseRepositoryClass); + assert.equal(repo.name, repoName); + return repo; +} + +/** + * Create (define) an entity CRUD repository class for the given model. + * + * @example + * + * ```ts + * const ProductRepository = defineEntityCrudRepositoryClass(Product); + * ``` + * + * @param entityClass - An entity class such as `Product`. + * @param baseRepositoryClass - Base repository class. Defaults to `DefaultCrudRepository` + * + * @typeParam E - An entity class + * @typeParam IdType - ID type for the entity + * @typeParam Relations - Relations for the entity + */ +export function defineEntityCrudRepositoryClass< + E extends Entity, + IdType, + Relations extends object = {} +>( + entityClass: Constructor & {prototype: E}, + baseRepositoryClass: BaseRepositoryClass< + E, + EntityCrudRepository + > = (DefaultCrudRepository as unknown) as BaseRepositoryClass< + E, + EntityCrudRepository + >, +): EntityCrudRepositoryClass { + return defineCrudRepositoryClass(entityClass, baseRepositoryClass); +} + +/** + * Create (define) a KeyValue repository class for the given entity. + * + * @example + * + * ```ts + * const ProductKeyValueRepository = defineKeyValueRepositoryClass(Product); + * ``` + * + * @param entityClass - An entity class such as `Product`. + * @param baseRepositoryClass - Base KeyValue repository class. + * Defaults to `DefaultKeyValueRepository` + * + * @typeParam M - Model class + * @typeParam R - KeyValueRepository class/interface + */ +export function defineKeyValueRepositoryClass< + M extends Model, + R extends KeyValueRepository = KeyValueRepository +>( + entityClass: Constructor & {prototype: M}, + baseRepositoryClass: BaseRepositoryClass< + M, + R + > = (DefaultKeyValueRepository as unknown) as BaseRepositoryClass, +): KeyValueRepositoryClass { + const repoName = entityClass.name + 'Repository'; + const defineNamedRepo = new Function( + 'EntityCtor', + 'BaseRepository', + `return class ${repoName} extends BaseRepository { + constructor(dataSource) { + super(EntityCtor, dataSource); + } + };`, + ); + + const repo = defineNamedRepo(entityClass, baseRepositoryClass); + assert.equal(repo.name, repoName); + return repo; +} + +/** + * Signature for KeyValueRepository classes + * + * @typeParam M - Model class + */ +export interface KeyValueRepositoryClass< + M extends Model, + R extends KeyValueRepository = KeyValueRepository +> { + /** + * The constructor for the generated key value repository class + * @param dataSource - DataSource object + */ + new (dataSource: juggler.DataSource): R; + prototype: KeyValueRepository; +} diff --git a/packages/repository/src/index.ts b/packages/repository/src/index.ts index 665e24185804..66e1d39a4df9 100644 --- a/packages/repository/src/index.ts +++ b/packages/repository/src/index.ts @@ -14,6 +14,7 @@ export * from './model'; export * from './query'; export * from './relations'; export * from './repositories'; +export * from './define-repository-class'; export * from './transaction'; export * from './type-resolver'; export * from './types'; From 36d2a1b4b919c0c57389c8b26eb9a6fef8bf735f Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 5 Mar 2020 10:03:55 -0800 Subject: [PATCH 2/5] feat(rest-crud): use functions from @loopback/repository to define repository classes --- packages/rest-crud/README.md | 20 ++++--- .../default-model-crud-rest.acceptance.ts | 5 +- .../unit/define-crud-repository-class.unit.ts | 22 -------- .../rest-crud/src/crud-rest.api-builder.ts | 5 +- packages/rest-crud/src/index.ts | 4 +- packages/rest-crud/src/repository-builder.ts | 55 ------------------- 6 files changed, 21 insertions(+), 90 deletions(-) delete mode 100644 packages/rest-crud/src/__tests__/unit/define-crud-repository-class.unit.ts delete mode 100644 packages/rest-crud/src/repository-builder.ts diff --git a/packages/rest-crud/README.md b/packages/rest-crud/README.md index 77b1d4c3ec34..8d61cc1f60ce 100644 --- a/packages/rest-crud/README.md +++ b/packages/rest-crud/README.md @@ -64,9 +64,9 @@ class defined without the need for a repository or controller class file. If you would like more flexibility, e.g. if you would only like to define a default `CrudRest` controller or repository, you can use the two helper methods -(`defineCrudRestController` and `defineCrudRepositoryClass`) exposed from -`@loopback/rest-crud`. These functions will help you create controllers and -respositories using code. +(`defineCrudRestController` from `@loopback/rest-crud` and +`defineEntityCrudRepositoryClass` from `@loopback/repository`). These functions +will help you create controllers and repositories using code. For the examples in the following sections, we are also assuming a model named `Product`, and a datasource named `db` have already been created. @@ -100,24 +100,28 @@ endpoints of an existing model with a respository. ### Creating a CRUD repository -Use the `defineCrudRepositoryClass` method to create named repositories (based -on the Model) for your app. +Use the `defineEntityCrudRepositoryClass` method to create named repositories +(based on the Model) for your app. Usage example: ```ts -const ProductRepository = defineCrudRepositoryClass(Product); +import {defineEntityCrudRepositoryClass} from '@loopback/repository'; + +const ProductRepository = defineEntityCrudRepositoryClass(Product); this.repository(ProductRepository); inject('datasources.db')(ProductRepository, undefined, 0); ``` ### Integrated example -Here is an example of an app which uses `defineCrudRepositoryClass` and +Here is an example of an app which uses `defineEntityCrudRepositoryClass` and `defineCrudRestController` to fulfill its repository and controller requirements. ```ts +import {defineEntityCrudRepositoryClass} from '@loopback/repository'; + export class TryApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { @@ -128,7 +132,7 @@ export class TryApplication extends BootMixin( async boot(): Promise { await super.boot(); - const ProductRepository = defineCrudRepositoryClass(Product); + const ProductRepository = defineEntityCrudRepositoryClass(Product); const repoBinding = this.repository(ProductRepository); inject('datasources.db')(ProductRepository, undefined, 0); diff --git a/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts b/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts index 1701390d1dd7..7379b50f2798 100644 --- a/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts +++ b/packages/rest-crud/src/__tests__/acceptance/default-model-crud-rest.acceptance.ts @@ -4,6 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import { + defineEntityCrudRepositoryClass, Entity, EntityCrudRepository, juggler, @@ -18,7 +19,7 @@ import { givenHttpServerConfig, toJSON, } from '@loopback/testlab'; -import {defineCrudRepositoryClass, defineCrudRestController} from '../..'; +import {defineCrudRestController} from '../..'; // In this test scenario, we create a product with a required & an optional // property and use the default model settings (strict mode, forceId). @@ -287,7 +288,7 @@ describe('CrudRestController for a simple Product model', () => { async function setupTestScenario() { const db = new juggler.DataSource({connector: 'memory'}); - const ProductRepository = defineCrudRepositoryClass(Product); + const ProductRepository = defineEntityCrudRepositoryClass(Product); repo = new ProductRepository(db); diff --git a/packages/rest-crud/src/__tests__/unit/define-crud-repository-class.unit.ts b/packages/rest-crud/src/__tests__/unit/define-crud-repository-class.unit.ts deleted file mode 100644 index c8b471f7e7d6..000000000000 --- a/packages/rest-crud/src/__tests__/unit/define-crud-repository-class.unit.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/rest-crud -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {Entity, model, property} from '@loopback/repository'; -import {expect} from '@loopback/testlab'; -import {defineCrudRepositoryClass} from '../..'; - -describe('defineCrudRepositoryClass', () => { - it('should generate repository based on Model name', async () => { - @model() - class Product extends Entity { - @property({id: true}) - id: number; - } - - const ProductRepository = defineCrudRepositoryClass(Product); - - expect(ProductRepository.name).to.equal('ProductRepository'); - }); -}); diff --git a/packages/rest-crud/src/crud-rest.api-builder.ts b/packages/rest-crud/src/crud-rest.api-builder.ts index c16625cf7e25..370537870d2b 100644 --- a/packages/rest-crud/src/crud-rest.api-builder.ts +++ b/packages/rest-crud/src/crud-rest.api-builder.ts @@ -18,12 +18,13 @@ import { import { ApplicationWithRepositories, Class, + defineEntityCrudRepositoryClass, Entity, EntityCrudRepository, } from '@loopback/repository'; import {Model} from '@loopback/rest'; import debugFactory from 'debug'; -import {defineCrudRepositoryClass, defineCrudRestController} from '.'; +import {defineCrudRestController} from '.'; const debug = debugFactory('loopback:boot:crud-rest'); @@ -86,7 +87,7 @@ function setupCrudRepository( entityClass: typeof Entity & {prototype: Entity}, config: ModelCrudRestApiConfig, ): Class> { - const repositoryClass = defineCrudRepositoryClass(entityClass); + const repositoryClass = defineEntityCrudRepositoryClass(entityClass); injectFirstConstructorArg( repositoryClass, diff --git a/packages/rest-crud/src/index.ts b/packages/rest-crud/src/index.ts index 33130144140d..482acd247ac8 100644 --- a/packages/rest-crud/src/index.ts +++ b/packages/rest-crud/src/index.ts @@ -3,7 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +// Reexport `defineEntityCrudRepositoryClass` from `@loopback/repository` as +// `defineCrudRepositoryClass` for backward-compatibility +export {defineEntityCrudRepositoryClass as defineCrudRepositoryClass} from '@loopback/repository'; export * from './crud-rest.api-builder'; export * from './crud-rest.component'; export * from './crud-rest.controller'; -export * from './repository-builder'; diff --git a/packages/rest-crud/src/repository-builder.ts b/packages/rest-crud/src/repository-builder.ts deleted file mode 100644 index f3ea30213c76..000000000000 --- a/packages/rest-crud/src/repository-builder.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/rest-crud -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import { - DefaultCrudRepository, - Entity, - EntityCrudRepository, - juggler, -} from '@loopback/repository'; -import assert from 'assert'; - -/** - * Create (define) a repository class for the given model. - * - * @example - * - * ```ts - * const ProductRepository = defineCrudRepositoryClass(Product); - * ``` - * - * @param modelCtor A model class, e.g. `Product`. - */ -export function defineCrudRepositoryClass< - T extends Entity, - IdType, - Relations extends object = {} ->( - entityClass: typeof Entity & {prototype: T}, -): RepositoryClass { - const repoName = entityClass.name + 'Repository'; - const defineNamedRepo = new Function( - 'EntityCtor', - 'BaseRepository', - `return class ${repoName} extends BaseRepository { - constructor(dataSource) { - super(EntityCtor, dataSource); - } - };`, - ); - - // TODO(bajtos) make DefaultCrudRepository configurable (?) - const repo = defineNamedRepo(entityClass, DefaultCrudRepository); - assert.equal(repo.name, repoName); - return repo; -} - -export interface RepositoryClass< - T extends Entity, - IdType, - Relations extends object -> { - new (ds: juggler.DataSource): EntityCrudRepository; -} From 90930c0e33c622dd7d7b1ebd149360382e82b7be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 23 Mar 2020 09:51:40 +0100 Subject: [PATCH 3/5] fixup! refactor generic types in define-repository-class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of accepting Model/Entity instance and dealing with Constructor complexity, which requires additional type casts, I am proposing to rework `BaseRepositoryClass` to accept `typeof Model` instead. This way the generic argument can capture: 1. class constructor 2. static members 3. prototype (instance) members Signed-off-by: Miroslav Bajtoš --- .../repository/src/define-repository-class.ts | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/repository/src/define-repository-class.ts b/packages/repository/src/define-repository-class.ts index 3dde4d7ded79..157729be18d1 100644 --- a/packages/repository/src/define-repository-class.ts +++ b/packages/repository/src/define-repository-class.ts @@ -3,8 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Constructor} from '@loopback/context'; import assert from 'assert'; +import {PrototypeOf} from './common-types'; import {Entity, Model} from './model'; import { CrudRepository, @@ -54,19 +54,16 @@ export interface EntityCrudRepositoryClass< * @typeParam M - Model class * @typeParam R - Repository class/interface */ -export interface BaseRepositoryClass> { +export interface BaseRepositoryClass< + M extends typeof Model, + R extends Repository> +> { /** * The constructor for the generated repository class * @param modelClass - Model class * @param dataSource - DataSource object */ - new ( - // Make model class conditional of Model or Entity - modelClass: (M extends Entity ? typeof Entity : typeof Model) & { - prototype: M; - }, - dataSource: juggler.DataSource, - ): R; + new (modelClass: M, dataSource: juggler.DataSource): R; prototype: R; } /** @@ -84,12 +81,12 @@ export interface BaseRepositoryClass> { * @typeParam R - CRUD Repository class/interface */ export function defineCrudRepositoryClass< - M extends Model, - R extends CrudRepository + M extends typeof Model, + R extends CrudRepository> >( - modelClass: Constructor & {prototype: M}, + modelClass: M, baseRepositoryClass: BaseRepositoryClass, -): CrudRepositoryClass { +): CrudRepositoryClass, R> { const repoName = modelClass.name + 'Repository'; const defineNamedRepo = new Function( 'ModelCtor', @@ -123,19 +120,19 @@ export function defineCrudRepositoryClass< * @typeParam Relations - Relations for the entity */ export function defineEntityCrudRepositoryClass< - E extends Entity, + E extends typeof Entity, IdType, Relations extends object = {} >( - entityClass: Constructor & {prototype: E}, + entityClass: E, baseRepositoryClass: BaseRepositoryClass< E, - EntityCrudRepository - > = (DefaultCrudRepository as unknown) as BaseRepositoryClass< + EntityCrudRepository, IdType, Relations> + > = DefaultCrudRepository as BaseRepositoryClass< E, - EntityCrudRepository + EntityCrudRepository, IdType, Relations> >, -): EntityCrudRepositoryClass { +): EntityCrudRepositoryClass, IdType, Relations> { return defineCrudRepositoryClass(entityClass, baseRepositoryClass); } @@ -148,7 +145,7 @@ export function defineEntityCrudRepositoryClass< * const ProductKeyValueRepository = defineKeyValueRepositoryClass(Product); * ``` * - * @param entityClass - An entity class such as `Product`. + * @param modelClass - An entity class such as `Product`. * @param baseRepositoryClass - Base KeyValue repository class. * Defaults to `DefaultKeyValueRepository` * @@ -156,15 +153,15 @@ export function defineEntityCrudRepositoryClass< * @typeParam R - KeyValueRepository class/interface */ export function defineKeyValueRepositoryClass< - M extends Model, - R extends KeyValueRepository = KeyValueRepository + M extends typeof Model, + R extends KeyValueRepository> >( - entityClass: Constructor & {prototype: M}, + entityClass: M, baseRepositoryClass: BaseRepositoryClass< M, R > = (DefaultKeyValueRepository as unknown) as BaseRepositoryClass, -): KeyValueRepositoryClass { +): KeyValueRepositoryClass, R> { const repoName = entityClass.name + 'Repository'; const defineNamedRepo = new Function( 'EntityCtor', From ccca433a65ca76b8350136e2151815175893a918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 23 Mar 2020 10:34:43 +0100 Subject: [PATCH 4/5] fixup! add support for custom repository methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- .../define-repository-class.unit.ts | 34 +++++++++++++++---- .../repository/src/define-repository-class.ts | 14 ++++---- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts index a3a8adf63c58..e26e672932b3 100644 --- a/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts @@ -3,7 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Constructor} from '@loopback/context'; import {expect} from '@loopback/testlab'; import { AndClause, @@ -29,10 +28,10 @@ import { describe('RepositoryClass builder', () => { describe('defineCrudRepositoryClass', () => { it('should generate CRUD repository class', async () => { - const AddressRepository = defineCrudRepositoryClass( - Address, - DummyCrudRepository, - ); + const AddressRepository = defineCrudRepositoryClass< + typeof Address, + DummyCrudRepository
+ >(Address, DummyCrudRepository); // `CrudRepository.prototype.find` is inherited expect(AddressRepository.prototype.find).to.be.a.Function(); // `DummyCrudRepository.prototype.findByTitle` is inherited @@ -76,13 +75,14 @@ describe('RepositoryClass builder', () => { expect(ProductRepository.name).to.equal('ProductRepository'); expect(ProductRepository.prototype.find).to.be.a.Function(); expect(ProductRepository.prototype.findById).to.be.a.Function(); + expect(ProductRepository.prototype.findByName).to.be.a.Function(); expect(Object.getPrototypeOf(ProductRepository)).to.equal( BaseProductRepository, ); }); }); - describe('defineEntityKeyValueRepositoryClass', () => { + describe('defineKeyValueRepositoryClass', () => { it('should generate key value repository class', async () => { const ProductRepository = defineKeyValueRepositoryClass(Product); @@ -92,6 +92,26 @@ describe('RepositoryClass builder', () => { DefaultKeyValueRepository, ); }); + + it('supports custom base repository class', () => { + class MyKeyValueRepo extends DefaultKeyValueRepository< + M + > { + async getByName(name: string): Promise { + throw new Error('not implemented'); + } + } + + const ProductRepository = defineKeyValueRepositoryClass< + typeof Product, + MyKeyValueRepo + >(Product, MyKeyValueRepo); + + expect(ProductRepository.name).to.equal('ProductRepository'); + expect(ProductRepository.prototype.get).to.be.a.Function(); + expect(ProductRepository.prototype.getByName).to.be.a.Function(); + expect(Object.getPrototypeOf(ProductRepository)).to.equal(MyKeyValueRepo); + }); }); @model() @@ -117,7 +137,7 @@ describe('RepositoryClass builder', () => { class DummyCrudRepository implements CrudRepository { constructor( - private modelCtor: Constructor, + private modelCtor: typeof Model & {prototype: M}, private dataSource: juggler.DataSource, ) {} create( diff --git a/packages/repository/src/define-repository-class.ts b/packages/repository/src/define-repository-class.ts index 157729be18d1..c6e33cda1939 100644 --- a/packages/repository/src/define-repository-class.ts +++ b/packages/repository/src/define-repository-class.ts @@ -122,17 +122,15 @@ export function defineCrudRepositoryClass< export function defineEntityCrudRepositoryClass< E extends typeof Entity, IdType, - Relations extends object = {} + Relations extends object, + R extends EntityCrudRepository, IdType, Relations> >( entityClass: E, baseRepositoryClass: BaseRepositoryClass< E, - EntityCrudRepository, IdType, Relations> - > = DefaultCrudRepository as BaseRepositoryClass< - E, - EntityCrudRepository, IdType, Relations> - >, -): EntityCrudRepositoryClass, IdType, Relations> { + R + > = (DefaultCrudRepository as unknown) as BaseRepositoryClass, +): CrudRepositoryClass, R> { return defineCrudRepositoryClass(entityClass, baseRepositoryClass); } @@ -192,5 +190,5 @@ export interface KeyValueRepositoryClass< * @param dataSource - DataSource object */ new (dataSource: juggler.DataSource): R; - prototype: KeyValueRepository; + prototype: R; } From b8e6a044feda6bd2285160c7d8547dbf93407103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Mar 2020 15:18:30 +0200 Subject: [PATCH 5/5] feat: simplify the API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miroslav Bajtoš --- .../define-repository-class.unit.ts | 64 +------- .../repository/src/define-repository-class.ts | 144 ++++++++---------- 2 files changed, 68 insertions(+), 140 deletions(-) diff --git a/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts index e26e672932b3..3251773dfc16 100644 --- a/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts @@ -5,30 +5,28 @@ import {expect} from '@loopback/testlab'; import { - AndClause, AnyObject, - Condition, Count, CrudRepository, DataObject, DefaultCrudRepository, DefaultKeyValueRepository, - defineCrudRepositoryClass, defineEntityCrudRepositoryClass, defineKeyValueRepositoryClass, + defineRepositoryClass, Entity, Filter, juggler, model, Model, - OrClause, property, + Where, } from '../../..'; describe('RepositoryClass builder', () => { - describe('defineCrudRepositoryClass', () => { - it('should generate CRUD repository class', async () => { - const AddressRepository = defineCrudRepositoryClass< + describe('defineRepositoryClass', () => { + it('should generate custom repository class', async () => { + const AddressRepository = defineRepositoryClass< typeof Address, DummyCrudRepository
>(Address, DummyCrudRepository); @@ -56,32 +54,6 @@ describe('RepositoryClass builder', () => { }); }); - describe('defineEntityCrudRepositoryClass with custom base class', () => { - it('should generate entity CRUD repository class', async () => { - class BaseProductRepository extends DefaultCrudRepository< - Product, - number - > { - async findByName(name: string): Promise { - return this.find({where: {name}}); - } - } - - const ProductRepository = defineEntityCrudRepositoryClass( - Product, - BaseProductRepository, - ); - - expect(ProductRepository.name).to.equal('ProductRepository'); - expect(ProductRepository.prototype.find).to.be.a.Function(); - expect(ProductRepository.prototype.findById).to.be.a.Function(); - expect(ProductRepository.prototype.findByName).to.be.a.Function(); - expect(Object.getPrototypeOf(ProductRepository)).to.equal( - BaseProductRepository, - ); - }); - }); - describe('defineKeyValueRepositoryClass', () => { it('should generate key value repository class', async () => { const ProductRepository = defineKeyValueRepositoryClass(Product); @@ -92,26 +64,6 @@ describe('RepositoryClass builder', () => { DefaultKeyValueRepository, ); }); - - it('supports custom base repository class', () => { - class MyKeyValueRepo extends DefaultKeyValueRepository< - M - > { - async getByName(name: string): Promise { - throw new Error('not implemented'); - } - } - - const ProductRepository = defineKeyValueRepositoryClass< - typeof Product, - MyKeyValueRepo - >(Product, MyKeyValueRepo); - - expect(ProductRepository.name).to.equal('ProductRepository'); - expect(ProductRepository.prototype.get).to.be.a.Function(); - expect(ProductRepository.prototype.getByName).to.be.a.Function(); - expect(Object.getPrototypeOf(ProductRepository)).to.equal(MyKeyValueRepo); - }); }); @model() @@ -160,19 +112,19 @@ describe('RepositoryClass builder', () => { } updateAll( dataObject: DataObject, - where?: Condition | AndClause | OrClause | undefined, + where?: Where | undefined, options?: AnyObject | undefined, ): Promise { throw new Error('Method not implemented.'); } deleteAll( - where?: Condition | AndClause | OrClause | undefined, + where?: Where | undefined, options?: AnyObject | undefined, ): Promise { throw new Error('Method not implemented.'); } count( - where?: Condition | AndClause | OrClause | undefined, + where?: Where | undefined, options?: AnyObject | undefined, ): Promise { throw new Error('Method not implemented.'); diff --git a/packages/repository/src/define-repository-class.ts b/packages/repository/src/define-repository-class.ts index c6e33cda1939..fb85887be78d 100644 --- a/packages/repository/src/define-repository-class.ts +++ b/packages/repository/src/define-repository-class.ts @@ -7,24 +7,24 @@ import assert from 'assert'; import {PrototypeOf} from './common-types'; import {Entity, Model} from './model'; import { - CrudRepository, DefaultCrudRepository, DefaultKeyValueRepository, - EntityCrudRepository, juggler, - KeyValueRepository, Repository, } from './repositories'; /** - * Signature for CrudRepository classes + * Signature for a Repository class bound to a given model. The constructor + * accepts only the dataSource to use for persistence. + * + * `define*` functions return a class implementing this interface. * * @typeParam M - Model class * @typeParam R - Repository class/interface */ -export interface CrudRepositoryClass< +export interface NamedRepositoryClass< M extends Model, - R extends CrudRepository + R extends Repository > { /** * The constructor for the generated repository class @@ -34,24 +34,20 @@ export interface CrudRepositoryClass< prototype: R; } -/** - * Signature for EntityCrudRepository classes - * - * @typeParam E - An entity class - * @typeParam IdType - ID type for the entity - * @typeParam Relations - Relations for the entity - */ -export interface EntityCrudRepositoryClass< - E extends Entity, - IdType, - Relations extends object -> extends CrudRepositoryClass> {} - /** * Signature for repository classes that can be used as the base class for - * `define*` functions + * `define*` functions. The constructor of a base repository class accepts + * the target model constructor and the datasource to use. + * + * `define*` functions require a class implementing this interface on input. + * + * @typeParam M - Model class constructor, e.g `typeof Model`. + * **❗️IMPORTANT: The type argument `M` is describing the model constructor type + * (e.g. `typeof Model`), not the model instance type (`Model`) as is the case + * in other repository-related types. The constructor type is required + * to support custom repository classes requiring a Model subclass in the + * constructor arguments, e.g. `Entity` or a user-provided model.** * - * @typeParam M - Model class * @typeParam R - Repository class/interface */ export interface BaseRepositoryClass< @@ -66,27 +62,45 @@ export interface BaseRepositoryClass< new (modelClass: M, dataSource: juggler.DataSource): R; prototype: R; } + /** - * Create (define) a CRUD repository class for the given model. + * Create (define) a repository class for the given model. + * + * See also `defineEntityCrudRepositoryClass` and `defineKeyValueRepositoryClass` + * for convenience wrappers providing repository class factory for the default + * CRUD and KeyValue implementations. + * + * **❗️IMPORTANT: The compiler (TypeScript 3.8) is not able to correctly infer + * generic arguments `M` and `R` from the class constructors provided in + * function arguments. You must always provide both M and R types explicitly.** * * @example * * ```ts - * const AddressRepository = defineCrudRepositoryClass(Address); + * const AddressRepository = defineRepositoryClass< + * typeof Address, + * DefaultEntityCrudRepository< + * Address, + * typeof Address.prototype.id, + * AddressRelations + * >, + * >(Address, DefaultCrudRepository); * ``` * * @param modelClass - A model class such as `Address`. + * @param baseRepositoryClass - Repository implementation to use as the base, + * e.g. `DefaultCrudRepository`. * - * @typeParam M - Model class - * @typeParam R - CRUD Repository class/interface + * @typeParam M - Model class constructor (e.g. `typeof Address`) + * @typeParam R - Repository class (e.g. `DefaultCrudRepository`) */ -export function defineCrudRepositoryClass< +export function defineRepositoryClass< M extends typeof Model, - R extends CrudRepository> + R extends Repository> >( modelClass: M, baseRepositoryClass: BaseRepositoryClass, -): CrudRepositoryClass, R> { +): NamedRepositoryClass, R> { const repoName = modelClass.name + 'Repository'; const defineNamedRepo = new Function( 'ModelCtor', @@ -105,37 +119,39 @@ export function defineCrudRepositoryClass< /** * Create (define) an entity CRUD repository class for the given model. + * This function always uses `DefaultCrudRepository` as the base class, + * use `defineRepositoryClass` if you want to use your own base repository. * * @example * * ```ts - * const ProductRepository = defineEntityCrudRepositoryClass(Product); + * const ProductRepository = defineEntityCrudRepositoryClass< + * Product, + * typeof Product.prototype.id, + * ProductRelations + * >(Product); * ``` * * @param entityClass - An entity class such as `Product`. - * @param baseRepositoryClass - Base repository class. Defaults to `DefaultCrudRepository` * * @typeParam E - An entity class * @typeParam IdType - ID type for the entity * @typeParam Relations - Relations for the entity */ export function defineEntityCrudRepositoryClass< - E extends typeof Entity, + E extends Entity, IdType, - Relations extends object, - R extends EntityCrudRepository, IdType, Relations> + Relations extends object >( - entityClass: E, - baseRepositoryClass: BaseRepositoryClass< - E, - R - > = (DefaultCrudRepository as unknown) as BaseRepositoryClass, -): CrudRepositoryClass, R> { - return defineCrudRepositoryClass(entityClass, baseRepositoryClass); + entityClass: typeof Entity & {prototype: E}, +): NamedRepositoryClass> { + return defineRepositoryClass(entityClass, DefaultCrudRepository); } /** * Create (define) a KeyValue repository class for the given entity. + * This function always uses `DefaultKeyValueRepository` as the base class, + * use `defineRepositoryClass` if you want to use your own base repository. * * @example * @@ -144,51 +160,11 @@ export function defineEntityCrudRepositoryClass< * ``` * * @param modelClass - An entity class such as `Product`. - * @param baseRepositoryClass - Base KeyValue repository class. - * Defaults to `DefaultKeyValueRepository` - * - * @typeParam M - Model class - * @typeParam R - KeyValueRepository class/interface - */ -export function defineKeyValueRepositoryClass< - M extends typeof Model, - R extends KeyValueRepository> ->( - entityClass: M, - baseRepositoryClass: BaseRepositoryClass< - M, - R - > = (DefaultKeyValueRepository as unknown) as BaseRepositoryClass, -): KeyValueRepositoryClass, R> { - const repoName = entityClass.name + 'Repository'; - const defineNamedRepo = new Function( - 'EntityCtor', - 'BaseRepository', - `return class ${repoName} extends BaseRepository { - constructor(dataSource) { - super(EntityCtor, dataSource); - } - };`, - ); - - const repo = defineNamedRepo(entityClass, baseRepositoryClass); - assert.equal(repo.name, repoName); - return repo; -} - -/** - * Signature for KeyValueRepository classes * * @typeParam M - Model class */ -export interface KeyValueRepositoryClass< - M extends Model, - R extends KeyValueRepository = KeyValueRepository -> { - /** - * The constructor for the generated key value repository class - * @param dataSource - DataSource object - */ - new (dataSource: juggler.DataSource): R; - prototype: R; +export function defineKeyValueRepositoryClass( + modelClass: typeof Model & {prototype: M}, +): NamedRepositoryClass> { + return defineRepositoryClass(modelClass, DefaultKeyValueRepository); }