Skip to content

Commit 9c87dac

Browse files
committed
Update OpenAPI typings, add more methods
1 parent 7e4a68d commit 9c87dac

File tree

8 files changed

+4166
-3128
lines changed

8 files changed

+4166
-3128
lines changed

packages/sdk/src/client.ts

Lines changed: 159 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import {
18+
import type {
1919
APIVersion,
2020
ApiResponse,
2121
NameOrSnowflake,
@@ -24,24 +24,41 @@ import {
2424
Route,
2525
responses
2626
} from '@ncharts/types';
27+
2728
import { DEFAULT_API_VERSION, DEFAULT_BASE_URL } from './constants';
28-
import type { AbstractAuthStrategy } from './auth';
2929
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';
3034
import { UserContainer } from './containers/users';
3135
import { HTTPError } from './errors/HTTPError';
3236
import assert from 'assert';
3337
import defu from 'defu';
34-
import { transformJSON, transformYaml } from './internal';
3538

3639
export type HTTPMethod = 'get' | 'put' | 'head' | 'post' | 'patch' | 'delete';
3740
export const Methods: readonly HTTPMethod[] = ['get', 'put', 'head', 'post', 'patch', 'delete'] as const;
3841

42+
/**
43+
* Fetch implementation blue-print.
44+
*/
3945
export type Fetch = (input: RequestInit | URL, init?: RequestInit) => Promise<Response>;
4046

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+
4157
// @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]
4562
];
4663

4764
/**
@@ -56,6 +73,15 @@ export interface ClientOptions {
5673
*/
5774
apiVersion?: APIVersion | 'latest';
5875

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+
5985
/**
6086
* Base URL to send requests to
6187
*
@@ -102,9 +128,9 @@ export interface RequestOptions<R extends Route, Method extends HTTPMethod, Body
102128
fetchOptions?: Omit<RequestInit, 'body' | 'headers' | 'method' | 'window'>;
103129

104130
/**
105-
* The determined `Content-Type` value to use
131+
* The determined `Content-Type` value to use.
106132
*/
107-
contentType?: string;
133+
contentType?: 'application/json' | 'form-data';
108134

109135
/**
110136
* Any additional headers to append to this request
@@ -138,6 +164,7 @@ const kClientOptions = {
138164
export class Client {
139165
#authStrategy: AbstractAuthStrategy | undefined;
140166
#apiVersion: APIVersion | 'latest';
167+
#FormData: { new (...args: any[]): FormData };
141168
#baseURL: string;
142169
#headers: Record<string, string>;
143170
#fetch: Fetch;
@@ -148,65 +175,88 @@ export class Client {
148175
* @param options Request options, if any.
149176
* @return Standard web HTTP response.
150177
*/
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>;
152182

153183
/**
154184
* Sends a PATCH request to the API server.
155185
* @param endpoint The endpoint to send the request to
156186
* @param options Request options, if any.
157187
* @return Standard web HTTP response.
158188
*/
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>;
160193

161194
/**
162195
* Sends a POST request to the API server.
163196
* @param endpoint The endpoint to send the request to
164197
* @param options Request options, if any.
165198
* @return Standard web HTTP response.
166199
*/
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>;
168204

169205
/**
170206
* Sends a HEAD request to the API server.
171207
* @param endpoint The endpoint to send the request to
172208
* @param options Request options, if any.
173209
* @return Standard web HTTP response.
174210
*/
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>;
176215

177216
/**
178217
* Sends a PUT request to the API server.
179218
* @param endpoint The endpoint to send the request to
180219
* @param options Request options, if any.
181220
* @return Standard web HTTP response.
182221
*/
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>;
184223

185224
/**
186225
* Sends a GET request to the API server.
187226
* @param endpoint The endpoint to send the request to
188227
* @param options Request options, if any.
189228
* @return Standard web HTTP response.
190229
*/
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;
192241

193242
/**
194243
* Returns a {@link UserContainer} based on the passed-in {@link NameOrSnowflake}.
195244
* @param idOrName The username or the user's ID to pass in.
196245
* @return The {@link UserContainer} to do such methods.
197246
*/
198-
users!: (idOrName: NameOrSnowflake) => UserContainer;
247+
readonly users!: (idOrName: NameOrSnowflake) => UserContainer;
199248

200249
/** {@link UserContainer} that redirects requests to `/users/@me`. */
201-
me!: UserContainer;
250+
readonly me!: UserContainer;
202251

203252
constructor(options: ClientOptions = kClientOptions) {
204253
this.#authStrategy = options.auth;
205254
this.#apiVersion = options.apiVersion || 1;
206255
this.#baseURL = options.baseURL || DEFAULT_BASE_URL;
207256
this.#headers = options.headers || {};
208257

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) {
210260
const [major, minor] = process.version.split('.').map(Number);
211261
if ((major < 16 && minor < 15) || (major === 17 && minor < 5)) {
212262
throw new Error(
@@ -222,6 +272,8 @@ export class Client {
222272
// @ts-ignore
223273
this.#fetch = global.fetch || options.fetch;
224274

275+
// @ts-ignore
276+
this.#FormData = global.FormData || options.FormData;
225277
for (const method of Methods) {
226278
this[method] = function (
227279
this: Client,
@@ -232,15 +284,17 @@ export class Client {
232284
};
233285
}
234286

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;
239289

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);
243295
}
296+
297+
this.me = new UserContainer(this, '@me');
244298
}
245299

246300
// @ts-ignore
@@ -280,6 +334,20 @@ export class Client {
280334

281335
body = JSON.stringify(options.body);
282336
}
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;
283351
}
284352
}
285353

@@ -294,7 +362,7 @@ export class Client {
294362
};
295363

296364
if (body !== null) fetchOptions.body = body;
297-
return this.#fetch(new URL(url), fetchOptions);
365+
return this.#fetch(new URL(url, this.#baseURL), fetchOptions);
298366
}
299367

