From e2e91147dc27b36958545422c0778eda3e1de884 Mon Sep 17 00:00:00 2001 From: Caleb Barnes Date: Thu, 16 Jan 2025 14:07:02 -0800 Subject: [PATCH] fix(js-client): handle type definitions for request body params (#6018) * fix(js-client): handle type definitions for requestBody Handling type definitions for requestBody in API schema while still taking into consideration optional / void params if all path, query, and requestBody keys are optional. * fix(js-client): add reference docs links and code example to NetlifyAPI client definition comment Improve DX by adding links to docs and small code sample. * fix(js-client): add types for request body json and octet stream - combined the request body type with the Params type - handle cases where request body properties are optional - handle cases where all params and request body are optional - allow 'application/json' body to be a JSON object or a function returning one - allow 'application/octet-stream' body to be a Node.js ReadStream or a function returning one - simplify by extracting repetitive logic to helper types like `IsParamsOrRequestBodyRequired` and `IsRequestBodyOptional` etc. * refactor(js-client): rename RequestBodyDecorator to DetailedRequestBody for clarity These types are responsible for adding detailed annotations to the `body` parameter of API methods, such as describing its usage and examples for `application/json` and `application/octet-stream`. The new names make their purpose more explicit and improve readability for future maintenance and usage. --- packages/js-client/src/index.ts | 11 +++ packages/js-client/src/types.ts | 128 +++++++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/packages/js-client/src/index.ts b/packages/js-client/src/index.ts index 507e9cc6c6..8a17b297d8 100644 --- a/packages/js-client/src/index.ts +++ b/packages/js-client/src/index.ts @@ -29,6 +29,17 @@ type APIOptions = { globalParams?: Record } +/** + * The Netlify API client. + * @see {@link https://open-api.netlify.com | Open API Reference} + * @see {@link https://docs.netlify.com/api/get-started | Online Documentation} + * + * @example + * ```ts + * const client = new NetlifyAPI('YOUR_ACCESS_TOKEN') + * const sites = await client.listSites() + * ``` + */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -- NetlifyAPI is a class and the interface just inherits mapped types export interface NetlifyAPI extends DynamicMethods {} diff --git a/packages/js-client/src/types.ts b/packages/js-client/src/types.ts index 565d0f714d..eba7d1fbe1 100644 --- a/packages/js-client/src/types.ts +++ b/packages/js-client/src/types.ts @@ -1,3 +1,5 @@ +import type { ReadStream } from 'node:fs' + import type { operations } from '@netlify/open-api' /** @@ -43,6 +45,117 @@ type SnakeToCamel = { */ type Params = SnakeToCamel | T +type HasRequestBody = 'requestBody' extends keyof operations[K] + ? operations[K]['requestBody'] extends { content: any } + ? 'application/json' extends keyof operations[K]['requestBody']['content'] + ? true + : 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] + ? true + : false + : false + : false + +/** + * Extracts the request body type from the operation. + */ +type RequestBody = 'requestBody' extends keyof operations[K] + ? operations[K]['requestBody'] extends { content: any } + ? 'application/json' extends keyof operations[K]['requestBody']['content'] + ? operations[K]['requestBody']['content']['application/json'] + : 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] + ? ReadStream | (() => ReadStream) + : never + : never + : never + +type IsRequestBodyJson = 'requestBody' extends keyof operations[K] + ? operations[K]['requestBody'] extends { content: any } + ? 'application/json' extends keyof operations[K]['requestBody']['content'] + ? true + : false + : false + : false + +type IsRequestBodyOctetStream = 'requestBody' extends keyof operations[K] + ? operations[K]['requestBody'] extends { content: any } + ? 'application/octet-stream' extends keyof operations[K]['requestBody']['content'] + ? true + : false + : false + : false + +type RequestBodyParam = + HasRequestBody extends true + ? IsRequestBodyOptional extends true + ? DetailedRequestBodyOptional + : DetailedRequestBody + : never + +type DetailedRequestBody = + IsRequestBodyJson extends true + ? { + /** + * The request body for `application/json`. + * Automatically serialized to JSON based on the operation. + * Can be a JSON object or a function returning one. + */ + body: RequestBody | (() => RequestBody) + } + : IsRequestBodyOctetStream extends true + ? { + /** + * The request body for `application/octet-stream`. + * Can be a Node.js readable stream or a function returning one + * @example + * fs.createReadStream('./file') + * @example + * () => fs.createReadStream('./file') + */ + body: ReadStream | (() => ReadStream) + } + : never + +type DetailedRequestBodyOptional = + IsRequestBodyJson extends true + ? { + /** + * The request body for `application/json`. + * Automatically serialized to JSON based on the operation. + * Can be a JSON object or a function returning one. + */ + body?: RequestBody | (() => RequestBody) + } + : IsRequestBodyOctetStream extends true + ? { + /** + * The request body for `application/octet-stream`. + * Can be a Node.js readable stream or a function returning one + * @example + * fs.createReadStream('./file') + * @example + * () => fs.createReadStream('./file') + */ + body?: ReadStream | (() => ReadStream) + } + : never + +/** + * Determines whether all properties in the request body are optional. + */ +type IsRequestBodyOptional = + HasRequestBody extends true ? (AreAllOptional> extends true ? true : false) : true + +/** + * Determines whether any parameters or request body are required. + */ +type IsParamsOrRequestBodyRequired = 'parameters' extends keyof operations[K] + ? IsPathAndQueryOptional extends true + ? IsRequestBodyOptional extends true + ? false + : true + : true + : false + /** * Extracts and combines `path` and `query` parameters into a single type. */ @@ -59,11 +172,18 @@ type ExtractPathAndQueryParameters = 'parameters' ex : undefined : undefined -type OperationParams = 'parameters' extends keyof operations[K] - ? IsPathAndQueryOptional extends true - ? ExtractPathAndQueryParameters | void +/** + * Combines path, query, and request body parameters into a single type. + */ +type CombinedParamsAndRequestBody = + HasRequestBody extends true + ? ExtractPathAndQueryParameters & RequestBodyParam : ExtractPathAndQueryParameters - : void + +type OperationParams = + IsParamsOrRequestBodyRequired extends false + ? CombinedParamsAndRequestBody | void + : CombinedParamsAndRequestBody type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 /**