Skip to content

Commit

Permalink
feat(js-client): add type definitions (#6009)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
CalebBarnes authored Jan 13, 2025
1 parent a6d31ce commit 9894c6b
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 15 deletions.
22 changes: 7 additions & 15 deletions packages/js-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,8 +29,11 @@ type APIOptions = {
globalParams?: Record<string, unknown>
}

// 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<string, string> = {
accept: 'application/json',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
54 changes: 54 additions & 0 deletions packages/js-client/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { operations } from '@netlify/open-api'

/**
* Converts snake_case to camelCase for TypeScript types.
*/
type CamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<CamelCase<U>>}` : S

/**
* Creates a union of both snake_case and camelCase keys with their respective types.
*/
type SnakeToCamel<T> = {
[K in keyof T as CamelCase<K & string>]: T[K]
}

/**
* Combines snake_case and camelCase parameters.
*/
type CombinedCaseParams<T> = SnakeToCamel<T> | T

/**
* Combines `path` and `query` parameters into a single type.
*/
type OperationParams<K extends keyof operations> = 'parameters' extends keyof operations[K]
? 'path' extends keyof operations[K]['parameters']
? 'query' extends keyof operations[K]['parameters']
? CombinedCaseParams<
Omit<operations[K]['parameters']['path'], keyof operations[K]['parameters']['query']> &
operations[K]['parameters']['query']
>
: CombinedCaseParams<operations[K]['parameters']['path']>
: 'query' extends keyof operations[K]['parameters']
? CombinedCaseParams<operations[K]['parameters']['query']>
: undefined
: undefined

type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
/**
* Extracts the response type from the operation.
*/
type OperationResponse<K extends keyof operations> = '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<K>) => Promise<OperationResponse<K>>
}

0 comments on commit 9894c6b

Please sign in to comment.