diff --git a/packages/adapter-commons/src/service.ts b/packages/adapter-commons/src/service.ts index 0d407f4357..349592f784 100644 --- a/packages/adapter-commons/src/service.ts +++ b/packages/adapter-commons/src/service.ts @@ -231,4 +231,6 @@ export class AdapterService< } async setup () {} + + async teardown () {} } diff --git a/packages/express/src/declarations.ts b/packages/express/src/declarations.ts index 44a2ed8373..9031f04e99 100644 --- a/packages/express/src/declarations.ts +++ b/packages/express/src/declarations.ts @@ -23,6 +23,7 @@ export interface ExpressOverrides { listen(port: number, hostname: string, callback?: () => void): Promise; listen(port: number|string|any, callback?: () => void): Promise; listen(callback?: () => void): Promise; + close (): Promise; use: ExpressUseHandler; } diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index 5a0249e446..957dc0567d 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -2,6 +2,7 @@ import express, { Express } from 'express'; import { Application as FeathersApplication, defaultServiceMethods } from '@feathersjs/feathers'; import { routing } from '@feathersjs/transport-commons'; import { createDebug } from '@feathersjs/commons'; +import http from 'http'; import { Application } from './declarations'; @@ -26,6 +27,7 @@ export default function feathersExpress (feathersApp?: Feather const app = expressApp as any as Application; const { use: expressUse, listen: expressListen } = expressApp as any; const feathersUse = feathersApp.use; + let server:http.Server | undefined; Object.assign(app, { use (location: string & keyof S, ...rest: any[]) { @@ -69,12 +71,25 @@ export default function feathersExpress (feathersApp?: Feather }, async listen (...args: any[]) { - const server = expressListen.call(this, ...args); + server = expressListen.call(this, ...args); await this.setup(server); debug('Feathers application listening'); return server; + }, + + async close () { + if ( server ) { + server.close(); + + await new Promise((resolve) => { + server.on('close', () => { resolve(true) }); + }) + } + + debug('Feathers application closing'); + await this.teardown(); } } as Application); diff --git a/packages/express/test/index.test.ts b/packages/express/test/index.test.ts index f05bb92b6b..dca63971cd 100644 --- a/packages/express/test/index.test.ts +++ b/packages/express/test/index.test.ts @@ -174,6 +174,42 @@ describe('@feathersjs/express', () => { await new Promise(resolve => server.close(() => resolve(server))); }); + it('.close calls .teardown', async () => { + const app = feathersExpress(feathers()); + let called = false; + + app.use('/myservice', { + async get (id: Id) { + return { id }; + }, + + async teardown (appParam, path) { + assert.strictEqual(appParam, app); + assert.strictEqual(path, 'myservice'); + called = true; + } + + }); + + await app.listen(8787); + await app.close(); + + assert.ok(called); + }); + + it('.close closes http server', async () => { + const app = feathersExpress(feathers()); + let called = false; + + const server = await app.listen(8787); + server.on('close', () => { + called = true; + }) + + await app.close(); + assert.ok(called); + }); + it('passes middleware as options', () => { const feathersApp = feathers(); const app = feathersExpress(feathersApp); diff --git a/packages/feathers/src/application.ts b/packages/feathers/src/application.ts index 9676be9398..e255cddad0 100644 --- a/packages/feathers/src/application.ts +++ b/packages/feathers/src/application.ts @@ -160,4 +160,26 @@ export class Feathers extends EventEmitter implements Feathe return this; }); } + + teardown () { + let promise = Promise.resolve(); + + // Teardown each service (pass the app so that they can look up other services etc.) + for (const path of Object.keys(this.services)) { + promise = promise.then(() => { + const service: any = this.service(path as any); + + if (typeof service.teardown === 'function') { + debug(`Teardown service for \`${path}\``); + + return service.teardown(this, path); + } + }); + } + + return promise.then(() => { + this._isSetup = false; + return this; + }); + } } diff --git a/packages/feathers/src/declarations.ts b/packages/feathers/src/declarations.ts index 73c5228169..4bcb43755b 100644 --- a/packages/feathers/src/declarations.ts +++ b/packages/feathers/src/declarations.ts @@ -35,6 +35,8 @@ export interface ServiceMethods> { remove (id: NullableId, params?: Params): Promise; setup (app: Application, path: string): Promise; + + teardown (app: Application, path: string): Promise; } export interface ServiceOverloads> { @@ -217,6 +219,8 @@ export interface FeathersApplication { * @param map The application hook settings. */ hooks (map: HookOptions): this; + + teardown (cb?: () => Promise): Promise; } // This needs to be an interface instead of a type diff --git a/packages/feathers/src/service.ts b/packages/feathers/src/service.ts index bf0bb7a25a..445aed778e 100644 --- a/packages/feathers/src/service.ts +++ b/packages/feathers/src/service.ts @@ -30,6 +30,7 @@ export const protectedMethods = Object.keys(Object.prototype) 'error', 'hooks', 'setup', + 'teardown', 'publish' ]); diff --git a/packages/feathers/test/application.test.ts b/packages/feathers/test/application.test.ts index ee65d134f2..aeb52ee15a 100644 --- a/packages/feathers/test/application.test.ts +++ b/packages/feathers/test/application.test.ts @@ -89,6 +89,10 @@ describe('Feathers application', () => { this.path = path; }, + async teardown (this: any, _app: any, path: string) { + this.path = path; + }, + async create (data: any) { return data; } @@ -114,7 +118,9 @@ describe('Feathers application', () => { async removeListener (data: any) { return data; }, - async setup () {} + async setup () {}, + + async teardown () {} }; assert.throws(() => feathers().use('/dummy', dummyService, { @@ -127,6 +133,11 @@ describe('Feathers application', () => { }), { message: '\'setup\' on service \'dummy\' is not allowed as a custom method name' }); + assert.throws(() => feathers().use('/dummy', dummyService, { + methods: ['create', 'teardown'] + }), { + message: '\'teardown\' on service \'dummy\' is not allowed as a custom method name' + }); }); it('can use a root level service', async () => { @@ -331,6 +342,43 @@ describe('Feathers application', () => { }); }); + describe('.teardown', () => { + it('app.teardown calls .teardown on all services', async () => { + const app = feathers(); + let teardownCount = 0; + + app.use('/dummy', { + async setup () {}, + async teardown (appRef: any, path: any) { + teardownCount++; + assert.strictEqual(appRef, app); + assert.strictEqual(path, 'dummy'); + } + }); + + app.use('/simple', { + get (id: string) { + return Promise.resolve({ id }); + } + }); + + app.use('/dummy2', { + async setup () {}, + async teardown (appRef: any, path: any) { + teardownCount++; + assert.strictEqual(appRef, app); + assert.strictEqual(path, 'dummy2'); + } + }); + + await app.setup(); + await app.teardown(); + + assert.equal((app as any)._isSetup, false); + assert.strictEqual(teardownCount, 2); + }); + }); + describe('mixins', () => { class Dummy { dummy = true; diff --git a/packages/koa/src/declarations.ts b/packages/koa/src/declarations.ts index 506a72d462..721fe2e8eb 100644 --- a/packages/koa/src/declarations.ts +++ b/packages/koa/src/declarations.ts @@ -5,6 +5,7 @@ import '@feathersjs/authentication'; export type ApplicationAddons = { listen (port?: number, ...args: any[]): Promise; + close (): Promise; } export type Application = diff --git a/packages/koa/src/index.ts b/packages/koa/src/index.ts index 1e19ca4933..c5ca38a1c7 100644 --- a/packages/koa/src/index.ts +++ b/packages/koa/src/index.ts @@ -3,6 +3,7 @@ import koaQs from 'koa-qs'; import { Application as FeathersApplication } from '@feathersjs/feathers'; import { routing } from '@feathersjs/transport-commons'; import { createDebug } from '@feathersjs/commons'; +import http from 'http'; import { Application } from './declarations'; @@ -28,6 +29,7 @@ export function koa (feathersApp?: FeathersApplication, const app = feathersApp as any as Application; const { listen: koaListen, use: koaUse } = koaApp; const feathersUse = feathersApp.use as any; + let server:http.Server | undefined; Object.assign(app, { use (location: string|Koa.Middleware, ...args: any[]) { @@ -39,12 +41,26 @@ export function koa (feathersApp?: FeathersApplication, }, async listen (port?: number, ...args: any[]) { - const server = koaListen.call(this, port, ...args); + server = koaListen.call(this, port, ...args); await this.setup(server); debug('Feathers application listening'); return server; + }, + + async close () { + if ( server ) { + server.close(); + + await new Promise((resolve) => { + server.on('close', () => { resolve(true) }); + }) + } + + debug('Feathers server closed'); + + await this.teardown(); } } as Application); diff --git a/packages/koa/test/index.test.ts b/packages/koa/test/index.test.ts index 475d748681..8c9db95a2c 100644 --- a/packages/koa/test/index.test.ts +++ b/packages/koa/test/index.test.ts @@ -53,7 +53,7 @@ describe('@feathersjs/koa', () => { it('Koa wrapped and context.app are the same', async () => { const app = koa(feathers()); - + app.use('/test', { async get (id: Id) { return { id }; @@ -140,6 +140,42 @@ describe('@feathersjs/koa', () => { }); }); + it('.close calls .teardown', async () => { + const app = koa(feathers()); + let called = false; + + app.use('/myservice', { + async get (id: Id) { + return { id }; + }, + + async teardown (appParam, path) { + assert.strictEqual(appParam, app); + assert.strictEqual(path, 'myservice'); + called = true; + } + + }); + + await app.listen(8787); + await app.close(); + + assert.ok(called); + }); + + it('.close closes http server', async () => { + const app = koa(feathers()); + let called = false; + + const server = await app.listen(8787); + server.on('close', () => { + called = true; + }) + + await app.close(); + assert.ok(called); + }); + restTests('Services', 'todo', 8465); restTests('Root service', '/', 8465); });