diff --git a/packages/core/src/keys.ts b/packages/core/src/keys.ts index 21775387ffe6..3050150bdefe 100644 --- a/packages/core/src/keys.ts +++ b/packages/core/src/keys.ts @@ -53,4 +53,10 @@ export namespace CoreBindings { * request context */ export const CONTROLLER_METHOD_META = 'controller.method.meta'; + + /** + * Binding key for the controller instance resolved in the current request + * context + */ + export const CONTROLLER_CURRENT = BindingKey.create('controller.current'); } diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index df93f88676a4..7538a3451521 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -15,6 +15,7 @@ import { ResolvedRoute, RouteEntry, ControllerClass, + ControllerFactory, } from './router/routing-table'; import {ParsedRequest} from './internal-types'; @@ -33,8 +34,12 @@ export class HttpHandler { this.handleRequest = (req, res) => this._handleRequest(req, res); } - registerController(name: ControllerClass, spec: ControllerSpec) { - this._routes.registerController(name, spec); + registerController( + spec: ControllerSpec, + controllerCtor: ControllerClass, + controllerFactory?: ControllerFactory, + ) { + this._routes.registerController(spec, controllerCtor, controllerFactory); } registerRoute(route: RouteEntry) { diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 30e64cbd5da2..450481d764fd 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -14,6 +14,12 @@ export { ResolvedRoute, createResolvedRoute, parseRequestUrl, + ControllerClass, + ControllerInstance, + ControllerFactory, + createControllerFactoryForBinding, + createControllerFactoryForClass, + createControllerFactoryForInstance, } from './router/routing-table'; export * from './providers'; diff --git a/packages/rest/src/rest.application.ts b/packages/rest/src/rest.application.ts index 2c5f734b9c02..93c23ab87cc5 100644 --- a/packages/rest/src/rest.application.ts +++ b/packages/rest/src/rest.application.ts @@ -10,7 +10,11 @@ import {Binding, Constructor} from '@loopback/context'; import {format} from 'util'; import {RestBindings} from './keys'; import {RestServer, HttpRequestListener, HttpServerLike} from './rest.server'; -import {ControllerClass, RouteEntry} from './router/routing-table'; +import { + RouteEntry, + ControllerClass, + ControllerFactory, +} from './router/routing-table'; import {OperationObject, OpenApiSpec} from '@loopback/openapi-v3-types'; export const ERR_NO_MULTI_SERVER = format( @@ -95,14 +99,16 @@ export class RestApplication extends Application implements HttpServerLike { * @param verb HTTP verb of the endpoint * @param path URL path of the endpoint * @param spec The OpenAPI spec describing the endpoint (operation) - * @param controller Controller constructor + * @param controllerCtor Controller constructor + * @param controllerFactory A factory function to create controller instance * @param methodName The name of the controller method */ - route( + route( verb: string, path: string, spec: OperationObject, - controller: ControllerClass, + controllerCtor: ControllerClass, + controllerFactory: ControllerFactory, methodName: string, ): Binding; @@ -121,18 +127,26 @@ export class RestApplication extends Application implements HttpServerLike { */ route(route: RouteEntry): Binding; - route( + route( routeOrVerb: RouteEntry | string, path?: string, spec?: OperationObject, - controller?: ControllerClass, + controllerCtor?: ControllerClass, + controllerFactory?: ControllerFactory, methodName?: string, ): Binding { const server = this.restServer; if (typeof routeOrVerb === 'object') { return server.route(routeOrVerb); } else { - return server.route(routeOrVerb, path!, spec!, controller!, methodName!); + return server.route( + routeOrVerb, + path!, + spec!, + controllerCtor!, + controllerFactory!, + methodName!, + ); } } diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 15629454729a..3652f7947b68 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -10,7 +10,10 @@ import { Route, ControllerRoute, RouteEntry, + ControllerFactory, ControllerClass, + ControllerInstance, + createControllerFactoryForBinding, } from './router/routing-table'; import {ParsedRequest} from './internal-types'; import {OpenApiSpec, OperationObject} from '@loopback/openapi-v3-types'; @@ -243,7 +246,8 @@ export class RestServer extends Context implements Server, HttpServerLike { if (apiSpec.components && apiSpec.components.schemas) { this._httpHandler.registerApiDefinitions(apiSpec.components.schemas); } - this._httpHandler.registerController(ctor, apiSpec); + const controllerFactory = createControllerFactoryForBinding(b.key); + this._httpHandler.registerController(apiSpec, ctor, controllerFactory); } for (const b of this.find('routes.*')) { @@ -292,7 +296,14 @@ export class RestServer extends Context implements Server, HttpServerLike { ); } - const route = new ControllerRoute(verb, path, spec, ctor); + const controllerFactory = createControllerFactoryForBinding(b.key); + const route = new ControllerRoute( + verb, + path, + spec, + ctor, + controllerFactory, + ); this._httpHandler.registerRoute(route); return; } @@ -351,7 +362,7 @@ export class RestServer extends Context implements Server, HttpServerLike { * ``` * */ - controller(controllerCtor: ControllerClass): Binding { + controller(controllerCtor: ControllerClass): Binding { return this.bind('controllers.' + controllerCtor.name).toClass( controllerCtor, ); @@ -372,14 +383,16 @@ export class RestServer extends Context implements Server, HttpServerLike { * @param verb HTTP verb of the endpoint * @param path URL path of the endpoint * @param spec The OpenAPI spec describing the endpoint (operation) - * @param controller Controller constructor + * @param controllerCtor Controller constructor + * @param controllerFactory A factory function to create controller instance * @param methodName The name of the controller method */ - route( + route( verb: string, path: string, spec: OperationObject, - controller: ControllerClass, + controllerCtor: ControllerClass, + controllerFactory: ControllerFactory, methodName: string, ): Binding; @@ -398,11 +411,12 @@ export class RestServer extends Context implements Server, HttpServerLike { */ route(route: RouteEntry): Binding; - route( + route( routeOrVerb: RouteEntry | string, path?: string, spec?: OperationObject, - controller?: ControllerClass, + controllerCtor?: ControllerClass, + controllerFactory?: ControllerFactory, methodName?: string, ): Binding { if (typeof routeOrVerb === 'object') { @@ -426,7 +440,7 @@ export class RestServer extends Context implements Server, HttpServerLike { }); } - if (!controller) { + if (!controllerCtor) { throw new AssertionError({ message: 'controller is required for a controller-based route', }); @@ -439,7 +453,14 @@ export class RestServer extends Context implements Server, HttpServerLike { } return this.route( - new ControllerRoute(routeOrVerb, path, spec, controller, methodName), + new ControllerRoute( + routeOrVerb, + path, + spec, + controllerCtor, + controllerFactory, + methodName, + ), ); } diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index 8dbc705dce65..8d42f9b9d6c6 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -9,10 +9,12 @@ import { PathsObject, } from '@loopback/openapi-v3-types'; import { + BindingScope, Context, Constructor, - instantiateClass, invokeMethod, + instantiateClass, + ValueOrPromise, } from '@loopback/context'; import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; @@ -61,13 +63,42 @@ export function parseRequestUrl(request: ServerRequest): ParsedRequest { return parsedRequest; } +/** + * A controller instance with open properties/methods + */ // tslint:disable-next-line:no-any -export type ControllerClass = Constructor; +export type ControllerInstance = {[name: string]: any} & object; +/** + * A factory function to create controller instances synchronously or + * asynchronously + */ +export type ControllerFactory = ( + ctx: Context, +) => ValueOrPromise; + +/** + * Controller class + */ +export type ControllerClass = Constructor; + +/** + * Routing table + */ export class RoutingTable { private readonly _routes: RouteEntry[] = []; - registerController(controller: ControllerClass, spec: ControllerSpec) { + /** + * Register a controller as the route + * @param spec + * @param controllerCtor + * @param controllerFactory + */ + registerController( + spec: ControllerSpec, + controllerCtor: ControllerClass, + controllerFactory?: ControllerFactory, + ) { assert( typeof spec === 'object' && !!spec, 'API specification must be a non-null object', @@ -83,7 +114,13 @@ export class RoutingTable { for (const verb in spec.paths[p]) { const opSpec: OperationObject = spec.paths[p][verb]; const fullPath = RoutingTable.joinPath(basePath, p); - const route = new ControllerRoute(verb, fullPath, opSpec, controller); + const route = new ControllerRoute( + verb, + fullPath, + opSpec, + controllerCtor, + controllerFactory, + ); this.registerRoute(route); } } @@ -98,6 +135,10 @@ export class RoutingTable { return fullPath; } + /** + * Register a route + * @param route A route entry + */ registerRoute(route: RouteEntry) { // TODO(bajtos) handle the case where opSpec.parameters contains $ref // See https://github.com/strongloop/loopback-next/issues/435 @@ -127,6 +168,10 @@ export class RoutingTable { return paths; } + /** + * Map a request to a route + * @param request + */ find(request: ParsedRequest): ResolvedRoute { for (const entry of this._routes) { const match = entry.match(request); @@ -139,14 +184,40 @@ export class RoutingTable { } } +/** + * An entry in the routing table + */ export interface RouteEntry { + /** + * http verb + */ readonly verb: string; + /** + * http path + */ readonly path: string; + /** + * OpenAPI operation spec + */ readonly spec: OperationObject; + /** + * Map an http request to a route + * @param request + */ match(request: ParsedRequest): ResolvedRoute | undefined; + /** + * Update bindings for the request context + * @param requestContext + */ updateBindings(requestContext: Context): void; + + /** + * A handler to invoke the resolved controller method + * @param requestContext + * @param args + */ invokeHandler( requestContext: Context, args: OperationArgs, @@ -155,15 +226,27 @@ export interface RouteEntry { describe(): string; } +/** + * A route with path parameters resolved + */ export interface ResolvedRoute extends RouteEntry { readonly pathParams: PathParameterValues; } +/** + * Base implementation of RouteEntry + */ export abstract class BaseRoute implements RouteEntry { public readonly verb: string; private readonly _keys: pathToRegexp.Key[] = []; private readonly _pathRegexp: RegExp; + /** + * Construct a new route + * @param verb http verb + * @param path http request path pattern + * @param spec OpenAPI operation spec + */ constructor( verb: string, public readonly path: string, @@ -259,55 +342,75 @@ export class Route extends BaseRoute { } } -type ControllerInstance = {[opName: string]: Function}; - -export class ControllerRoute extends BaseRoute { +/** + * A route backed by a controller + */ +export class ControllerRoute extends BaseRoute { + protected readonly _controllerCtor: ControllerClass; + protected readonly _controllerName: string; protected readonly _methodName: string; - + protected readonly _controllerFactory: ControllerFactory; + + /** + * Construct a controller based route + * @param verb http verb + * @param path http request path + * @param spec OpenAPI operation spec + * @param controllerCtor Controller class + * @param controllerFactory A factory function to create a controller instance + * @param methodName Controller method name, default to `x-operation-name` + */ constructor( verb: string, path: string, spec: OperationObject, - protected readonly _controllerCtor: ControllerClass, + controllerCtor: ControllerClass, + controllerFactory?: ControllerFactory, methodName?: string, ) { + const controllerName = spec['x-controller-name'] || controllerCtor.name; + methodName = methodName || spec['x-operation-name']; + + if (!methodName) { + throw new Error( + 'methodName must be provided either via the ControllerRoute argument ' + + 'or via "x-operation-name" extension field in OpenAPI spec. ' + + `Operation: "${verb} ${path}" ` + + `Controller: ${controllerName}.`, + ); + } + super( verb, path, // Add x-controller-name and x-operation-name if not present Object.assign( { - 'x-controller-name': _controllerCtor.name, + 'x-controller-name': controllerName, 'x-operation-name': methodName, - tags: [_controllerCtor.name], + tags: [controllerName], }, spec, ), ); - if (!methodName) { - methodName = this.spec['x-operation-name']; - } - - if (!methodName) { - throw new Error( - 'methodName must be provided either via the ControllerRoute argument ' + - 'or via "x-operation-name" extension field in OpenAPI spec. ' + - `Operation: "${verb} ${path}" ` + - `Controller: ${this._controllerCtor.name}.`, - ); - } - + this._controllerFactory = + controllerFactory || createControllerFactoryForClass(controllerCtor); + this._controllerCtor = controllerCtor; + this._controllerName = controllerName || controllerCtor.name; this._methodName = methodName; } describe(): string { - return `${this._controllerCtor.name}.${this._methodName}`; + return `${this._controllerName}.${this._methodName}`; } updateBindings(requestContext: Context) { - const ctor = this._controllerCtor; - requestContext.bind(CoreBindings.CONTROLLER_CLASS).to(ctor); + requestContext + .bind(CoreBindings.CONTROLLER_CURRENT) + .toDynamicValue(() => this._controllerFactory(requestContext)) + .inScope(BindingScope.SINGLETON); + requestContext.bind(CoreBindings.CONTROLLER_CLASS).to(this._controllerCtor); requestContext .bind(CoreBindings.CONTROLLER_METHOD_NAME) .to(this._methodName); @@ -317,7 +420,9 @@ export class ControllerRoute extends BaseRoute { requestContext: Context, args: OperationArgs, ): Promise { - const controller = await this._createControllerInstance(requestContext); + const controller = await requestContext.get( + 'controller.current', + ); if (typeof controller[this._methodName] !== 'function') { throw new HttpErrors.NotFound( `Controller method not found: ${this.describe()}`, @@ -331,16 +436,6 @@ export class ControllerRoute extends BaseRoute { args, ); } - - private async _createControllerInstance( - requestContext: Context, - ): Promise { - const valueOrPromise = instantiateClass( - this._controllerCtor, - requestContext, - ); - return (await Promise.resolve(valueOrPromise)) as ControllerInstance; - } } function describeOperationParameters(opSpec: OperationObject) { @@ -348,3 +443,43 @@ function describeOperationParameters(opSpec: OperationObject) { .map(p => (p && p.name) || '') .join(', '); } + +/** + * Create a controller factory function for a given binding key + * @param key Binding key + */ +export function createControllerFactoryForBinding( + key: string, +): ControllerFactory { + return ctx => ctx.get(key); +} + +/** + * Create a controller factory function for a given class + * @param controllerCtor Controller class + */ +export function createControllerFactoryForClass( + controllerCtor: ControllerClass, +): ControllerFactory { + return async ctx => { + // By default, we get an instance of the controller from the context + // using `controllers.` as the key + let inst = await ctx.get(`controllers.${controllerCtor.name}`, { + optional: true, + }); + if (inst === undefined) { + inst = await instantiateClass(controllerCtor, ctx); + } + return inst; + }; +} + +/** + * Create a controller factory function for a given instance + * @param controllerCtor Controller instance + */ +export function createControllerFactoryForInstance( + controllerInst: T, +): ControllerFactory { + return ctx => controllerInst; +} diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index 1c4617c9870d..6ee9d6fae8e4 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -13,6 +13,10 @@ import { RestApplication, SequenceActions, HttpServerLike, + ControllerClass, + ControllerInstance, + createControllerFactoryForClass, + createControllerFactoryForInstance, } from '../../..'; import {api, get, post, param, requestBody} from '@loopback/openapi-v3'; @@ -27,8 +31,8 @@ import { import {expect, Client, createClientForHandler} from '@loopback/testlab'; import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; -import {inject, Context} from '@loopback/context'; -import {ControllerClass} from '../../../src/router/routing-table'; +import {inject, Context, BindingScope} from '@loopback/context'; + import {createUnexpectedHttpErrorLogger} from '../../helpers'; /* # Feature: Routing @@ -357,6 +361,32 @@ describe('Routing', () => { }); }); + it('binds the current controller', async () => { + const app = givenAnApplication(); + const server = await givenAServer(app); + const spec = anOpenApiSpec() + .withOperationReturningString('get', '/name', 'checkController') + .build(); + + @api(spec) + class GetCurrentController { + async checkController( + @inject('controllers.GetCurrentController') inst: GetCurrentController, + ): Promise { + return { + result: this === inst, + }; + } + } + givenControllerInApp(app, GetCurrentController); + + return whenIMakeRequestTo(server) + .get('/name') + .expect({ + result: true, + }); + }); + it('supports function-based routes', async () => { const app = givenAnApplication(); const server = await givenAServer(app); @@ -524,7 +554,35 @@ describe('Routing', () => { .withParameter({name: 'name', in: 'query', type: 'string'}) .build(); - server.route('get', '/greet', spec, MyController, 'greet'); + server.route( + 'get', + '/greet', + spec, + MyController, + createControllerFactoryForClass(MyController), + 'greet', + ); + + const client = whenIMakeRequestTo(server); + await client.get('/greet?name=world').expect(200, 'hello world'); + }); + + it('supports controller routes with factory defined via server.route()', async () => { + const app = givenAnApplication(); + const server = await givenAServer(app); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + const spec = anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .build(); + + const factory = createControllerFactoryForClass(MyController); + server.route('get', '/greet', spec, MyController, factory, 'greet'); const client = whenIMakeRequestTo(server); await client.get('/greet?name=world').expect(200, 'hello world'); @@ -599,13 +657,41 @@ describe('Routing', () => { .withParameter({name: 'name', in: 'query', type: 'string'}) .build(); - app.route('get', '/greet', spec, MyController, 'greet'); + const factory = createControllerFactoryForClass(MyController); + app.route('get', '/greet', spec, MyController, factory, 'greet'); await whenIMakeRequestTo(app) .get('/greet?name=world') .expect(200, 'hello world'); }); + it('supports controller routes via app.route() with a factory', async () => { + const app = new RestApplication(); + + class MyController { + greet(name: string) { + return `hello ${name}`; + } + } + + class MySubController extends MyController { + greet(name: string) { + return super.greet(name) + '!'; + } + } + + const spec = anOperationSpec() + .withParameter({name: 'name', in: 'query', type: 'string'}) + .build(); + + const factory = createControllerFactoryForInstance(new MySubController()); + app.route('get', '/greet', spec, MyController, factory, 'greet'); + + await whenIMakeRequestTo(app) + .get('/greet?name=world') + .expect(200, 'hello world!'); + }); + it('provides httpHandler compatible with HTTP server API', async () => { const app = new RestApplication(); app.handler((sequence, req, res) => res.end('hello')); @@ -637,8 +723,11 @@ describe('Routing', () => { return await app.getServer(RestServer); } - function givenControllerInApp(app: Application, controller: ControllerClass) { - app.controller(controller); + function givenControllerInApp( + app: Application, + controller: ControllerClass, + ) { + app.controller(controller).inScope(BindingScope.CONTEXT); } function whenIMakeRequestTo(serverOrApp: HttpServerLike): Client { diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index 99f5d55f7c44..f9c49c18fd98 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -24,7 +24,10 @@ import {Application} from '@loopback/core'; import {expect, Client, createClientForHandler} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; import {inject, Context} from '@loopback/context'; -import {ControllerClass} from '../../../src/router/routing-table'; +import { + ControllerClass, + ControllerInstance, +} from '../../../src/router/routing-table'; const SequenceActions = RestBindings.SequenceActions; @@ -209,7 +212,9 @@ describe('Sequence', () => { server = await app.getServer(RestServer); } - function givenControllerInServer(controller: ControllerClass) { + function givenControllerInServer( + controller: ControllerClass, + ) { app.controller(controller); } diff --git a/packages/rest/test/integration/http-handler.integration.ts b/packages/rest/test/integration/http-handler.integration.ts index 6e42723f786c..c03936de4b4c 100644 --- a/packages/rest/test/integration/http-handler.integration.ts +++ b/packages/rest/test/integration/http-handler.integration.ts @@ -454,7 +454,7 @@ describe('HttpHandler', () => { ctor: new (...args: any[]) => Object, spec: ControllerSpec, ) { - handler.registerController(ctor, spec); + handler.registerController(spec, ctor); } function givenClient() { diff --git a/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts b/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts index 2b05a5d4bc92..cbdd5e18399f 100644 --- a/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts +++ b/packages/rest/test/unit/rest.server/rest.server.open-api-spec.unit.ts @@ -5,7 +5,12 @@ import {expect, validateApiSpec} from '@loopback/testlab'; import {Application} from '@loopback/core'; -import {RestServer, Route, RestComponent} from '../../..'; +import { + RestServer, + Route, + RestComponent, + createControllerFactoryForClass, +} from '../../..'; import {get, post, requestBody} from '@loopback/openapi-v3'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; import {model, property} from '@loopback/repository'; @@ -63,6 +68,7 @@ describe('RestServer.getApiSpec()', () => { '/greet.json', {responses: {}}, MyController, + createControllerFactoryForClass(MyController), 'greet', ); expect(binding.key).to.eql('routes.get %2Fgreet%2Ejson'); @@ -88,7 +94,14 @@ describe('RestServer.getApiSpec()', () => { greet() {} } - server.route('get', '/greet', {responses: {}}, MyController, 'greet'); + server.route( + 'get', + '/greet', + {responses: {}}, + MyController, + createControllerFactoryForClass(MyController), + 'greet', + ); const spec = server.getApiSpec(); expect(spec.paths).to.eql({ diff --git a/packages/rest/test/unit/router/controller-factory.test.ts b/packages/rest/test/unit/router/controller-factory.test.ts new file mode 100644 index 000000000000..38e529ca3011 --- /dev/null +++ b/packages/rest/test/unit/router/controller-factory.test.ts @@ -0,0 +1,47 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import { + createControllerFactoryForBinding, + createControllerFactoryForClass, + createControllerFactoryForInstance, +} from '../../..'; +import {Context} from '@loopback/core'; + +describe('createControllerFactory', () => { + let ctx: Context; + + beforeEach(() => { + ctx = new Context(); + }); + + it('creates a factory with binding key', async () => { + ctx.bind('controllers.my-controller').toClass(MyController); + const factory = createControllerFactoryForBinding( + 'controllers.my-controller', + ); + const inst = await factory(ctx); + expect(inst).to.be.instanceof(MyController); + }); + + it('creates a factory with class', async () => { + const factory = createControllerFactoryForClass(MyController); + const inst = await factory(ctx); + expect(inst).to.be.instanceof(MyController); + }); + + it('creates a factory with an instance', async () => { + const factory = createControllerFactoryForInstance(new MyController()); + const inst = await factory(ctx); + expect(inst).to.be.instanceof(MyController); + }); + + class MyController { + greet() { + return 'Hello'; + } + } +}); diff --git a/packages/rest/test/unit/router/controller-route.unit.ts b/packages/rest/test/unit/router/controller-route.unit.ts index ec4fec151182..c2316b2229d6 100644 --- a/packages/rest/test/unit/router/controller-route.unit.ts +++ b/packages/rest/test/unit/router/controller-route.unit.ts @@ -3,17 +3,98 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ControllerRoute} from '../../..'; +import { + ControllerRoute, + createControllerFactoryForClass, + createControllerFactoryForBinding, +} from '../../..'; import {expect} from '@loopback/testlab'; import {anOperationSpec} from '@loopback/openapi-spec-builder'; +import {ControllerFactory} from '../../..'; describe('ControllerRoute', () => { it('rejects routes with no methodName', () => { - class MyController {} const spec = anOperationSpec().build(); expect( () => new ControllerRoute('get', '/greet', spec, MyController), ).to.throw(/methodName must be provided.*"get \/greet".*MyController/); }); + + it('creates a factory', () => { + const spec = anOperationSpec().build(); + + const route = new MyRoute( + 'get', + '/greet', + spec, + MyController, + myControllerFactory, + 'greet', + ); + + expect(route._controllerFactory).to.be.a.Function(); + }); + + it('honors a factory', () => { + const spec = anOperationSpec().build(); + + const factory = createControllerFactoryForBinding( + 'controllers.my-controller', + ); + const route = new MyRoute( + 'get', + '/greet', + spec, + MyController, + factory, + 'greet', + ); + + expect(route._controllerFactory).to.be.exactly(factory); + }); + + it('infers controllerName from the class', () => { + const spec = anOperationSpec().build(); + + const route = new MyRoute( + 'get', + '/greet', + spec, + MyController, + myControllerFactory, + 'greet', + ); + + expect(route._controllerName).to.eql(MyController.name); + }); + + it('honors controllerName from the spec', () => { + const spec = anOperationSpec().build(); + spec['x-controller-name'] = 'my-controller'; + + const route = new MyRoute( + 'get', + '/greet', + spec, + MyController, + myControllerFactory, + 'greet', + ); + + expect(route._controllerName).to.eql('my-controller'); + }); + + class MyController { + greet() { + return 'Hello'; + } + } + + const myControllerFactory = createControllerFactoryForClass(MyController); + + class MyRoute extends ControllerRoute { + _controllerFactory: ControllerFactory; + _controllerName: string; + } }); diff --git a/packages/rest/test/unit/router/routing-table.unit.ts b/packages/rest/test/unit/router/routing-table.unit.ts index dd50c25fc0c4..4507dd055231 100644 --- a/packages/rest/test/unit/router/routing-table.unit.ts +++ b/packages/rest/test/unit/router/routing-table.unit.ts @@ -40,7 +40,7 @@ describe('RoutingTable', () => { } const spec = getControllerSpec(TestController); const table = new RoutingTable(); - table.registerController(TestController, spec); + table.registerController(spec, TestController); const paths = table.describeApiPaths(); const params = paths['/greet']['get'].parameters; expect(params).to.have.property('length', 1); @@ -59,7 +59,7 @@ describe('RoutingTable', () => { class TestController {} const table = new RoutingTable(); - table.registerController(TestController, spec); + table.registerController(spec, TestController); const request = givenRequest({ method: 'get', @@ -90,7 +90,7 @@ describe('RoutingTable', () => { class TestController {} const table = new RoutingTable(); - table.registerController(TestController, spec); + table.registerController(spec, TestController); const request = givenRequest({ method: 'get',