From 4b7ddc0260c6edd521f454d5ca0884280014a146 Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Wed, 27 Jul 2022 17:56:15 -0400 Subject: [PATCH] feat: test against @koa/router and clean up typing --- README.md | 2 +- package.json | 4 +-- src/integration.koa.test.ts | 2 +- src/koa.test.ts | 2 +- src/koa.ts | 51 +++++++++++++++++++++---------------- yarn.lock | 43 +++++++++++++++---------------- 6 files changed, 55 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 9ae2af3..0849ec1 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ If you're building a Koa app, you can use these generated types with the `implem ```typescript // app.ts import Koa from 'koa'; -import Router from 'koa-router'; +import Router from '@koa/router'; import { implementSchema } from '@lifeomic/one-schema'; diff --git a/package.json b/package.json index fb3f6f6..63ef878 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "yargs": "^17.5.1" }, "devDependencies": { + "@koa/router": "^12.0.0", "@lifeomic/eslint-config-standards": "^2.1.1", "@lifeomic/typescript-config": "^1.0.3", "@types/axios": "^0.14.0", @@ -33,7 +34,7 @@ "@types/js-yaml": "^4.0.5", "@types/koa": "^2.13.4", "@types/koa-bodyparser": "^4.3.7", - "@types/koa-router": "^7.4.4", + "@types/koa__router": "^8.0.11", "@types/node": "^14.0.0", "@types/tmp": "^0.2.3", "@types/yargs": "^17.0.10", @@ -45,7 +46,6 @@ "jest": "^28.1.0", "koa": "^2.13.4", "koa-bodyparser": "^4.3.0", - "koa-router": "^10.1.1", "openapi-schema-validator": "^11.0.1", "prettier": "^2.6.2", "semantic-release": "^19.0.3", diff --git a/src/integration.koa.test.ts b/src/integration.koa.test.ts index 9ae483c..70d9163 100644 --- a/src/integration.koa.test.ts +++ b/src/integration.koa.test.ts @@ -1,6 +1,6 @@ import axios, { AxiosInstance } from 'axios'; import Koa = require('koa'); -import Router = require('koa-router'); +import Router = require('@koa/router'); import bodyparser = require('koa-bodyparser'); import { ImplementationConfig, implementSchema, OneSchemaDefinition } from '.'; import { withAssumptions } from './meta-schema'; diff --git a/src/koa.test.ts b/src/koa.test.ts index e7ba779..424c750 100644 --- a/src/koa.test.ts +++ b/src/koa.test.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import Koa = require('koa'); import bodyParser = require('koa-bodyparser'); -import Router = require('koa-router'); +import Router = require('@koa/router'); import { implementSchema } from '.'; import { withAssumptions } from './meta-schema'; diff --git a/src/koa.ts b/src/koa.ts index b643137..ca1bda3 100644 --- a/src/koa.ts +++ b/src/koa.ts @@ -1,13 +1,12 @@ import { JSONSchema4 } from 'json-schema'; -import type { ParameterizedContext } from 'koa'; -import type Router from 'koa-router'; +import type { ExtendableContext, ParameterizedContext } from 'koa'; +import type Router from '@koa/router'; import type { EndpointsOf, IntrospectionResponse, OneSchema } from './types'; /** * We use this type to very cleanly remove these fields from the Koa context, so * that we can replace the fields with our strict types from the generated schema. * - * - `params` * - `request.body` * - `request.query` * @@ -24,29 +23,37 @@ import type { EndpointsOf, IntrospectionResponse, OneSchema } from './types'; * By explicitly removing the fields from the context, then re-adding them, we can be * sure they are typed correctly. */ -type WithTypedFieldsRemoved = - // Omit params and request - Omit & { +type ExtendableContextWithRequestFieldsRemoved = + // Omit request entirely + Omit & { // Re-add request, but without the "body" or "query" fields. - request: Omit; + request: Omit; }; export type ImplementationOf, State, Context> = { [Name in keyof EndpointsOf]: ( - // It's important that we remove our "typed" fields from the root `ParameterizedContext`, - // and not from the "inner" `Context` type. - // - // If we remove from the "inner" `Context` type, the `ParameterizedContext` will - // effectively just "re-add" the fields we removed. - // - // Basically, the "inner" Context can only be used for _extending_ the context, - // but not for _restricting_ it. - context: WithTypedFieldsRemoved> & { - params: EndpointsOf[Name]['PathParams']; - request: Name extends `${'GET' | 'DELETE'} ${string}` - ? { query: EndpointsOf[Name]['Request'] } - : { body: EndpointsOf[Name]['Request'] }; - }, + // prettier-ignore + context: + // 1. Start with a context that has request.body and request.query removed. + // This context also importantly does _not_ have the `params` property included, + // since it comes from a core `koa` type, rather than from `@koa/router`. + & ExtendableContextWithRequestFieldsRemoved + // 2. Now, we add the generated + well-typed `params`, `request.body`, and + // `request.query` properties. + & { + params: EndpointsOf[Name]['PathParams']; + request: Name extends `${'GET' | 'DELETE'} ${string}` + ? { query: EndpointsOf[Name]['Request'] } + : { body: EndpointsOf[Name]['Request'] }; + } + // 3. Now, add the `state` property and merge in the arbitrary custom context, to + // essentially mimic the behavior of koa's `ParameterizedContext`. + // + // Why not just use ParameterizedContext: When we tried to use ParameterizedContext + // directly, it was incompatible with Omit (omitting a single property resulted in + // a fully empty object). + & { state: State; } + & Context, ) => | EndpointsOf[Name]['Response'] | Promise[Name]['Response']>; @@ -142,7 +149,7 @@ export const implementSchema = >( const [method, path] = endpoint.split(' '); /** A shared route handler. */ - const handler: Router.IMiddleware = async (ctx, next) => { + const handler: Router.Middleware = async (ctx, next) => { // 1. Validate the input data. const requestSchema = schema.Endpoints[endpoint].Request; if (requestSchema) { diff --git a/yarn.lock b/yarn.lock index 3575c5e..b07e54f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -615,6 +615,16 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@koa/router@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@koa/router/-/router-12.0.0.tgz#2ae7937093fd392761c0e5833c368379d4a35737" + integrity sha512-cnnxeKHXlt7XARJptflGURdJaO+ITpNkOHmQu7NHmCoRinPbyvFzce/EG/E8Zy81yQ1W9MoSdtklc3nyaDReUw== + dependencies: + http-errors "^2.0.0" + koa-compose "^4.1.0" + methods "^1.1.2" + path-to-regexp "^6.2.1" + "@lifeomic/alpha@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@lifeomic/alpha/-/alpha-4.0.1.tgz#e7b5c8eddf454affbdc380a973d10407be11c5d8" @@ -1248,13 +1258,6 @@ dependencies: "@types/koa" "*" -"@types/koa-router@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/koa-router/-/koa-router-7.4.4.tgz#db72bde3616365d74f00178d5f243c4fce7da572" - integrity sha512-3dHlZ6CkhgcWeF6wafEUvyyqjWYfKmev3vy1PtOmr0mBc3wpXPU5E8fBBd4YQo5bRpHPfmwC5yDaX7s4jhIN6A== - dependencies: - "@types/koa" "*" - "@types/koa@*", "@types/koa@^2.13.4": version "2.13.4" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" @@ -1269,6 +1272,13 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/koa__router@^8.0.11": + version "8.0.11" + resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.11.tgz#d7b37e6db934fc072ea1baa2ab92bc8ac4564f3e" + integrity sha512-WXgKWpBsbS14kzmzD9LeFapOIa678h7zvUHxDwXwSx4ETKXhXLVUAToX6jZ/U7EihM7qwyD9W/BZvB0MRu7MTQ== + dependencies: + "@types/koa" "*" + "@types/lodash@^4.14.168", "@types/lodash@^4.14.181": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" @@ -3199,7 +3209,7 @@ http-cache-semantics@^4.1.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@2.0.0: +http-errors@2.0.0, http-errors@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== @@ -3210,7 +3220,7 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@^1.6.3, http-errors@^1.7.3, http-errors@~1.8.0: +http-errors@^1.6.3, http-errors@~1.8.0: version "1.8.1" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== @@ -4105,17 +4115,6 @@ koa-convert@^2.0.0: co "^4.6.0" koa-compose "^4.1.0" -koa-router@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-10.1.1.tgz#20809f82648518b84726cd445037813cd99f17ff" - integrity sha512-z/OzxVjf5NyuNO3t9nJpx7e1oR3FSBAauiwXtMQu4ppcnuNZzTaQ4p21P8A6r2Es8uJJM339oc4oVW+qX7SqnQ== - dependencies: - debug "^4.1.1" - http-errors "^1.7.3" - koa-compose "^4.1.0" - methods "^1.1.2" - path-to-regexp "^6.1.0" - koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" @@ -4502,7 +4501,7 @@ merge2@^1.3.0, merge2@^1.4.1: methods@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.5" @@ -5239,7 +5238,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@^6.1.0: +path-to-regexp@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==