15
15
* limitations under the License.
16
16
*/
17
17
18
- import {
18
+ import type {
19
19
APIVersion ,
20
20
ApiResponse ,
21
21
NameOrSnowflake ,
@@ -24,24 +24,41 @@ import {
24
24
Route ,
25
25
responses
26
26
} from '@ncharts/types' ;
27
+
27
28
import { DEFAULT_API_VERSION , DEFAULT_BASE_URL } from './constants' ;
28
- import type { AbstractAuthStrategy } from './auth' ;
29
29
import { hasOwnProperty , isBrowser , isObject } from '@noelware/utils' ;
30
+ import { transformJSON , transformYaml } from './internal' ;
31
+ import type { AbstractAuthStrategy } from './auth' ;
32
+ import { OrganizationContainer } from './containers/organizations' ;
33
+ import { ApiKeysContainer } from './containers/apikeys' ;
30
34
import { UserContainer } from './containers/users' ;
31
35
import { HTTPError } from './errors/HTTPError' ;
32
36
import assert from 'assert' ;
33
37
import defu from 'defu' ;
34
- import { transformJSON , transformYaml } from './internal' ;
35
38
36
39
export type HTTPMethod = 'get' | 'put' | 'head' | 'post' | 'patch' | 'delete' ;
37
40
export const Methods : readonly HTTPMethod [ ] = [ 'get' , 'put' , 'head' , 'post' , 'patch' , 'delete' ] as const ;
38
41
42
+ /**
43
+ * Fetch implementation blue-print.
44
+ */
39
45
export type Fetch = ( input : RequestInit | URL , init ?: RequestInit ) => Promise < Response > ;
40
46
47
+ /**
48
+ * FormData implementation blue-print
49
+ */
50
+ export interface FormData {
51
+ new ( ...args : any [ ] ) : FormData ;
52
+
53
+ append ( name : string , value : any , fileName ?: string ) : void ;
54
+ getBoundary ?( ) : void ;
55
+ }
56
+
41
57
// @ts -ignore
42
- const containers : Readonly < [ [ string , new ( client : Client , ...args : any [ ] ) => any ] ] > = [
43
- [ 'users' , UserContainer ] ,
44
- [ '@me' , UserContainer ]
58
+ const containers : Readonly < [ [ string , new ( client : Client , ...args : any [ ] ) => any , boolean ] ] > = [
59
+ [ 'organizations' , OrganizationContainer , true ] ,
60
+ [ 'apikeys' , ApiKeysContainer , false ] ,
61
+ [ 'users' , UserContainer , true ]
45
62
] ;
46
63
47
64
/**
@@ -56,6 +73,15 @@ export interface ClientOptions {
56
73
*/
57
74
apiVersion ?: APIVersion | 'latest' ;
58
75
76
+ /**
77
+ * {@link FormData } implementation when sending `multipart/form-data` requests. This will opt into the global's
78
+ * FormData implementation (if in the browser), or it will error when sending requests in.
79
+ *
80
+ * To avoid any errors when requesting data, you will need to install the [form-data](https://npm.im/form-data)
81
+ * Node.js package to send form data.
82
+ */
83
+ FormData ?: { new ( ...args : any [ ] ) : FormData } ;
84
+
59
85
/**
60
86
* Base URL to send requests to
61
87
*
@@ -102,9 +128,9 @@ export interface RequestOptions<R extends Route, Method extends HTTPMethod, Body
102
128
fetchOptions ?: Omit < RequestInit , 'body' | 'headers' | 'method' | 'window' > ;
103
129
104
130
/**
105
- * The determined `Content-Type` value to use
131
+ * The determined `Content-Type` value to use.
106
132
*/
107
- contentType ?: string ;
133
+ contentType ?: 'application/json' | 'form-data' ;
108
134
109
135
/**
110
136
* Any additional headers to append to this request
@@ -138,6 +164,7 @@ const kClientOptions = {
138
164
export class Client {
139
165
#authStrategy: AbstractAuthStrategy | undefined ;
140
166
#apiVersion: APIVersion | 'latest' ;
167
+ #FormData: { new ( ...args : any [ ] ) : FormData } ;
141
168
#baseURL: string ;
142
169
#headers: Record < string , string > ;
143
170
#fetch: Fetch ;
@@ -148,65 +175,88 @@ export class Client {
148
175
* @param options Request options, if any.
149
176
* @return Standard web HTTP response.
150
177
*/
151
- delete ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'delete' , Body > ) => Promise < Response > ;
178
+ readonly delete ! : < Body , R extends Route > (
179
+ endpoint : R ,
180
+ options ?: RequestOptions < R , 'delete' , Body >
181
+ ) => Promise < Response > ;
152
182
153
183
/**
154
184
* Sends a PATCH request to the API server.
155
185
* @param endpoint The endpoint to send the request to
156
186
* @param options Request options, if any.
157
187
* @return Standard web HTTP response.
158
188
*/
159
- patch ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'patch' , Body > ) => Promise < Response > ;
189
+ readonly patch ! : < Body , R extends Route > (
190
+ endpoint : R ,
191
+ options ?: RequestOptions < R , 'patch' , Body >
192
+ ) => Promise < Response > ;
160
193
161
194
/**
162
195
* Sends a POST request to the API server.
163
196
* @param endpoint The endpoint to send the request to
164
197
* @param options Request options, if any.
165
198
* @return Standard web HTTP response.
166
199
*/
167
- post ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'post' , Body > ) => Promise < Response > ;
200
+ readonly post ! : < Body , R extends Route > (
201
+ endpoint : R ,
202
+ options ?: RequestOptions < R , 'post' , Body >
203
+ ) => Promise < Response > ;
168
204
169
205
/**
170
206
* Sends a HEAD request to the API server.
171
207
* @param endpoint The endpoint to send the request to
172
208
* @param options Request options, if any.
173
209
* @return Standard web HTTP response.
174
210
*/
175
- head ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'head' , Body > ) => Promise < Response > ;
211
+ readonly head ! : < Body , R extends Route > (
212
+ endpoint : R ,
213
+ options ?: RequestOptions < R , 'head' , Body >
214
+ ) => Promise < Response > ;
176
215
177
216
/**
178
217
* Sends a PUT request to the API server.
179
218
* @param endpoint The endpoint to send the request to
180
219
* @param options Request options, if any.
181
220
* @return Standard web HTTP response.
182
221
*/
183
- put ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'put' , Body > ) => Promise < Response > ;
222
+ readonly put ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'put' , Body > ) => Promise < Response > ;
184
223
185
224
/**
186
225
* Sends a GET request to the API server.
187
226
* @param endpoint The endpoint to send the request to
188
227
* @param options Request options, if any.
189
228
* @return Standard web HTTP response.
190
229
*/
191
- get ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'get' , Body > ) => Promise < Response > ;
230
+ readonly get ! : < Body , R extends Route > ( endpoint : R , options ?: RequestOptions < R , 'get' , Body > ) => Promise < Response > ;
231
+
232
+ /**
233
+ * Returns a {@link OrganizationContainer} based on the passed-in {@link NameOrSnowflake}.
234
+ * @param idOrName The organization name or ID to pass in.
235
+ * @return The {@link OrganizationContainer} to do such methods.
236
+ */
237
+ readonly organizations ! : ( idOrName : NameOrSnowflake ) => OrganizationContainer ;
238
+
239
+ /** Container for sending requests to the API Keys API. */
240
+ readonly apikeys ! : ApiKeysContainer ;
192
241
193
242
/**
194
243
* Returns a {@link UserContainer} based on the passed-in {@link NameOrSnowflake}.
195
244
* @param idOrName The username or the user's ID to pass in.
196
245
* @return The {@link UserContainer} to do such methods.
197
246
*/
198
- users ! : ( idOrName : NameOrSnowflake ) => UserContainer ;
247
+ readonly users ! : ( idOrName : NameOrSnowflake ) => UserContainer ;
199
248
200
249
/** {@link UserContainer } that redirects requests to `/users/@me`. */
201
- me ! : UserContainer ;
250
+ readonly me ! : UserContainer ;
202
251
203
252
constructor ( options : ClientOptions = kClientOptions ) {
204
253
this . #authStrategy = options . auth ;
205
254
this . #apiVersion = options . apiVersion || 1 ;
206
255
this . #baseURL = options . baseURL || DEFAULT_BASE_URL ;
207
256
this . #headers = options . headers || { } ;
208
257
209
- if ( global . fetch === undefined || options . fetch === undefined ) {
258
+ // if there is no `global.fetch` impl and `options.fetch` is not defined
259
+ if ( global . fetch === undefined && options . fetch === undefined ) {
210
260
const [ major , minor ] = process . version . split ( '.' ) . map ( Number ) ;
211
261
if ( ( major < 16 && minor < 15 ) || ( major === 17 && minor < 5 ) ) {
212
262
throw new Error (
@@ -222,6 +272,8 @@ export class Client {
222
272
// @ts -ignore
223
273
this . #fetch = global . fetch || options . fetch ;
224
274
275
+ // @ts -ignore
276
+ this . #FormData = global . FormData || options . FormData ;
225
277
for ( const method of Methods ) {
226
278
this [ method ] = function (
227
279
this : Client ,
@@ -232,15 +284,17 @@ export class Client {
232
284
} ;
233
285
}
234
286
235
- for ( const [ key , cls ] of containers ) {
236
- if ( key === '@me' && this [ key ] === undefined ) {
237
- this [ key ] = new cls ( this , '@me' ) ;
238
- }
287
+ for ( const [ key , cls , isFunction ] of containers ) {
288
+ if ( this [ key ] ) continue ;
239
289
240
- this [ key ] = function ( this : Client , ...args : any [ ] ) {
241
- return new cls ( this , ...args ) ;
242
- } ;
290
+ this [ key ] = isFunction
291
+ ? function ( this : Client , ...args : any [ ] ) {
292
+ return new cls ( this , ...args ) ;
293
+ }
294
+ : new cls ( this ) ;
243
295
}
296
+
297
+ this . me = new UserContainer ( this , '@me' ) ;
244
298
}
245
299
246
300
// @ts -ignore
@@ -280,6 +334,20 @@ export class Client {
280
334
281
335
body = JSON . stringify ( options . body ) ;
282
336
}
337
+
338
+ const FormData = this . #FormData;
339
+ if ( options . contentType === 'form-data' && ! FormData )
340
+ throw new Error ( 'Missing form-data dependency in Node.js' ) ;
341
+
342
+ if ( options . contentType === 'form-data' && options . body instanceof FormData ) {
343
+ const body = options . body as FormData ;
344
+ if ( body . getBoundary !== undefined ) {
345
+ headers [ 'content-type' ] = `multipart/form-data; boundary=${ body . getBoundary ( ) } ` ;
346
+ }
347
+ }
348
+
349
+ // we can't infer it, so we'll just do it here lol
350
+ body = options . body as any ;
283
351
}
284
352
}
285
353
@@ -294,7 +362,7 @@ export class Client {
294
362
} ;
295
363
296
364
if ( body !== null ) fetchOptions . body = body ;
297
- return this . #fetch( new URL ( url ) , fetchOptions ) ;
365
+ return this . #fetch( new URL ( url , this . #baseURL ) , fetchOptions ) ;
298
366
}
299
367
300
368
/**
@@ -308,7 +376,7 @@ export class Client {
308
376
* @returns An {@link Buffer} on Node.js, or a {@link ArrayBuffer} in the browser of a CDN object
309
377
* if the feature is enabled.
310
378
*/
311
- cdn ( prefix : string = '/cdn' , ...paths : string [ ] ) {
379
+ async cdn ( prefix : string = '/cdn' , ...paths : string [ ] ) : Promise < responses . main . CDN > {
312
380
let path = '' ;
313
381
if ( ! paths . length ) path = '/' ;
314
382
else {
@@ -317,29 +385,38 @@ export class Client {
317
385
}
318
386
}
319
387
320
- return new Promise < responses . main . CDN > ( ( resolve , reject ) =>
321
- this . get ( `/${ prefix } ${ path } ` as unknown as Route )
322
- . then ( async ( resp ) => {
323
- if ( ! resp . ok ) {
324
- if ( resp . status === 404 ) {
325
- return reject ( new Error ( "Server doesn't have the CDN feature enabled" ) ) ;
388
+ const resp = await this . get ( `/${ prefix } ${ path } ` as unknown as Route ) ;
389
+ if ( ! resp . ok ) {
390
+ if ( resp . status === 404 ) throw new Error ( 'Server does not have CDN feature enabled' ) ;
391
+
392
+ const data = await transformJSON <
393
+ Exclude < ApiResponse < never , { sdk : true ; error : unknown } | undefined > , { success : true } >
394
+ > ( resp ) . catch ( ( err ) => ( {
395
+ success : false ,
396
+ errors : [
397
+ {
398
+ code : 'UNABLE_TO_PARSE' ,
399
+ message : err . message ,
400
+ detail : {
401
+ sdk : true ,
402
+ error : err
326
403
}
327
-
328
- return reject ( new HTTPError ( resp . status ) ) ;
329
404
}
405
+ ]
406
+ } ) ) ;
330
407
331
- const buf = await resp . arrayBuffer ( ) ;
332
- if ( isBrowser ) return resolve ( buf ) ;
408
+ throw new HTTPError ( resp . status , hasOwnProperty ( data , 'errors' ) ? data . errors : [ ] ) ;
409
+ }
333
410
334
- const buffer = Buffer . alloc ( buf . byteLength ) ;
335
- for ( let i = 0 ; i < buffer . length ; i ++ ) {
336
- buffer [ i ] = buf [ i ] ;
337
- }
411
+ const buf = await resp . arrayBuffer ( ) ;
412
+ if ( isBrowser ) return buf ;
338
413
339
- return resolve ( buf ) ;
340
- } )
341
- . catch ( reject )
342
- ) ;
414
+ const buffer = Buffer . alloc ( buf . byteLength ) ;
415
+ for ( let i = 0 ; i < buf . byteLength ; i ++ ) {
416
+ buffer [ i ] = buf [ i ] ;
417
+ }
418
+
419
+ return buffer ;
343
420
}
344
421
345
422
/**
@@ -386,13 +463,14 @@ export class Client {
386
463
* or an API response object (usually a 404 if it is not enabled).
387
464
*/
388
465
metrics (
466
+ path : string ,
389
467
options ?: Omit <
390
468
RequestOptions < '/features' , 'get' > ,
391
469
'pathParameters' | 'queryParameters' | 'body' | 'contentType'
392
470
>
393
471
) {
394
472
return new Promise < ApiResponse | string > ( ( resolve , reject ) =>
395
- this . get ( '/metrics' as unknown as Route , options ) . then ( ( resp ) =>
473
+ this . get ( path as unknown as Route , options ) . then ( ( resp ) =>
396
474
! resp . ok && resp . headers . get ( 'content-type' ) ?. includes ( 'application/json' )
397
475
? transformJSON < ApiResponse > ( resp ) . then ( resolve ) . catch ( reject )
398
476
: resp . text ( ) . then ( resolve ) . catch ( reject )
@@ -431,7 +509,12 @@ export class Client {
431
509
* .then(() => console.log('heartbeat was ok!'))
432
510
* .catch(console.error);
433
511
*/
434
- heartbeat ( options ?: RequestOptions < '/heartbeat' , 'head' > ) {
512
+ heartbeat (
513
+ options ?: Omit <
514
+ RequestOptions < '/heartbeat' , 'head' > ,
515
+ 'pathParameters' | 'queryParameters' | 'body' | 'contentType'
516
+ >
517
+ ) {
435
518
return new Promise < void > ( ( resolve , reject ) =>
436
519
this . head ( '/heartbeat' , options )
437
520
. then ( ( resp ) => {
@@ -446,13 +529,36 @@ export class Client {
446
529
) ;
447
530
}
448
531
449
- // indexMappings(id: string) {
450
- // return new Promise<responses.main.IndexMappings>((resolve, reject) =>
451
- // this.get('/indexes/{idOrName}', { contentType: 'application/json', pathParameters: {} }).then((resp) =>
452
- // transformYaml<responses.main.IndexMappings>(resp).then(resolve).catch(reject)
453
- // )
454
- // );
455
- // }
532
+ /**
533
+ * Retrieves a user or organization's chart index, which Helm will use to determine
534
+ * how to download a repository.
535
+ *
536
+ * @param idOrName The snowflake ID or user/organization name.
537
+ * @param options Request options
538
+ * @returns The {@link responses.main.IndexMappings IndexMappings} object,
539
+ * if a YAML parser is available (need to install [js-yaml](https://npm.im/js-yaml)),
540
+ * or a String if a YAML parser is not available.
541
+ */
542
+ indexMappings (
543
+ idOrName : NameOrSnowflake ,
544
+ options ?: Omit <
545
+ RequestOptions < '/indexes/{idOrName}' , 'get' > ,
546
+ 'pathParameters' | 'queryParameters' | 'body' | 'contentType'
547
+ >
548
+ ) : Promise < responses . main . IndexMappings > {
549
+ return new Promise ( ( resolve , reject ) =>
550
+ this . get ( '/indexes/{idOrName}' , {
551
+ contentType : 'application/json' ,
552
+
553
+ // @ts -ignore
554
+ pathParameters : {
555
+ idOrName
556
+ } ,
557
+
558
+ ...( options ?? { } )
559
+ } ) . then ( ( resp ) => transformYaml < responses . main . IndexMappings > ( resp ) . then ( resolve ) . catch ( reject ) )
560
+ ) ;
561
+ }
456
562
457
563
private _buildUrl < R extends Route > ( url : R , options ?: RequestOptions < R , HTTPMethod > ) {
458
564
let formedUrl = this . #baseURL;
0 commit comments