Skip to content

Commit 9894c6b

Browse files
authored
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.
1 parent a6d31ce commit 9894c6b

File tree

2 files changed

+61
-15
lines changed

2 files changed

+61
-15
lines changed

Diff for: packages/js-client/src/index.ts

+7-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import pWaitFor from 'p-wait-for'
33
import { getMethods } from './methods/index.js'
44
import { openApiSpec } from './open_api.js'
55
import { getOperations } from './operations.js'
6+
import type { DynamicMethods } from './types.js'
67

78
// 1 second
89
const DEFAULT_TICKET_POLL = 1e3
@@ -28,8 +29,11 @@ type APIOptions = {
2829
globalParams?: Record<string, unknown>
2930
}
3031

32+
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- NetlifyAPI is a class and the interface just inherits mapped types
33+
export interface NetlifyAPI extends DynamicMethods {}
34+
3135
export class NetlifyAPI {
32-
#accessToken: string | null = null
36+
#accessToken: string | undefined | null = null
3337

3438
defaultHeaders: Record<string, string> = {
3539
accept: 'application/json',
@@ -66,11 +70,11 @@ export class NetlifyAPI {
6670
}
6771

6872
/** Retrieves the access token */
69-
get accessToken(): string | null {
73+
get accessToken(): string | undefined | null {
7074
return this.#accessToken
7175
}
7276

73-
set accessToken(token: string | null) {
77+
set accessToken(token: string | undefined | null) {
7478
if (!token) {
7579
delete this.defaultHeaders.Authorization
7680
this.#accessToken = null
@@ -108,18 +112,6 @@ export class NetlifyAPI {
108112
this.accessToken = accessTokenResponse.access_token
109113
return accessTokenResponse.access_token
110114
}
111-
112-
// Those methods are getting implemented by the Object.assign(this, { ...methods }) in the constructor
113-
// This is a way where we can still maintain proper types while not implementing them.
114-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
115-
showTicket(_config: { ticketId: string }): Promise<{ authorized: boolean }> {
116-
throw new Error('Will be overridden in constructor!')
117-
}
118-
119-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
120-
exchangeTicket(_config: { ticketId: string }): Promise<{ access_token: string }> {
121-
throw new Error('Will be overridden in constructor!')
122-
}
123115
}
124116

125117
export const methods = getOperations()

Diff for: packages/js-client/src/types.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { operations } from '@netlify/open-api'
2+
3+
/**
4+
* Converts snake_case to camelCase for TypeScript types.
5+
*/
6+
type CamelCase<S extends string> = S extends `${infer T}_${infer U}` ? `${T}${Capitalize<CamelCase<U>>}` : S
7+
8+
/**
9+
* Creates a union of both snake_case and camelCase keys with their respective types.
10+
*/
11+
type SnakeToCamel<T> = {
12+
[K in keyof T as CamelCase<K & string>]: T[K]
13+
}
14+
15+
/**
16+
* Combines snake_case and camelCase parameters.
17+
*/
18+
type CombinedCaseParams<T> = SnakeToCamel<T> | T
19+
20+
/**
21+
* Combines `path` and `query` parameters into a single type.
22+
*/
23+
type OperationParams<K extends keyof operations> = 'parameters' extends keyof operations[K]
24+
? 'path' extends keyof operations[K]['parameters']
25+
? 'query' extends keyof operations[K]['parameters']
26+
? CombinedCaseParams<
27+
Omit<operations[K]['parameters']['path'], keyof operations[K]['parameters']['query']> &
28+
operations[K]['parameters']['query']
29+
>
30+
: CombinedCaseParams<operations[K]['parameters']['path']>
31+
: 'query' extends keyof operations[K]['parameters']
32+
? CombinedCaseParams<operations[K]['parameters']['query']>
33+
: undefined
34+
: undefined
35+
36+
type SuccessHttpStatusCodes = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
37+
/**
38+
* Extracts the response type from the operation.
39+
*/
40+
type OperationResponse<K extends keyof operations> = 'responses' extends keyof operations[K]
41+
? SuccessHttpStatusCodes extends infer StatusKeys
42+
? StatusKeys extends keyof operations[K]['responses']
43+
? 'content' extends keyof operations[K]['responses'][StatusKeys]
44+
? 'application/json' extends keyof operations[K]['responses'][StatusKeys]['content']
45+
? operations[K]['responses'][StatusKeys]['content']['application/json']
46+
: never
47+
: never
48+
: never
49+
: never
50+
: never
51+
52+
export type DynamicMethods = {
53+
[K in keyof operations]: (params: OperationParams<K>) => Promise<OperationResponse<K>>
54+
}

0 commit comments

Comments
 (0)