diff --git a/README.md b/README.md index d6895be..ebdb158 100644 --- a/README.md +++ b/README.md @@ -33,18 +33,33 @@ Endpoints: message: { type: string } Response: $ref: '#/definitions/Post' + GET /posts: + Name: listPosts + Request: + type: object + properties: + filter: { type: string } + Response: + type: array + items: + $ref: '#/definitions/Post' ``` -Next, run some generation. +### Schema Assumptions + +`one-schema` provides a set of JSONSchema assumptions to help simplify Request/Response JSONSchema entries in commonly desired ways. + +These assumptions are described by the `SchemaAssumptions` type in [`src/meta-schema.ts`](src/meta-schema.ts) and can be individually or wholly disabled in the Node API and at the command line via the `--asssumptions` flag. ### Axios Client Generation -Use the `generate-axios-client` command to generate a nicely typed Axios-based client. +Use the `generate-axios-client` command to generate a nicely typed Axios-based client from the schema. ``` one-schema generate-axios-client \ --schema schema.yml \ --output generated-client.ts \ + --assumptions all \ --format ``` @@ -59,8 +74,16 @@ export type Endpoints = { Request: { message: string; }; + PathParams: {}; Response: Post; }; + 'GET /posts': { + Request: { + filter: string; + }; + PathParams: {}; + Response: Post[]; + }; }; export type Post = { @@ -80,7 +103,8 @@ export class Client { constructor(private readonly client: AxiosInstance) {} createPost( - data: Endpoints['POST /posts']['Request'], + data: Endpoints['POST /posts']['Request'] & + Endpoints['POST /posts']['PathParams'], config?: AxiosRequestConfig, ): Promise> { return this.client.request({ @@ -90,6 +114,19 @@ export class Client { url: substituteParams('/posts', data), }); } + + listPosts( + params: Endpoints['GET /posts']['Request'] & + Endpoints['GET /posts']['PathParams'], + config?: AxiosRequestConfig, + ): Promise> { + return this.client.request({ + ...config, + method: 'GET', + params: removePathParams('/posts', params), + url: substituteParams('/posts', params), + }); + } } ``` @@ -111,6 +148,19 @@ console.log(response.data); // id: 'some-id', // message: 'some-message' // } + +const response = await client.listPosts({ + filter: 'some-filter', +}); + +console.log(response.data); +// [ +// { +// id: 'some-id', +// message: 'some-message' +// }, +// ... +// ] ``` ### API Type Generation @@ -121,6 +171,7 @@ Use the `generate-api-types` command to generate helpful types to use for server one-schema generate-api-types \ --schema schema.yml \ --output generated-api.ts \ + --assumptions all \ --format ``` @@ -135,8 +186,16 @@ export type Endpoints = { Request: { message: string; }; + PathParams: {}; Response: Post; }; + 'GET /posts': { + Request: { + filter: string; + }; + PathParams: {}; + Response: Post[]; + }; }; export type Post = { @@ -176,12 +235,19 @@ implementSchema(Schema, { }, implementation: { 'POST /posts': (ctx) => { - // `ctx.request.body` is well-typed and has been run-time-validated. + // `ctx.request.body` is well-typed and has been run-time validated. console.log(ctx.request.body.message); // TypeScript enforces that this matches the `Response` schema. return { id: '123', message: 'test message' }; }, + 'GET /posts': (ctx) => { + // `ctx.request.query` is well-typed and has been run-time validated + console.log(ctx.request.query.filter); + + // TypeScript enforces that this matches the `Response` schema. + return [{ id: '123', message: 'test message' }]; + }, }, }); @@ -199,6 +265,7 @@ Use the `generate-open-api-spec` command to generate an OpenAPI spec from a simp one-schema generate-open-api-spec \ --schema schema.yml \ --output openapi-schema.json \ + --assumptions all \ --apiVersion "1.0.0" \ --apiTitle "Simple API" \ --format @@ -263,14 +330,36 @@ The output (in `generated-openapi-schema.json`): } } } + }, + "get": { + "operationId": "listPosts", + "responses": { + "200": { + "description": "TODO", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Post" + } + } + } + } + } + }, + "parameters": [ + { + "in": "query", + "name": "filter", + "schema": { + "type": "string" + }, + "required": true + } + ] } } } } ``` - -### Schema Assumptions - -`one-schema` provides a set of JSONSchema assumptions to help simplify Request/Response JSONSchema entries in commonly desired ways. - -These assumptions are described by the `SchemaAssumptions` type in [`src/meta-schema.ts`](src/meta-schema.ts) and can be individually or wholly disabled in the Node API and at the command line via the `--asssumptions` flag. diff --git a/src/integration.koa.test.ts b/src/integration.koa.test.ts index b33e480..868185e 100644 --- a/src/integration.koa.test.ts +++ b/src/integration.koa.test.ts @@ -150,7 +150,7 @@ test('GET method', async () => { { implementation: { 'GET /posts': (ctx) => { - return ctx.request.body; + return ctx.request.query; }, }, }, diff --git a/src/koa.ts b/src/koa.ts index 2af783d..56ad0f4 100644 --- a/src/koa.ts +++ b/src/koa.ts @@ -3,15 +3,24 @@ import type { ParameterizedContext } from 'koa'; import type Router = require('koa-router'); import type { EndpointsOf, OneSchema } from './types'; +// This declare is required to override the "declare" that comes from +// koa-bodyparser. Without this, the typings from one-schema will be +// overriden and collapsed into "any". +declare module 'koa' { + interface Request { + body?: unknown; + } +} + export type ImplementationOf, State, Context> = { [Name in keyof EndpointsOf]: ( context: ParameterizedContext< State, Context & { params: EndpointsOf[Name]['PathParams']; - request: { - body: EndpointsOf[Name]['Request']; - }; + request: Name extends `${'GET' | 'DELETE'} ${string}` + ? { query: EndpointsOf[Name]['Request'] } + : { body: EndpointsOf[Name]['Request'] }; } >, ) => Promise[Name]['Response']>; @@ -82,15 +91,24 @@ export const implementSchema = >( // 1. Validate the input data. const requestSchema = Endpoints[endpoint].Request; if (requestSchema) { - ctx.request.body = parse( - ctx, - endpoint, - { ...requestSchema, definitions: Resources }, - // 1a. For GET and DELETE, use the query parameters. Otherwise, use the body. - ['GET', 'DELETE'].includes(method) - ? ctx.request.query - : ctx.request.body, - ); + // 1a. For GET and DELETE, validate the query params. + if (['GET', 'DELETE'].includes(method)) { + // @ts-ignore + ctx.request.query = parse( + ctx, + endpoint, + { ...requestSchema, definitions: Resources }, + ctx.request.query, + ); + } else { + // 1b. Otherwise, use the body. + ctx.request.body = parse( + ctx, + endpoint, + { ...requestSchema, definitions: Resources }, + ctx.request.body, + ); + } } // 2. Run the provided route handler. diff --git a/tsconfig.json b/tsconfig.json index f84816a..3282118 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "inlineSources": false, "inlineSourceMap": false, "esModuleInterop": false, - "declaration": true + "declaration": true, + "skipLibCheck": true } }