Skip to content

Commit

Permalink
fix(js-client): handle type definitions for request body params (#6018)
Browse files Browse the repository at this point in the history
* 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.
CalebBarnes authored Jan 16, 2025
1 parent ad7715b commit e2e9114
Showing 2 changed files with 135 additions and 4 deletions.
11 changes: 11 additions & 0 deletions packages/js-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,17 @@ type APIOptions = {
globalParams?: Record<string, unknown>
}

/**
* 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 {}

128 changes: 124 additions & 4 deletions packages/js-client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ReadStream } from 'node:fs'

import type { operations } from '@netlify/open-api'

/**
@@ -43,6 +45,117 @@ type SnakeToCamel<T> = {
*/
type Params<T> = SnakeToCamel<T> | T

type HasRequestBody<K extends keyof operations> = '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<K extends keyof operations> = '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<K extends keyof operations> = '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<K extends keyof operations> = '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<K extends keyof operations> =
HasRequestBody<K> extends true
? IsRequestBodyOptional<K> extends true
? DetailedRequestBodyOptional<K>
: DetailedRequestBody<K>
: never

type DetailedRequestBody<K extends keyof operations> =
IsRequestBodyJson<K> 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<K> | (() => RequestBody<K>)
}
: IsRequestBodyOctetStream<K> 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<K extends keyof operations> =
IsRequestBodyJson<K> 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<K> | (() => RequestBody<K>)
}
: IsRequestBodyOctetStream<K> 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<K extends keyof operations> =
HasRequestBody<K> extends true ? (AreAllOptional<RequestBody<K>> extends true ? true : false) : true

/**
* Determines whether any parameters or request body are required.
*/
type IsParamsOrRequestBodyRequired<K extends keyof operations> = 'parameters' extends keyof operations[K]
? IsPathAndQueryOptional<K> extends true
? IsRequestBodyOptional<K> extends true
? false
: true
: true
: false

/**
* Extracts and combines `path` and `query` parameters into a single type.
*/
@@ -59,11 +172,18 @@ type ExtractPathAndQueryParameters<K extends keyof operations> = 'parameters' ex
: undefined
: undefined

type OperationParams<K extends keyof operations> = 'parameters' extends keyof operations[K]
? IsPathAndQueryOptional<K> extends true
? ExtractPathAndQueryParameters<K> | void
/**
* Combines path, query, and request body parameters into a single type.
*/
type CombinedParamsAndRequestBody<K extends keyof operations> =
HasRequestBody<K> extends true
? ExtractPathAndQueryParameters<K> & RequestBodyParam<K>
: ExtractPathAndQueryParameters<K>
: void

type OperationParams<K extends keyof operations> =
IsParamsOrRequestBodyRequired<K> extends false
? CombinedParamsAndRequestBody<K> | void
: CombinedParamsAndRequestBody<K>

type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
/**

0 comments on commit e2e9114

Please sign in to comment.