From 9894c6b4feaa5821505409a17a492f91218cbdfa Mon Sep 17 00:00:00 2001 From: Caleb Barnes Date: Mon, 13 Jan 2025 09:36:48 -0800 Subject: [PATCH] feat(js-client): add type definitions (#6009) * feat(js-client): add type definitions Add type definitions for dynamic client methods by defining the NetlifyAPI class as an interface that inherits mapped types. * feat(js-client): treat path params and query params as one object The api client methods will assign these to the correct places but just accept the params as a single object. * fix(js-client): update accessToken type to allow undefined The api response for accessToken can possibly be undefined but we were only allowing string or null before. * feat(js-client): handle snake_case and camelCase params in type definitions Since the API client allows using camelCased params we need to allow these to keep backwards compatibility. --- packages/js-client/src/index.ts | 22 +++++--------- packages/js-client/src/types.ts | 54 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 packages/js-client/src/types.ts diff --git a/packages/js-client/src/index.ts b/packages/js-client/src/index.ts index c4a44d410e..507e9cc6c6 100644 --- a/packages/js-client/src/index.ts +++ b/packages/js-client/src/index.ts @@ -3,6 +3,7 @@ import pWaitFor from 'p-wait-for' import { getMethods } from './methods/index.js' import { openApiSpec } from './open_api.js' import { getOperations } from './operations.js' +import type { DynamicMethods } from './types.js' // 1 second const DEFAULT_TICKET_POLL = 1e3 @@ -28,8 +29,11 @@ type APIOptions = { globalParams?: Record } +// 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 {} + export class NetlifyAPI { - #accessToken: string | null = null + #accessToken: string | undefined | null = null defaultHeaders: Record = { accept: 'application/json', @@ -66,11 +70,11 @@ export class NetlifyAPI { } /** Retrieves the access token */ - get accessToken(): string | null { + get accessToken(): string | undefined | null { return this.#accessToken } - set accessToken(token: string | null) { + set accessToken(token: string | undefined | null) { if (!token) { delete this.defaultHeaders.Authorization this.#accessToken = null @@ -108,18 +112,6 @@ export class NetlifyAPI { this.accessToken = accessTokenResponse.access_token return accessTokenResponse.access_token } - - // Those methods are getting implemented by the Object.assign(this, { ...methods }) in the constructor - // This is a way where we can still maintain proper types while not implementing them. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - showTicket(_config: { ticketId: string }): Promise<{ authorized: boolean }> { - throw new Error('Will be overridden in constructor!') - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - exchangeTicket(_config: { ticketId: string }): Promise<{ access_token: string }> { - throw new Error('Will be overridden in constructor!') - } } export const methods = getOperations() diff --git a/packages/js-client/src/types.ts b/packages/js-client/src/types.ts new file mode 100644 index 0000000000..7e6520ea16 --- /dev/null +++ b/packages/js-client/src/types.ts @@ -0,0 +1,54 @@ +import type { operations } from '@netlify/open-api' + +/** + * Converts snake_case to camelCase for TypeScript types. + */ +type CamelCase = S extends `${infer T}_${infer U}` ? `${T}${Capitalize>}` : S + +/** + * Creates a union of both snake_case and camelCase keys with their respective types. + */ +type SnakeToCamel = { + [K in keyof T as CamelCase]: T[K] +} + +/** + * Combines snake_case and camelCase parameters. + */ +type CombinedCaseParams = SnakeToCamel | T + +/** + * Combines `path` and `query` parameters into a single type. + */ +type OperationParams = 'parameters' extends keyof operations[K] + ? 'path' extends keyof operations[K]['parameters'] + ? 'query' extends keyof operations[K]['parameters'] + ? CombinedCaseParams< + Omit & + operations[K]['parameters']['query'] + > + : CombinedCaseParams + : 'query' extends keyof operations[K]['parameters'] + ? CombinedCaseParams + : undefined + : undefined + +type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 +/** + * Extracts the response type from the operation. + */ +type OperationResponse = 'responses' extends keyof operations[K] + ? SuccessHttpStatusCodes extends infer StatusKeys + ? StatusKeys extends keyof operations[K]['responses'] + ? 'content' extends keyof operations[K]['responses'][StatusKeys] + ? 'application/json' extends keyof operations[K]['responses'][StatusKeys]['content'] + ? operations[K]['responses'][StatusKeys]['content']['application/json'] + : never + : never + : never + : never + : never + +export type DynamicMethods = { + [K in keyof operations]: (params: OperationParams) => Promise> +}