diff --git a/README.md b/README.md index ae63b75..3c811d2 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,32 @@ const app = new Koa().use(router.middleware()); app.listen(); ``` +In case the main router requires authorization headers, and you want to query the introspection route without them, you can expose it on a custom router +like so: + +```typescript +const router = OneSchemaRouter.create({ + using: new Router(), + introspection: { + route: '/introspection', + router: new Router({ prefix: '/private' }), + serviceVersion: process.env.LIFEOMIC_SERVICE_VERSION, + }, +}) + .declare({ + route: 'POST /items', + name: 'createItem', + request: z.object({ message: z.string() }), + response: z.object({ id: z.string(), message: z.string() }), + }) + .declare({ + route: 'GET /items/:id', + name: 'getItemById', + request: z.object({ filter: z.string() }), + response: z.object({ id: z.string(), message: z.string() }), + }); +``` + Once you have routes declared, add implementations for each route. Enjoy perfect type inference and auto-complete for path parameters, query parameters, and the request body. ```typescript diff --git a/src/koa.test.ts b/src/koa.test.ts index 9cf1399..7f8e0c5 100644 --- a/src/koa.test.ts +++ b/src/koa.test.ts @@ -118,7 +118,7 @@ test('router typing is inferred correctly', () => { parse: () => null as any, on: router, implementation: { - 'GET /dummy-route': (ctx) => { + 'GET /dummy-route': (ctx: any) => { // assert state is extended correctly ctx.state.dummyStateProperty; diff --git a/src/koa.ts b/src/koa.ts index 513a576..ad34422 100644 --- a/src/koa.ts +++ b/src/koa.ts @@ -34,6 +34,10 @@ export type IntrospectionConfig = { * The current version of the service, served as part of introspection. */ serviceVersion: string; + /** + * An optional alternative router to use for the introspection route. + */ + router?: Router; }; /** @@ -123,7 +127,8 @@ export const implementSchema = < }: ImplementationConfig, ): void => { if (introspection) { - router.get(introspection.route, (ctx, next) => { + const introRouter = introspection.router || router; + introRouter.get(introspection.route, (ctx, next) => { const response: IntrospectionResponse = { schema, serviceVersion: introspection.serviceVersion, diff --git a/src/router.test.ts b/src/router.test.ts index a866c0d..45de1c6 100644 --- a/src/router.test.ts +++ b/src/router.test.ts @@ -120,6 +120,63 @@ test('introspection', async () => { }); }); +test('introspection with custom router', async () => { + const { client } = setup(() => + OneSchemaRouter.create({ + using: new Router(), + introspection: { + route: '/private/introspection', + serviceVersion: '123', + router: new Router({ prefix: '/custom' }), + }, + }) + .declare({ + name: 'getSomething', + route: 'GET /something/:id', + description: 'it gets something', + request: z.object({ filter: z.string() }), + response: z.object({ message: z.string(), id: z.string() }), + }) + .implement('GET /something/:id', () => ({ id: '', message: '' })), + ); + + const wrongIntroRouter = await client.get('/private/introspection'); + expect(wrongIntroRouter.status).toStrictEqual(404); + + const result = await client.get('/custom/private/introspection'); + + expect(result.data).toStrictEqual({ + serviceVersion: '123', + schema: { + Endpoints: { + 'GET /something/:id': { + Description: 'it gets something', + Name: 'getSomething', + Request: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + filter: { type: 'string' }, + }, + required: ['filter'], + type: 'object', + }, + Response: { + $schema: 'http://json-schema.org/draft-07/schema#', + additionalProperties: false, + properties: { + id: { type: 'string' }, + message: { type: 'string' }, + }, + required: ['message', 'id'], + type: 'object', + }, + }, + }, + }, + }); +}); + describe('type inference', () => { test('type inference for implementation return type', () => { setup((router) => @@ -311,7 +368,7 @@ describe('implementations', () => { response: z.object({ message: z.string() }), }) .implement(`${method} /items`, (ctx) => ({ - message: ctx.request.query.message + '-response', + message: (ctx.request.query.message as string) + '-response', })), ); diff --git a/src/router.ts b/src/router.ts index 60437e9..e4ac861 100644 --- a/src/router.ts +++ b/src/router.ts @@ -34,11 +34,15 @@ export class OneSchemaRouter< private constructor( private schema: Schema, - { introspection, using: router }: OneSchemaRouterConfig, + private config: OneSchemaRouterConfig, ) { + const { introspection, using: router } = config; this.router = router; + if (introspection) { - router.get(introspection.route, (ctx, next) => { + const introRouter = introspection.router || router; + + introRouter.get(introspection.route, (ctx, next) => { const response: IntrospectionResponse = { serviceVersion: introspection.serviceVersion, schema: convertRouterSchemaToJSONSchemaStyle(this.schema), @@ -111,7 +115,15 @@ export class OneSchemaRouter< } middleware(): Router.Middleware { - return compose([this.router.routes(), this.router.allowedMethods()]); + const middlewares = [this.router.routes(), this.router.allowedMethods()]; + + if (this.config.introspection?.router) { + middlewares.push( + this.config.introspection.router.routes(), + this.config.introspection.router.allowedMethods(), + ); + } + return compose(middlewares); } }