From 3db07eb556c5462b5ac169dd2eae9b5db59bfaf4 Mon Sep 17 00:00:00 2001 From: shimks Date: Thu, 29 Mar 2018 14:04:23 -0400 Subject: [PATCH] feat(repository): have @repository take in constructor as arg --- docs/site/Controller-generator.md | 5 +- docs/site/Controllers.md | 4 +- docs/site/Decorators.md | 3 +- docs/site/Repositories.md | 4 +- docs/site/todo-tutorial-controller.md | 6 +- .../todo/src/controllers/todo.controller.ts | 7 +- .../controller-rest-template.ts.ejs | 5 +- packages/cli/test/controller.js | 5 +- packages/repository/README.md | 2 +- .../repository/src/decorators/repository.ts | 98 ++++++++++++---- .../test/unit/decorator/repository-with-di.ts | 105 +++++++++++------- 11 files changed, 157 insertions(+), 87 deletions(-) diff --git a/docs/site/Controller-generator.md b/docs/site/Controller-generator.md index 55db82506101..ec0668d85a20 100644 --- a/docs/site/Controller-generator.md +++ b/docs/site/Controller-generator.md @@ -76,7 +76,7 @@ Here's an example of what the template will produce given a `Todo` model and a `TodoRepository`: ```ts -import {Filter, Where} from '@loopback/repository'; +import {Filter, Where, repository} from '@loopback/repository'; import { post, param, @@ -86,13 +86,12 @@ import { del, requestBody } from '@loopback/rest'; -import {inject} from '@loopback/context'; import {Todo} from '../models'; import {TodoRepository} from '../repositories'; export class TodoController { constructor( - @inject('repositories.TodoRepository') + @repository(TodoRepository) public todoRepository: TodoRepository, ) {} diff --git a/docs/site/Controllers.md b/docs/site/Controllers.md index ab0af2083ddd..4c081c6ec548 100644 --- a/docs/site/Controllers.md +++ b/docs/site/Controllers.md @@ -195,7 +195,7 @@ import {repository} from '@loopback/repository'; export class HelloController { constructor( - @repository(HelloRepository.name) protected repository: HelloRepository, + @repository(HelloRepository) protected repository: HelloRepository, ) {} // returns a list of our objects @@ -273,7 +273,7 @@ import {repository} from '@loopback/repository'; export class HelloController { constructor( - @repository(HelloRepository.name) protected repo: HelloRepository, + @repository(HelloRepository) protected repo: HelloRepository, ) {} // returns a list of our objects diff --git a/docs/site/Decorators.md b/docs/site/Decorators.md index db0922258f21..0942cfa4ad29 100644 --- a/docs/site/Decorators.md +++ b/docs/site/Decorators.md @@ -575,7 +575,8 @@ For usage examples, see [Define Models](Repositories.md#define-models) ### Repository Decorator Syntax: -[`@repository(model: string | typeof Entity, dataSource?: string | juggler.DataSource)`](http://apidocs.loopback.io/@loopback%2frepository/#1503) + +[@repository(modelOrRepo: string | Class> | typeof Entity, dataSource?: string | juggler.DataSource)](http://apidocs.loopback.io/@loopback%2frepository/#1503) This decorator either injects an existing repository or creates a repository from a model and a datasource. diff --git a/docs/site/Repositories.md b/docs/site/Repositories.md index 6a8eb03e69c8..54939c90e8dc 100644 --- a/docs/site/Repositories.md +++ b/docs/site/Repositories.md @@ -155,7 +155,7 @@ DataSource for in the constructor of your controller class as follows: ```ts export class AccountController { constructor( - @repository(AccountRepository.name) public repository: AccountRepository, + @repository(AccountRepository) public repository: AccountRepository, ) {} ``` @@ -331,7 +331,7 @@ Injection: ```ts export class AccountController { - @repository(NewRepository.name) private repository: NewRepository; + @repository(NewRepository) private repository: NewRepository; } ``` diff --git a/docs/site/todo-tutorial-controller.md b/docs/site/todo-tutorial-controller.md index 65a8f483e6a9..2c3a4d729110 100644 --- a/docs/site/todo-tutorial-controller.md +++ b/docs/site/todo-tutorial-controller.md @@ -41,7 +41,7 @@ import {TodoRepository} from '../repositories'; export class TodoController { constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, + @repository(TodoRepository) protected todoRepo: TodoRepository, ) {} } ``` @@ -71,7 +71,7 @@ import {HttpErrors, post, param, requestBody} from '@loopback/rest'; export class TodoController { constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, + @repository(TodoRepository) protected todoRepo: TodoRepository, ) {} @post('/todo') @@ -121,7 +121,7 @@ import { export class TodoController { constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, + @repository(TodoRepository) protected todoRepo: TodoRepository, ) {} @post('/todo') diff --git a/examples/todo/src/controllers/todo.controller.ts b/examples/todo/src/controllers/todo.controller.ts index a1a8a1955e2a..568edb3919b2 100644 --- a/examples/todo/src/controllers/todo.controller.ts +++ b/examples/todo/src/controllers/todo.controller.ts @@ -18,12 +18,7 @@ import { } from '@loopback/rest'; export class TodoController { - // TODO(bajtos) Fix documentation (and argument names?) of @repository() - // to allow the usage below. - // See https://github.com/strongloop/loopback-next/issues/744 - constructor( - @repository(TodoRepository.name) protected todoRepo: TodoRepository, - ) {} + constructor(@repository(TodoRepository) protected todoRepo: TodoRepository) {} @post('/todo') async createTodo(@requestBody() todo: Todo) { diff --git a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs index a1d7ab74df40..b6a6b9d0d0be 100644 --- a/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs +++ b/packages/cli/generators/controller/templates/src/controllers/controller-rest-template.ts.ejs @@ -1,4 +1,4 @@ -import {Filter, Where} from '@loopback/repository'; +import {Filter, Where, repository} from '@loopback/repository'; import { post, param, @@ -8,14 +8,13 @@ import { del, requestBody } from '@loopback/openapi-v3'; -import {inject} from '@loopback/context'; import {<%= modelName %>} from '../models'; import {<%= repositoryName %>} from '../repositories'; export class <%= name %>Controller { constructor( - @inject('repositories.<%= repositoryName %>') + @repository(<%= repositoryName %>) public <%= repositoryNameCamel %> : <%= repositoryName %>, ) {} diff --git a/packages/cli/test/controller.js b/packages/cli/test/controller.js index e8aa8e1619eb..066edd93acc5 100644 --- a/packages/cli/test/controller.js +++ b/packages/cli/test/controller.js @@ -281,10 +281,7 @@ describe('lb4 controller', () => { assert.fileContent(tmpDir + withInputName, /class FooBarController/); // Repository and injection - assert.fileContent( - tmpDir + withInputName, - /\@inject\('repositories.BarRepository'\)/ - ); + assert.fileContent(tmpDir + withInputName, /\@repository\(BarRepository\)/); assert.fileContent( tmpDir + withInputName, /barRepository \: BarRepository/ diff --git a/packages/repository/README.md b/packages/repository/README.md index cc96db186e9a..44c4603cd805 100644 --- a/packages/repository/README.md +++ b/packages/repository/README.md @@ -93,7 +93,7 @@ import {post, requestBody, get, param} from '@loopback/openapi-v3'; export class NoteController { constructor( // Use constructor dependency injection to set up the repository - @repository(NoteRepository.name) public noteRepo: NoteRepository, + @repository(NoteRepository) public noteRepo: NoteRepository, ) {} // Create a new note diff --git a/packages/repository/src/decorators/repository.ts b/packages/repository/src/decorators/repository.ts index 5d0f7e3ff618..71916dc78dc4 100644 --- a/packages/repository/src/decorators/repository.ts +++ b/packages/repository/src/decorators/repository.ts @@ -4,8 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import * as assert from 'assert'; -import {Model, Entity} from '../model'; -import {Repository} from '../repository'; +import {Entity} from '../model'; import {DataSource} from '../datasource'; import { DefaultCrudRepository, @@ -13,6 +12,19 @@ import { } from '../legacy-juggler-bridge'; import {juggler} from '../loopback-datasource-juggler'; import {inject, Context, Injection} from '@loopback/context'; +import {Class} from '../common-types'; +import {Repository} from '../repository'; +import {Model} from '../model'; + +/** + * Type definition for decorators returned by `@repository` decorator factory + */ +export type RepositoryDecorator = ( + target: Object, + key?: string | symbol, + // tslint:disable-next-line:no-any + descriptorOrIndex?: TypedPropertyDescriptor | number, +) => void; /** * Metadata for a repository @@ -42,7 +54,7 @@ export class RepositoryMetadata { /** * Constructor for RepositoryMetadata * - * @param model Name or class of the model. If the value is a string and + * @param modelOrRepo Name or class of the model. If the value is a string and * `dataSource` is not present, it will treated as the name of a predefined * repository * @param dataSource Name or instance of the data source @@ -76,41 +88,85 @@ export class RepositoryMetadata { } /** - * Decorator for model definitions - * @param model Name of the repo or name/class of the model - * @param dataSource Name or instance of the data source - * @returns {(target:AnyType)} + * Decorator for repository injections on properties or method arguments + * + * ```ts + * class CustomerController { + * @repository(CustomerRepository) public custRepo: CustomerRepository; * - * For example: + * constructor( + * @repository(ProductRepository) public prodRepo: ProductRepository, + * ) {} + * // ... + * } + * ``` * - * - @repository('myCustomerRepo') - * - @repository('Customer', 'mysqlDataSource') - * - @repository(Customer, mysqlDataSource) - * - @repository('Customer', mysqlDataSource) - * - @repository(Customer, 'mysqlDataSource') + * @param repositoryName Name of the repo */ -export function repository( +export function repository( + repositoryName: string | Class>, +): RepositoryDecorator; + +/** + * Decorator for DefaultCrudRepository generation and injection on properties + * or method arguments based on the given model and dataSource (or their names) + * + * ```ts + * class CustomerController { + * @repository('Customer', 'mySqlDataSource') + * public custRepo: DefaultCrudRepository< + * Customer, + * typeof Customer.prototype.id + * >; + * + * constructor( + * @repository(Product, mySqlDataSource) + * public prodRepo: DefaultCrudRepository< + * Product, + * typeof Product.prototype.id + * >, + * ) {} + * // ... + * } + * ``` + * + * @param model Name/class of the model + * @param dataSource Name/instance of the dataSource + */ +export function repository( model: string | typeof Entity, + dataSource: string | juggler.DataSource, +): RepositoryDecorator; + +export function repository( + modelOrRepo: string | Class> | typeof Entity, dataSource?: string | juggler.DataSource, ) { - const meta = new RepositoryMetadata(model, dataSource); + const stringOrModel = + typeof modelOrRepo !== 'string' && modelOrRepo.prototype.execute + ? modelOrRepo.name + : (modelOrRepo as typeof Entity); + const meta = new RepositoryMetadata(stringOrModel, dataSource); return function( target: Object, key?: symbol | string, - descriptor?: TypedPropertyDescriptor> | number, + // tslint:disable-next-line:no-any + descriptorOrIndex?: TypedPropertyDescriptor | number, ) { - if (key || typeof descriptor === 'number') { + if (key || typeof descriptorOrIndex === 'number') { if (meta.name) { // Make it shortcut to `@inject('repositories.MyRepo')` // Please note key is undefined for constructor. If strictNullChecks // is true, the compiler will complain as reflect-metadata won't // accept undefined or null for key. Use ! to fool the compiler. - inject('repositories.' + meta.name, meta)(target, key!, descriptor); + inject('repositories.' + meta.name, meta)( + target, + key!, + descriptorOrIndex, + ); } else { // Use repository-factory to create a repository from model + dataSource - // inject('repository-factory', meta)(target, key!, descriptor); - inject('', meta, resolve)(target, key!, descriptor); - // throw new Error('@repository(model, dataSource) is not implemented'); + inject('', meta, resolve)(target, key!, descriptorOrIndex); } return; } diff --git a/packages/repository/test/unit/decorator/repository-with-di.ts b/packages/repository/test/unit/decorator/repository-with-di.ts index df9c880abe8d..22bfd05d46ce 100644 --- a/packages/repository/test/unit/decorator/repository-with-di.ts +++ b/packages/repository/test/unit/decorator/repository-with-di.ts @@ -16,57 +16,80 @@ import { DataSourceType, } from '../../../'; -class MyController { - constructor(@repository('noteRepo') public noteRepo: Repository) {} -} - describe('repository class', () => { let ctx: Context; - before(function() { - const ds: DataSourceType = new DataSourceConstructor({ - name: 'db', - connector: 'memory', - }); + before(givenCtx); - class Note extends Entity { - static definition = new ModelDefinition({ - name: 'note', - properties: { - title: 'string', - content: 'string', - id: {type: 'number', id: true}, - }, - }); + // tslint:disable-next-line:max-line-length + it('supports referencing predefined repository by name via constructor', async () => { + const myController = await ctx.get( + 'controllers.StringBoundController', + ); + expect(myController.noteRepo instanceof DefaultCrudRepository).to.be.true(); + }); - title: string; - content: string; + it('supports referencing predefined repository via constructor', async () => { + const myController = await ctx.get( + 'controllers.RepositoryBoundController', + ); + expect(myController.noteRepo instanceof DefaultCrudRepository).to.be.true(); + }); - constructor(data?: Partial) { - super(data); - } + const ds: DataSourceType = new DataSourceConstructor({ + name: 'db', + connector: 'memory', + }); + + class Note extends Entity { + static definition = new ModelDefinition({ + name: 'note', + properties: { + title: 'string', + content: 'string', + id: {type: 'number', id: true}, + }, + }); + + title: string; + content: string; + + constructor(data?: Partial) { + super(data); } + } - class MyRepository extends DefaultCrudRepository { - constructor( - @inject('models.Note') myModel: typeof Note, - @inject('dataSources.memory') dataSource: DataSourceType, - ) { - super(myModel, dataSource); - } + class MyRepository extends DefaultCrudRepository { + constructor( + @inject('models.Note') myModel: typeof Note, + @inject('dataSources.memory') dataSource: DataSourceType, + ) { + super(myModel, dataSource); } + } + + class StringBoundController { + constructor( + @repository('MyRepository') public noteRepo: Repository, + ) {} + } + + class RepositoryBoundController { + constructor( + @repository(MyRepository) public noteRepo: Repository, + ) {} + } + + function givenCtx() { ctx = new Context(); ctx.bind('models.Note').to(Note); ctx.bind('dataSources.memory').to(ds); - ctx.bind('repositories.noteRepo').toClass(MyRepository); - ctx.bind('controllers.MyController').toClass(MyController); - }); - - // tslint:disable-next-line:max-line-length - it('supports referencing predefined repository by name via constructor', async () => { - const myController = await ctx.get( - 'controllers.MyController', - ); - expect(myController.noteRepo instanceof DefaultCrudRepository).to.be.true(); - }); + ctx.bind('repositories.MyRepository').toClass(MyRepository); + ctx + .bind('controllers.StringBoundController') + .toClass(StringBoundController); + ctx + .bind('controllers.RepositoryBoundController') + .toClass(RepositoryBoundController); + } });