300368
/**
@@ -308,7 +376,7 @@ export class Client {
308376
* @returns An {@link Buffer} on Node.js, or a {@link ArrayBuffer} in the browser of a CDN object
309377
* if the feature is enabled.
310378
*/
311-
cdn(prefix: string = '/cdn', ...paths: string[]) {
379+
async cdn(prefix: string = '/cdn', ...paths: string[]): Promise<responses.main.CDN> {
312380
let path = '';
313381
if (!paths.length) path = '/';
314382
else {
@@ -317,29 +385,38 @@ export class Client {
317385
}
318386
}
319387

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
326403
}
327-
328-
return reject(new HTTPError(resp.status));
329404
}
405+
]
406+
}));
330407

331-
const buf = await resp.arrayBuffer();
332-
if (isBrowser) return resolve(buf);
408+
throw new HTTPError(resp.status, hasOwnProperty(data, 'errors') ? data.errors : []);
409+
}
333410

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;
338413

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;
343420
}
344421

345422
/**
@@ -386,13 +463,14 @@ export class Client {
386463
* or an API response object (usually a 404 if it is not enabled).
387464
*/
388465
metrics(
466+
path: string,
389467
options?: Omit<
390468
RequestOptions<'/features', 'get'>,
391469
'pathParameters' | 'queryParameters' | 'body' | 'contentType'
392470
>
393471
) {
394472
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) =>
396474
!resp.ok && resp.headers.get('content-type')?.includes('application/json')
397475
? transformJSON<ApiResponse>(resp).then(resolve).catch(reject)
398476
: resp.text().then(resolve).catch(reject)
@@ -431,7 +509,12 @@ export class Client {
431509
* .then(() => console.log('heartbeat was ok!'))
432510
* .catch(console.error);
433511
*/
434-
heartbeat(options?: RequestOptions<'/heartbeat', 'head'>) {
512+
heartbeat(
513+
options?: Omit<
514+
RequestOptions<'/heartbeat', 'head'>,
515+
'pathParameters' | 'queryParameters' | 'body' | 'contentType'
516+
>
517+
) {
435518
return new Promise<void>((resolve, reject) =>
436519
this.head('/heartbeat', options)
437520
.then((resp) => {
@@ -446,13 +529,36 @@ export class Client {
446529
);
447530
}
448531

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+
}
456562

457563
private _buildUrl<R extends Route>(url: R, options?: RequestOptions<R, HTTPMethod>) {
458564
let formedUrl = this.#baseURL;

0 commit comments

Comments
 (0)