diff --git a/packages/oats-runtime/src/server.ts b/packages/oats-runtime/src/server.ts index dc0d28fd..3af93807 100644 --- a/packages/oats-runtime/src/server.ts +++ b/packages/oats-runtime/src/server.ts @@ -129,6 +129,11 @@ function voidify(value: object | undefined | null) { } function cleanHeaders(mode: Mode, maker: Maker, headers: object) { + // If headers are not declared in the spec, return null. + const acceptsNull = maker(null); + if (acceptsNull.isSuccess()) { + return acceptsNull.success(); + } // note: we now expect that on client side headers are type conforming so do not need to lowercase those // this is slightly breaking change is somebody has subverted the type checker const normalized = mode === 'server' ? lowercaseObject(headers) : headers; diff --git a/packages/oats-runtime/test/server.spec.ts b/packages/oats-runtime/test/server.spec.ts index 024a7704..e3d56911 100644 --- a/packages/oats-runtime/test/server.spec.ts +++ b/packages/oats-runtime/test/server.spec.ts @@ -13,7 +13,7 @@ import { describe('safe', () => { it('validates request and response', async () => { const endpoint = server.safe( - makeObject({}), + makeVoid(), makeVoid(), makeObject({ param: makeNumber() }) as Maker, makeVoid(), @@ -47,7 +47,7 @@ describe('safe', () => { it('accepts empty array as body', async () => { const endpoint = server.safe( - makeObject({}), + makeVoid(), makeVoid(), makeObject({ param: makeNumber() }) as Maker, makeArray(makeString()), diff --git a/packages/oats/src/generate-types.ts b/packages/oats/src/generate-types.ts index 28a80e73..6707482d 100644 --- a/packages/oats/src/generate-types.ts +++ b/packages/oats/src/generate-types.ts @@ -17,7 +17,6 @@ interface ImportDefinition { // bit of a lie here really const voidSchema: oas.SchemaObject = { type: 'void' as any }; -const emptyObjectSchema: oas.SchemaObject = { type: 'object', additionalProperties: false }; export type Resolve = ( ref: string, @@ -243,15 +242,16 @@ export function run(options: Options) { paramSchema: undefined | ReadonlyArray, oasSchema: oas.OpenAPIObject ) { + const noQueryParams = { type: 'object' as const, additionalProperties: false }; if (!paramSchema) { - return generateTopLevelType(op, emptyObjectSchema); + return generateTopLevelType(op, noQueryParams); } const schema = oautil.deref(paramSchema, oasSchema); const queryParams = schema .map(schema => oautil.deref(schema, oasSchema)) .filter(schema => schema.in === 'query'); if (queryParams.length === 0) { - return generateTopLevelType(op, emptyObjectSchema); + return generateTopLevelType(op, noQueryParams); } if (queryParams.some(param => !!param.explode)) { assert(queryParams.length === 1, 'only one explode: true parameter is supported'); @@ -277,8 +277,7 @@ export function run(options: Options) { oasSchema: oas.OpenAPIObject, normalize = (name: string) => name ) { - const empty = generateTopLevelType(op, type === 'header' ? emptyObjectSchema : voidSchema); - + const empty = generateTopLevelType(op, voidSchema); if (!paramSchema) { return empty; } diff --git a/test/fetch-adapter/fetch-adapter.spec.ts b/test/fetch-adapter/fetch-adapter.spec.ts index 22368fc5..a9ccbdbb 100644 --- a/test/fetch-adapter/fetch-adapter.spec.ts +++ b/test/fetch-adapter/fetch-adapter.spec.ts @@ -78,7 +78,7 @@ describe('fetch adapter', () => { expect(receivedContext.method).toEqual('get'); expect(receivedContext.path).toEqual('/'); expect(receivedContext.query).toEqual({}); - expect(receivedContext.headers).toEqual({}); + expect(receivedContext.headers).toEqual(null); expect(receivedContext.body).toEqual(null); }); @@ -94,7 +94,7 @@ describe('fetch adapter', () => { expect(receivedContext.method).toEqual('get'); expect(receivedContext.path).toEqual('/with-query'); expect(receivedContext.query).toEqual({ one: 'the loneliest number' }); - expect(receivedContext.headers).toEqual({}); + expect(receivedContext.headers).toEqual(null); expect(receivedContext.body).toEqual(null); }); @@ -110,7 +110,7 @@ describe('fetch adapter', () => { expect(receivedContext.method).toEqual('get'); expect(receivedContext.path).toEqual('/with-array-query'); expect(receivedContext.query).toEqual({ numbers: ['one', 'two'] }); - expect(receivedContext.headers).toEqual({}); + expect(receivedContext.headers).toEqual(null); expect(receivedContext.body).toEqual(null); }); @@ -143,7 +143,7 @@ describe('fetch adapter', () => { expect(receivedContext).toBeDefined(); expect(receivedContext.method).toEqual('post'); expect(receivedContext.path).toEqual('/json-body'); - expect(receivedContext.headers).toEqual({}); + expect(receivedContext.headers).toEqual(null); expect(receivedContext.body).toEqual({ contentType: 'application/json', value: { one: 'the loneliest number' } @@ -165,7 +165,7 @@ describe('fetch adapter', () => { expect(receivedContext).toBeDefined(); expect(receivedContext.method).toEqual('patch'); expect(receivedContext.path).toEqual('/with-patch'); - expect(receivedContext.headers).toEqual({}); + expect(receivedContext.headers).toEqual(null); expect(receivedContext.body).toEqual({ contentType: 'application/json', value: { one: 'the loneliest number' } diff --git a/test/request-headers/server-parse-headers.spec.ts b/test/request-headers/server-parse-headers.spec.ts index 9efb77e0..0eecd40c 100644 --- a/test/request-headers/server-parse-headers.spec.ts +++ b/test/request-headers/server-parse-headers.spec.ts @@ -8,10 +8,10 @@ import { AddressInfo } from 'net'; import { Axios } from 'axios'; describe('server parse headers', () => { - let parsedHeaders: object | null = null; + let parsedHeadersRef: { current: object | void } | null = null; beforeEach(() => { - parsedHeaders = null; + parsedHeadersRef = null; }); const getRoutes = () => @@ -20,21 +20,21 @@ describe('server parse headers', () => { { '/send-required-header': { get: async ctx => { - parsedHeaders = ctx.headers; + parsedHeadersRef = { current: ctx.headers }; return runtime.noContent(204); } }, '/send-optional-header': { get: async ctx => { - parsedHeaders = ctx.headers; + parsedHeadersRef = { current: ctx.headers }; return runtime.noContent(204); } }, '/send-no-headers': { get: async ctx => { - parsedHeaders = ctx.headers; + parsedHeadersRef = { current: ctx.headers }; return runtime.noContent(204); } @@ -100,7 +100,7 @@ describe('server parse headers', () => { expect(response.data).toContain( 'invalid request headers authorization: expected a string, but got `undefined` instead.' ); - expect(parsedHeaders).toBeNull(); + expect(parsedHeadersRef).toBeNull(); }); it('should drop unknown headers', async () => { @@ -109,8 +109,7 @@ describe('server parse headers', () => { }); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({ authorization: 'basic auth' }); - expect('unknown' in parsedHeaders!).toBe(false); + expect(parsedHeadersRef).toEqual({ current: { authorization: 'basic auth' } }); }); it('should accept request with required headers', async () => { @@ -119,7 +118,7 @@ describe('server parse headers', () => { }); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({ authorization: 'basic auth' }); + expect(parsedHeadersRef).toEqual({ current: { authorization: 'basic auth' } }); }); }); @@ -130,7 +129,7 @@ describe('server parse headers', () => { const response = await axios.get(endpointUrl); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({}); + expect(parsedHeadersRef).toEqual({ current: {} }); }); it('should include optional headers', async () => { @@ -139,7 +138,7 @@ describe('server parse headers', () => { }); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({ 'x-request-id': 'testrequestid' }); + expect(parsedHeadersRef).toEqual({ current: { 'x-request-id': 'testrequestid' } }); }); }); @@ -152,14 +151,14 @@ describe('server parse headers', () => { }); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({}); + expect(parsedHeadersRef).toEqual({ current: null }); }); it('should accept request without headers', async () => { const response = await axios.get(endpointUrl); expect(response.status).toBe(204); - expect(parsedHeaders).toEqual({}); + expect(parsedHeadersRef).toEqual({ current: null }); }); }); });