From faa02407834add81f4ecc55bc6696f690aa4422c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 7 Apr 2020 18:28:37 +0200 Subject: [PATCH 1/2] feat(repository): generic factory for repository classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `defineRepositoryClass`, a generic variant of `defineCrudRepositoryClass` that can be used for any base repository implementation. Implement two convenience wrappers for creating default CRUD/KeyValue repository classes: - `defineCrudRepositoryClass` - `defineKeyValueRepositoryClass` Co-authored-by: Miroslav Bajtoš --- .../define-repository-class.unit.ts | 138 ++++++++++++++ .../repository/src/define-repository-class.ts | 170 ++++++++++++++++++ packages/repository/src/index.ts | 1 + 3 files changed, 309 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..a2e26deb1bf3 --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/define-repository-class.unit.ts @@ -0,0 +1,138 @@ +// 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 {expect} from '@loopback/testlab'; +import { + AnyObject, + Count, + CrudRepository, + DataObject, + DefaultCrudRepository, + DefaultKeyValueRepository, + defineCrudRepositoryClass, + defineKeyValueRepositoryClass, + defineRepositoryClass, + Entity, + Filter, + juggler, + model, + Model, + property, + Where, +} from '../../..'; + +describe('RepositoryClass builder', () => { + describe('defineRepositoryClass', () => { + it('should generate custom repository class', async () => { + const AddressRepository = defineRepositoryClass< + typeof Address, + DummyCrudRepository
+ >(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('defineCrudRepositoryClass', () => { + it('should generate entity CRUD repository class', async () => { + const ProductRepository = defineCrudRepositoryClass(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('defineKeyValueRepositoryClass', () => { + 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: typeof Model & {prototype: M}, + 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?: Where | undefined, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + deleteAll( + where?: Where | undefined, + options?: AnyObject | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + count( + where?: Where | 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..cf0d66414b63 --- /dev/null +++ b/packages/repository/src/define-repository-class.ts @@ -0,0 +1,170 @@ +// 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 assert from 'assert'; +import {PrototypeOf} from './common-types'; +import {Entity, Model} from './model'; +import { + DefaultCrudRepository, + DefaultKeyValueRepository, + juggler, + Repository, +} from './repositories'; + +/** + * 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 ModelRepositoryClass< + M extends Model, + R extends Repository +> { + /** + * The constructor for the generated repository class + * @param dataSource - DataSource object + */ + new (dataSource: juggler.DataSource): R; + prototype: R; +} + +/** + * Signature for repository classes that can be used as the base class for + * `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 R - Repository class/interface + */ +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 (modelClass: M, dataSource: juggler.DataSource): R; + prototype: R; +} + +/** + * Create (define) a repository class for the given model. + * + * See also `defineCrudRepositoryClass` 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 = 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 constructor (e.g. `typeof Address`) + * @typeParam R - Repository class (e.g. `DefaultCrudRepository`) + */ +export function defineRepositoryClass< + M extends typeof Model, + R extends Repository> +>( + modelClass: M, + baseRepositoryClass: BaseRepositoryClass, +): ModelRepositoryClass, R> { + 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. + * This function always uses `DefaultCrudRepository` as the base class, + * use `defineRepositoryClass` if you want to use your own base repository. + * + * @example + * + * ```ts + * const ProductRepository = defineCrudRepositoryClass< + * Product, + * typeof Product.prototype.id, + * ProductRelations + * >(Product); + * ``` + * + * @param entityClass - An entity class such as `Product`. + * + * @typeParam E - An entity class + * @typeParam IdType - ID type for the entity + * @typeParam Relations - Relations for the entity + */ +export function defineCrudRepositoryClass< + E extends Entity, + IdType, + Relations extends object +>( + entityClass: typeof Entity & {prototype: E}, +): ModelRepositoryClass> { + 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 + * + * ```ts + * const ProductKeyValueRepository = defineKeyValueRepositoryClass(Product); + * ``` + * + * @param modelClass - An entity class such as `Product`. + * + * @typeParam M - Model class + */ +export function defineKeyValueRepositoryClass( + modelClass: typeof Model & {prototype: M}, +): ModelRepositoryClass> { + return defineRepositoryClass(modelClass, DefaultKeyValueRepository); +} diff --git a/packages/repository/src/index.ts b/packages/repository/src/index.ts index 51f62b96adbb..420c28b19cc1 100644 --- a/packages/repository/src/index.ts +++ b/packages/repository/src/index.ts @@ -30,6 +30,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 aaf77df2f7ab80861962ad30b5d14922355b388b Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 7 Apr 2020 18:29:41 +0200 Subject: [PATCH 2/2] refactor(rest-crud): use `defineCrudRepositoryClass` from `@loopback/repository` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop our own implementation in favor of the new generic version. Co-authored-by: Miroslav Bajtoš --- packages/rest-crud/README.md | 10 +++- .../default-model-crud-rest.acceptance.ts | 3 +- .../rest-crud/src/crud-rest.api-builder.ts | 3 +- packages/rest-crud/src/index.ts | 3 +- packages/rest-crud/src/repository-builder.ts | 55 ------------------- 5 files changed, 13 insertions(+), 61 deletions(-) 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..b3e2eae2ac2e 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 +`defineCrudRepositoryClass` 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. @@ -106,6 +106,8 @@ on the Model) for your app. Usage example: ```ts +import {defineCrudRepositoryClass} from '@loopback/repository'; + const ProductRepository = defineCrudRepositoryClass(Product); this.repository(ProductRepository); inject('datasources.db')(ProductRepository, undefined, 0); @@ -118,6 +120,8 @@ Here is an example of an app which uses `defineCrudRepositoryClass` and requirements. ```ts +import {defineCrudRepositoryClass} from '@loopback/repository'; + export class TryApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { 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 19429ca7185d..b409babbe794 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 { + defineCrudRepositoryClass, 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). diff --git a/packages/rest-crud/src/crud-rest.api-builder.ts b/packages/rest-crud/src/crud-rest.api-builder.ts index c16625cf7e25..e211676bf0b3 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, + defineCrudRepositoryClass, 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'); diff --git a/packages/rest-crud/src/index.ts b/packages/rest-crud/src/index.ts index dc9318c2f8c5..3943ece3349b 100644 --- a/packages/rest-crud/src/index.ts +++ b/packages/rest-crud/src/index.ts @@ -13,7 +13,8 @@ * @packageDocumentation */ +// Re-export `defineCrudRepositoryClass` for backward-compatibility +export {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 92c422250a8b..000000000000 --- a/packages/rest-crud/src/repository-builder.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright IBM Corp. 2019,2020. 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; -}