From 64b5439c434478c67e9fd0206dd38e382b66668f Mon Sep 17 00:00:00 2001 From: Chuck MANCHUCK Reeves Date: Wed, 18 Oct 2023 20:17:47 +0000 Subject: [PATCH] docs(server-client): adding docblocks --- jest.config.ts | 5 - packages/server-client/lib/client.ts | 360 +++++++++++++++--- .../lib/enums/AuthenticationType.ts | 26 ++ .../lib/enums/GenericErrorCodes.ts | 81 ++++ packages/server-client/lib/fileClient.ts | 71 +++- packages/server-client/lib/transformers.ts | 50 ++- packages/server-client/lib/types/APILink.ts | 8 + packages/server-client/lib/types/APILinks.ts | 13 + .../server-client/lib/types/ConfigParams.ts | 19 + packages/server-client/package.json | 23 +- packages/server-client/typedoc.json | 5 + packages/vetch/__tests__/index.test.ts | 5 - packages/vetch/__tests__/vetch.test.ts | 207 ---------- packages/vetch/lib/enums/HTTPMethods.ts | 18 +- packages/vetch/lib/enums/contentType.ts | 19 + packages/vetch/lib/enums/index.ts | 3 + packages/vetch/lib/enums/responseTypes.ts | 20 + packages/vetch/lib/errors/vetchError.ts | 29 ++ packages/vetch/lib/index.ts | 19 +- packages/vetch/lib/interfaces/headers.ts | 3 - .../vetch/lib/interfaces/vetchHttpRequest.ts | 3 - packages/vetch/lib/interfaces/vetchOptions.ts | 32 -- .../vetch/lib/interfaces/vetchResponse.ts | 13 - packages/vetch/lib/types/headers.ts | 15 + packages/vetch/lib/types/index.ts | 4 + packages/vetch/lib/types/vetchError.ts | 13 - packages/vetch/lib/types/vetchOptions.ts | 41 ++ packages/vetch/lib/types/vetchPromise.ts | 11 +- packages/vetch/lib/types/vetchResponse.ts | 27 ++ packages/vetch/lib/vetch.ts | 177 --------- packages/vetch/package.json | 29 +- 31 files changed, 788 insertions(+), 561 deletions(-) create mode 100644 packages/server-client/typedoc.json delete mode 100644 packages/vetch/__tests__/index.test.ts delete mode 100644 packages/vetch/__tests__/vetch.test.ts create mode 100644 packages/vetch/lib/enums/contentType.ts create mode 100644 packages/vetch/lib/enums/index.ts create mode 100644 packages/vetch/lib/errors/vetchError.ts delete mode 100644 packages/vetch/lib/interfaces/headers.ts delete mode 100644 packages/vetch/lib/interfaces/vetchHttpRequest.ts delete mode 100644 packages/vetch/lib/interfaces/vetchOptions.ts delete mode 100644 packages/vetch/lib/interfaces/vetchResponse.ts create mode 100644 packages/vetch/lib/types/headers.ts create mode 100644 packages/vetch/lib/types/index.ts delete mode 100644 packages/vetch/lib/types/vetchError.ts create mode 100644 packages/vetch/lib/types/vetchOptions.ts create mode 100644 packages/vetch/lib/types/vetchResponse.ts delete mode 100644 packages/vetch/lib/vetch.ts diff --git a/jest.config.ts b/jest.config.ts index 6474a42a..0fa22ee2 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -104,11 +104,6 @@ const config: Config.InitialOptions = { testMatch: ['/packages/verify2/__tests__/**/*.test.ts'], coveragePathIgnorePatterns: ['node_modules', '__tests__'], }, - { - displayName: 'VETCH', - testMatch: ['/packages/vetch/__tests__/**/*.test.ts'], - coveragePathIgnorePatterns: ['node_modules', '__tests__'], - }, { displayName: 'VIDEO', testMatch: ['/packages/video/__tests__/**/*.test.ts'], diff --git a/packages/server-client/lib/client.ts b/packages/server-client/lib/client.ts index 346f766b..d13288d4 100644 --- a/packages/server-client/lib/client.ts +++ b/packages/server-client/lib/client.ts @@ -1,11 +1,12 @@ +import fetch, { Response } from 'node-fetch'; +import AbortError from 'node-fetch'; import { Auth, AuthInterface, AuthParams } from '@vonage/auth'; import { - request as vetchRequest, ResponseTypes, VetchResponse, VetchOptions, HTTPMethods, - Vetch, + ContentType, } from '@vonage/vetch'; import { AuthenticationType } from './enums/AuthenticationType'; import * as transfomers from './transformers'; @@ -14,17 +15,50 @@ import { ConfigParams } from './types/index'; const log = debug('vonage:server-client'); -export abstract class Client { +export class Client { + /** + * Static property containing utility transformers. + * + * @type {typeof transfomers} + * @static + */ public static transformers = transfomers; - protected authType?: AuthenticationType; + /** + * The type of authentication used for the client's requests. + * + * @type {AuthenticationType | undefined} + * @default AuthenticationType.QUERY_KEY_SECRET + * @public + */ + public authType?: AuthenticationType = AuthenticationType.QUERY_KEY_SECRET; + /** + * The authentication instance responsible for generating authentication headers and query parameters. + * + * @type {AuthInterface} + * @protected + */ protected auth: AuthInterface; + /** + * Configuration settings for the client, including default hosts for various services and other request settings. + * + * @type {ConfigParams} + * @protected + */ protected config: ConfigParams; + /** + * Creates a new instance of the Client. + * + * @param {AuthInterface | AuthParams} credentials - The authentication credentials or an authentication instance. + * @param {ConfigParams} [options] - Optional configuration settings for the client. + * + * @example + * const client = new Client({ apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }); + */ constructor(credentials: AuthInterface | AuthParams, options?: ConfigParams) { - // eslint-disable-next-line max-len this.auth = !Object.prototype.hasOwnProperty.call( credentials, 'getQueryParams', @@ -39,15 +73,28 @@ export abstract class Client { meetingsHost: options?.meetingsHost || 'https://api-eu.vonage.com', proactiveHost: options?.proactiveHost || 'https://api-eu.vonage.com', responseType: options?.responseType || ResponseTypes.json, - timeout: options?.timeout || null, + timeout: options?.timeout || 3000, } as ConfigParams; } - public async addAuthenticationToRequest( + /** + * Adds the appropriate authentication headers or parameters to the request based on the authentication type. + * + * @param {VetchOptions} request - The request options to which authentication needs to be added. + * @return {Promise} - The request options with the added authentication. + * + * @example + * const requestOptions: VetchOptions = { + * url: 'https://api.example.com/data', + * method: HTTPMethods.GET + * }; + * const authenticatedRequest = await client.addAuthenticationToRequest(requestOptions); + */ + async addAuthenticationToRequest( request: VetchOptions, ): Promise { - let requestPath = 'data'; log(`adding ${this.authType || 'api key/secret'} to request`); + switch (this.authType) { case AuthenticationType.BASIC: request.headers = Object.assign({}, request.headers, { @@ -60,41 +107,48 @@ export abstract class Client { Authorization: await this.auth.createBearerHeader(), }); return request; - - case AuthenticationType.QUERY_KEY_SECRET: - requestPath = 'params'; - // falls through - case AuthenticationType.KEY_SECRET: - default: } - if (['GET', 'DELETE'].includes(request.method)) { - requestPath = 'params'; + // Regardless of the setting, set the auth params in the query string + // since these methods do not accept a body + if (this.authType === AuthenticationType.QUERY_KEY_SECRET + || ['GET', 'DELETE'].includes(request.method) + ) { + log(`adding parameters to query string`); + request.params = { + ...(request.params ? request.params : {}), + ...await this.auth.getQueryParams({}), + }; + return request; } const authParams = await this.auth.getQueryParams({}); - let params = { - ...request[requestPath], + request.data = request.data ?? {}; + + // JSON as default + log(`Adding parameters to body`); + request.data = { + ...request.data, ...authParams, }; - // This is most likely web-form - if ( - !request[requestPath] - && this.authType !== AuthenticationType.QUERY_KEY_SECRET - ) { - requestPath = 'body'; - params = new URLSearchParams({ - ...Object.fromEntries(request.body.entries()), - ...authParams, - }); + if (this.authType === AuthenticationType.KEY_SECRET) { + request.type = ContentType.FORM_URLENCODED; } - request[requestPath] = params; return request; } - public async sendDeleteRequest(url: string): Promise> { + /** + * Sends a DELETE request to the specified URL. + * + * @param {string} url - The URL endpoint for the DELETE request. + * @return {Promise>} - The response from the DELETE request. + * + * @example + * const response = await client.sendDeleteRequest('https://api.example.com/resource/123'); + */ + async sendDeleteRequest(url: string): Promise> { const request = { url, method: HTTPMethods.DELETE, @@ -103,25 +157,43 @@ export abstract class Client { return await this.sendRequest(request); } - public async sendFormSubmitRequest( + /** + * Sends a POST request with form data to the specified URL. + * + * @param {string} url - The URL endpoint for the POST request. + * @param {Record} [payload] - Optional payload containing form data to send with the POST request. + * @return {Promise>} - The response from the POST request. + * + * @example + * const response = await client.sendFormSubmitRequest('https://api.example.com/resource', { key: 'value' }); + */ + async sendFormSubmitRequest( url: string, - payload?: { [key: string]: any }, + payload?: Record, ): Promise> { const request = { url, method: HTTPMethods.POST, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - ...(payload ? { body: new URLSearchParams(payload) } : {}), + type: ContentType.FORM_URLENCODED, + ...(payload ? { data: payload } : {}), } as VetchOptions; return await this.sendRequest(request); } - public async sendGetRequest( + /** + * Sends a GET request to the specified URL with optional query parameters. + * + * @param {string} url - The URL endpoint for the GET request. + * @param {Record} [queryParams] - Optional query parameters to append to the URL. These should be compatible with Node's URLSearchParams. + * @return {Promise>} - The response from the GET request. + * + * @example + * const response = await client.sendGetRequest('https://api.example.com/resource', { key: 'value' }); + */ + async sendGetRequest( url: string, - queryParams?: { [key: string]: any }, + queryParams?: { [key: string]: unknown }, ): Promise> { const request = { url, @@ -132,51 +204,229 @@ export abstract class Client { return await this.sendRequest(request); } - public async sendPatchRequest( + /** + * Sends a PATCH request to the specified URL with an optional payload. + * + * @param {string} url - The URL endpoint for the PATCH request. + * @param {Record} [payload] - Optional payload to be sent as the body of the PATCH request. + * @return {Promise>} - The response from the PATCH request. + * + * @example + * const response = await client.sendPatchRequest('https://api.example.com/resource/123', { key: 'updatedValue' }); + */ + async sendPatchRequest( url: string, - payload?: { [key: string]: any }, + payload?: { [key: string]: unknown }, ): Promise> { return this.sendRequestWithData(HTTPMethods.PATCH, url, payload); } - public async sendPostRequest( + /** + * Sends a POST request to the specified URL with an optional payload. + * + * @param {string} url - The URL endpoint for the POST request. + * @param {Record} [payload] - Optional payload to be sent as the body of the POST request. + * @return {Promise>} - The response from the POST request. + * + * @example + * const response = await client.sendPostRequest('https://api.example.com/resource', { key: 'value' }); + */ + async sendPostRequest( url: string, - payload?: { [key: string]: any }, + payload?: { [key: string]: unknown }, ): Promise> { return this.sendRequestWithData(HTTPMethods.POST, url, payload); } - public sendPutRequest( + /** + * Sends a PUT request to the specified URL with an optional payload. + * + * @param {string} url - The URL endpoint for the PUT request. + * @param {Record} [payload] - Optional payload to be sent as the body of the PUT request. + * @return {Promise>} - The response from the PUT request. + * + * @example + * const response = await client.sendPutRequest('https://api.example.com/resource/123', { key: 'updatedValue' }); + */ + sendPutRequest( url: string, - payload?: { [key: string]: any }, + payload?: { [key: string]: unknown }, ): Promise> { return this.sendRequestWithData(HTTPMethods.PUT, url, payload); } - public async sendRequestWithData( - method: HTTPMethods, + /** + * Sends a request with JSON-encoded data to the specified URL using the provided HTTP method. + * + * @param {HTTPMethods.POST | HTTPMethods.PATCH | HTTPMethods.PUT} method - The HTTP method to be used for the request (only POST, PATCH, or PUT are acceptable). + * @param {string} url - The URL endpoint for the request. + * @param {Record} [payload] - Optional payload to be sent as the body of the request, JSON-encoded. + * @return {Promise>} - The response from the request. + * + * @example + * const response = await client.sendRequestWithData(HTTPMethods.POST, 'https://api.example.com/resource', { key: 'value' }); + */ + async sendRequestWithData( + method: HTTPMethods.POST | HTTPMethods.PATCH | HTTPMethods.PUT, url: string, - payload?: { [key: string]: any }, + payload?: { [key: string]: unknown }, ): Promise> { const request = { url, method: method, - headers: { - 'Content-Type': 'application/json', - }, + type: ContentType.JSON, ...(payload ? { data: payload } : {}), } as VetchOptions; return await this.sendRequest(request); } - public async sendRequest( + /** + * Sends a request adding necessary headers, handling authentication, and parsing the response. + * + * @param {VetchOptions} request - The options defining the request, including URL, method, headers, and data. + * @return {Promise>} - The parsed response from the request. + * + * @example + * const requestOptions: VetchOptions = { + * url: 'https://api.example.com/data', + * method: HTTPMethods.GET, + * headers: { 'Custom-Header': 'value' } + * }; + * const response = await client.sendRequest(requestOptions); + */ + async sendRequest( request: VetchOptions, ): Promise> { - request.timeout = this.config.timeout; + const timeout = request.timeout || this.config.timeout; + const controller = new AbortController(); + const timeoutId: NodeJS.Timeout = setTimeout( + () => { + controller.abort(); + }, + timeout, + ); + + + try{ + request = await this.prepareRequest(request); + + return await this.parseResponse( + request, + await fetch( + request.url, + { + method: request.method, + headers: request.headers as Record, + body: this.prepareBody(request), + ...(timeout ? { signal: controller.signal } :{}), + }, + ), + ); + + } catch (error) { + if (error instanceof AbortError) { + log(`Request timed out after ${timeout}`); + } + throw error; + } finally { + clearTimeout(timeoutId); + } + } + + /** + * Prepares the request with necessary headers, authentication, and query parameters. + * + * @param {VetchOptions} request - The initial request options. + * @return {Promise} - The modified request options. + */ + protected async prepareRequest(request: VetchOptions): Promise { + request.headers = { + ...request.headers, + 'user-agent':[ + `@vonage/server-sdk/3.0.0`, + ` node/${process.version.replace('v', '')}`, + this.config.appendUserAgent ? ` ${this.config.appendUserAgent}` : '', + ].join(), + }; + request = await this.addAuthenticationToRequest(request); - const vetch = new Vetch(this.config); - const result = await vetch.request(request); - return result; + + const url = new URL(request.url); + const params = new URLSearchParams(request.params); + + // copy params into the URL + for (const [param, value] of params.entries()) { + url.searchParams.append(param, value); + } + + request.url = url.toString(); + return request; + } + + /** + * Prepares the body for the request based on the content type. + * + * @param {VetchOptions} request - The request options. + * @return {string | undefined} - The prepared request body as a string or undefined. + */ + protected prepareBody(request: VetchOptions): string | undefined { + switch (request.type) { + case ContentType.JSON: + request.headers['content-type'] = ContentType.JSON; + return request.data ? JSON.stringify(request.data) : undefined; + + case ContentType.FORM_URLENCODED: + request.headers['content-type'] = ContentType.FORM_URLENCODED; + return request.data + ? new URLSearchParams(request.data).toString() + : undefined; + } + + return undefined; + } + + /** + * Parses the response based on its content type. + * + * @template T - The expected type of the parsed response data. + * + * @param {VetchOptions} request - The request options. + * @param {Response} response - The raw response from the request. + * @return {Promise>} - The parsed response. + */ + protected async parseResponse( + request: VetchOptions, + response: Response, + + ): Promise> { + let decoded = null; + switch (response.headers.get('content-type')) { + case ContentType.FORM_URLENCODED: + decoded = response.body + ? new URLSearchParams(await response.text()) + : '' ; + break; + case ContentType.JSON: + decoded = await response.json(); + break; + default: + decoded = await response.text(); + } + + const responseHeaders = {}; + + for (const [header, value] of response.headers.entries()) { + Object.assign(response, header, value); + } + + return { + data: decoded as T, + config: request, + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + request: request, + }; } } diff --git a/packages/server-client/lib/enums/AuthenticationType.ts b/packages/server-client/lib/enums/AuthenticationType.ts index bfa90b7c..e50909aa 100644 --- a/packages/server-client/lib/enums/AuthenticationType.ts +++ b/packages/server-client/lib/enums/AuthenticationType.ts @@ -1,7 +1,33 @@ +/** + * Enum representing the different types of authentication methods + * supported by the Vonage APIs. + * + * @enum {string} + */ export enum AuthenticationType { + /** + * @description Basic authentication using a base64-encoded string. + */ BASIC = 'basic', + + /** + * @description JSON Web Token (JWT) authentication. + */ JWT = 'jwt', + + /** + * @description Authentication using both API key and secret in the request body. + * @deprecated This method is deprecated. + */ KEY_SECRET = 'key_secret', + + /** + * @description Authentication using API key and secret in the query parameters. + */ QUERY_KEY_SECRET = 'query_key_secret', + + /** + * @description HMAC signature-based authentication. + */ SIGNATURE = 'signature', } diff --git a/packages/server-client/lib/enums/GenericErrorCodes.ts b/packages/server-client/lib/enums/GenericErrorCodes.ts index 1cbf3ee0..2a42bbe6 100644 --- a/packages/server-client/lib/enums/GenericErrorCodes.ts +++ b/packages/server-client/lib/enums/GenericErrorCodes.ts @@ -1,21 +1,102 @@ +/** + * Enum representing the different types of generic error codes + * that might be returned by the Vonage APIs. + * + * @enum {string} + */ export enum GenericErrors { + /** + * @description The account has insufficient funds. This request could not be performed due to your account balance being low. Top up your account in the Vonage Dashboard. + */ LOW_BALANCE = 'low-balance', + + /** + * @description The provided credentials are incorrect or missing. You did not provide correct credentials. Check your authentication credentials; they can be found in the Vonage Dashboard. + */ UNAUTHORIZED = 'unauthorized', + + /** + * @description The authenticated user does not have access to the requested resource. Your account does not have permission to perform this action. Check that you're using the correct credentials and that your account has this feature enabled. + */ FORBIDDEN = 'forbidden', + + /** + * @description The requested resource could not be found. The requested resource does not exist, or you do not have access to it. Check both the URI that you're requesting and your authentication credentials. + */ NOT_FOUND = 'not-found', + + /** + * @description The account is not provisioned for the requested service. The credentials provided do not have access to the requested product. Check that your API key is correct and has been whitelisted. + */ UNPROVISIONED = 'unprovisioned', + + /** + * @description The account has been suspended. This account has been suspended. Contact support@api.vonage.com for more information. + */ ACCOUNT_SUSPENDED = 'account-suspended', + + /** + * @description The provided JWT has expired. The JWT provided has expired. Generate a new JWT with an expiration date in the future. + */ JWT_EXPIRED = 'jwt-expired', + + /** + * @description The provided JWT has been revoked. The JWT provided has been revoked. Generate a new JWT using your private key. + */ JWT_REVOKED = 'jwt-revoked', + + /** + * @description The provided API key is invalid. The API key provided does not exist in our system, or you do not have access. Modify your request to provide a valid API key. + */ INVALID_API_KEY = 'invalid-api-key', + + /** + * @description The provided signature is invalid. The signature provided did not validate. Check your signature secret and ensure you're following the correct signing process. + */ INVALID_SIGNATURE = 'invalid-signature', + + /** + * @description The request originates from an unauthorized IP address. The source IP address of the request is not in our allow list. Make a request from an allowed IP address, or add your current IP to the list of authorized addresses. + */ INVALID_IP = 'invalid-ip', + + /** + * @description Multiple authentication methods were provided in the request. Multiple authentication methods were detected in your request. Provide exactly one authentication method. + */ MULTIPLE_AUTH_METHODS = 'multiple-auth-methods', + + /** + * @description The provided ID in the request is invalid. The ID provided does not exist in our system. Modify your request to provide a valid ID. + */ INVALID_ID = 'invalid-id', + + /** + * @description The provided JSON in the request body is invalid. The request body did not contain valid JSON. Send a JSON request body, including a Content-Type header of application/json. + */ INVALID_JSON = 'invalid-json', + + /** + * @description The HTTP verb used in the request is not allowed. This endpoint does not support the HTTP verb that you requested. Read the API documentation to see which verbs your desired endpoint supports. + */ WRONG_VERB = 'wrong-verb', + + /** + * @description The provided Accept header in the request is invalid. Invalid Accept header provided. Most Vonage APIs only send back application/json. Check the API documentation for the specific API you're working with for a complete list of supported data types. + */ ACCEPT_HEADER = 'accept-header', + + /** + * @description The provided Content-Type header in the request is invalid. Invalid Content-Type header provided. Most Vonage APIs only accept application/json. Check the API documentation for the specific API you're working with for a complete list of supported data types. + */ CONTENT_TYPE_HEADER = 'content-type-header', + + /** + * @description The requested service is unavailable due to legal reasons. Vonage APIs are unavailable in the following areas due to international sanctions: Sudan, Syria, Crimea, North Korea, Iran, and Cuba. + */ UNAVAILABLE_LEGAL = 'unavailable-legal', + + /** + * @description The application associated with the request has been suspended. This application has been suspended. Re-enable the application or create a new application to use. + */ APPLICATION_SUSPENDED = 'application-suspended', } diff --git a/packages/server-client/lib/fileClient.ts b/packages/server-client/lib/fileClient.ts index 551d2f59..a5299903 100644 --- a/packages/server-client/lib/fileClient.ts +++ b/packages/server-client/lib/fileClient.ts @@ -1,30 +1,89 @@ import { Client } from './client'; import { AuthenticationType } from './enums/AuthenticationType'; -import { writeFileSync } from 'fs'; import debug from 'debug'; +import { VetchError, VetchOptions, VetchResponse } from '@vonage/vetch'; +import { Response } from 'node-fetch'; +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; const log = debug('vonage:server-client'); +/** + * A client for downloading files from Vonage. + * + * @extends Client + */ export class FileClient extends Client { - protected authType = AuthenticationType.JWT; + public authType = AuthenticationType.JWT; + protected saveFilePath: string = ''; + + /** + * Downloads a file from Vonage and saves it to a specified path. + * + * @param {string} file - The URL or ID of the file to be downloaded. + * @param {string} path - The path where the downloaded file should be saved. + * + * @throws {Error} Throws an error if the file could not be downloaded or saved. + * + * @return {Promise} Resolves when the file is successfully downloaded and saved. + */ async downloadFile(file: string, path: string): Promise { log(`Downloading file: ${file}`); let fileId = file; try { const fileURL = new URL(file); - fileId = fileURL.pathname.split('/').pop(); + fileId = fileURL.pathname.split('/').pop() || ''; } catch (_) { log(`Not a url`); } log(`File Id ${fileId}`); - const resp = await this.sendGetRequest( + this.saveFilePath = path; + await this.sendGetRequest( `${this.config.apiHost}/v1/files/${fileId}`, ); - log(`Saving to ${path}`); - writeFileSync(path, resp.data); log('File saved'); } + + /** + * Parses the API response and saves the file to the specified path. + * + * @param {VetchOptions} request - The request options. + * @param {Response} response - The response object from the API. + * + * @throws {VetchError} Throws an error if the response is not as expected. + * + * @return {Promise>} Returns a parsed response. + * @protected + */ + protected async parseResponse( + request: VetchOptions, + response: Response, + ): Promise> { + if (!response.ok) { + throw new VetchError( + `Unexpected response when downloading file: ${response.statusText}`, + request, + ); + } + + log(`Saving to ${this.saveFilePath}`); + await pipeline(response.body, createWriteStream(this.saveFilePath)); + + const responseHeaders = {}; + + for (const [header, value] of response.headers.entries()) { + Object.assign(response, header, value); + } + + return { + config: request, + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + request: request, + }; + } } diff --git a/packages/server-client/lib/transformers.ts b/packages/server-client/lib/transformers.ts index 5b636ed3..ac11bf19 100644 --- a/packages/server-client/lib/transformers.ts +++ b/packages/server-client/lib/transformers.ts @@ -4,8 +4,20 @@ import kebabCase from 'lodash.kebabcase'; import partial from 'lodash.partial'; import isObject from 'lodash.isobject'; +export type TransformFunction = (key: string) => string; + +/** + * Transforms the keys of an object based on a provided transformation function. + * + * @param {TransformFunction} transformFn - The function to transform the object's keys. + * @param {Record} objectToTransform - The object whose keys are to be transformed. + * @param {boolean} [deep=false] - Whether to deeply transform nested object keys. + * @param {boolean} [preserve=false] - Whether to preserve the original object's keys. + * + * @return {Record} A new object with transformed keys. + */ export const transformObjectKeys = ( - transformFn: (key: string | number) => string, + transformFn: TransformFunction, objectToTransform: Record, deep = false, preserve = false, @@ -31,9 +43,9 @@ export const transformObjectKeys = ( isObject(t) ? transformObjectKeys( transformFn, - t as Record, - deep, - preserve, + t as Record, + deep, + preserve, ) : t, ); @@ -42,17 +54,41 @@ export const transformObjectKeys = ( transformedObject[newKey] = transformObjectKeys( transformFn, - value as Record, - deep, - preserve, + value as Record, + deep, + preserve, ); } return transformedObject; }; +/** + * Transforms the keys of an object to camelCase. + * + * @param {Record} objectToTransform - The object whose keys are to be transformed. + * @param {boolean} [deep=false] - Whether to deeply transform nested object keys. + * @param {boolean} [preserve=false] - Whether to preserve the original object's keys. + * @return {Record} A new object with camelCased keys. + */ export const camelCaseObjectKeys = partial(transformObjectKeys, camelCase); +/** + * Transforms the keys of an object to snake_case. + * + * @param {Record} objectToTransform - The object whose keys are to be transformed. + * @param {boolean} [deep=false] - Whether to deeply transform nested object keys. + * @param {boolean} [preserve=false] - Whether to preserve the original object's keys. + * @return {Record} A new object with snake_cased keys. + */ export const snakeCaseObjectKeys = partial(transformObjectKeys, snakeCase); +/** + * Transforms the keys of an object to kebab-case. + * + * @param {Record} objectToTransform - The object whose keys are to be transformed. + * @param {boolean} [deep=false] - Whether to deeply transform nested object keys. + * @param {boolean} [preserve=false] - Whether to preserve the original object's keys. + * @return {Record} A new object with kebab-cased keys. + */ export const kebabCaseObjectKeys = partial(transformObjectKeys, kebabCase); diff --git a/packages/server-client/lib/types/APILink.ts b/packages/server-client/lib/types/APILink.ts index 8827f0dd..62514c50 100644 --- a/packages/server-client/lib/types/APILink.ts +++ b/packages/server-client/lib/types/APILink.ts @@ -1,3 +1,11 @@ +/** + * Represents a link object in the HAL format. + * + * @see {@link https://tools.ietf.org/html/rfc5988} for more details on Web Linking. + * + * @typedef {object} APILink + * @property {string} href - The URL of the link. + */ export type APILink = { href: string // TODO Add more from RFC 5988? diff --git a/packages/server-client/lib/types/APILinks.ts b/packages/server-client/lib/types/APILinks.ts index 5f8d0176..44ef5075 100644 --- a/packages/server-client/lib/types/APILinks.ts +++ b/packages/server-client/lib/types/APILinks.ts @@ -1,5 +1,18 @@ import { APILink } from './APILink'; +/** + * Represents a set of links in the HAL format. + * + * @see {@link https://tools.ietf.org/html/draft-kelly-json-hal-08} for more details on HAL format. + * + * @typedef {object} APILinks + * @property {object} _links - The set of links. + * @property {APILink} _links.self - The link for the current resource. + * @property {APILink} [_links.next] - The link for the next page of resources. + * @property {APILink} [_links.first] - The link for the first page of resources. + * @property {APILink} [_links.last] - The link for the last page of resources. + * @property {APILink} [_links.prev] - The link for the previous page of resources. + */ export type APILinks = { _links: { self: APILink diff --git a/packages/server-client/lib/types/ConfigParams.ts b/packages/server-client/lib/types/ConfigParams.ts index 2b87972b..19625920 100644 --- a/packages/server-client/lib/types/ConfigParams.ts +++ b/packages/server-client/lib/types/ConfigParams.ts @@ -1,4 +1,23 @@ import { ResponseTypes } from '@vonage/vetch'; + +/** + * Type defining configuration parameters for API requests. + * + * @typedef {Object} ConfigParams + * + * @property {string} [restHost] - The base URL for REST API requests. + * @property {string} [apiHost] - The base URL for API requests. + * @property {string} [videoHost] - The base URL for video-related API requests. + * @property {ResponseTypes} [responseType] - Expected response type/format + * from the API (e.g., json, buffer, etc.). + * @property {number} [timeout] - The timeout duration (in milliseconds) for + * API requests. + * @property {string} [proactiveHost] - The base URL for proactive API requests. + * @property {string} [meetingsHost] - The base URL for meetings-related API + * requests. + * @property {string} [appendUserAgent] - Additional user-agent string to + * append in API requests. + */ export type ConfigParams = { restHost?: string; apiHost?: string; diff --git a/packages/server-client/package.json b/packages/server-client/package.json index e45e7541..334ac2e0 100644 --- a/packages/server-client/package.json +++ b/packages/server-client/package.json @@ -1,7 +1,8 @@ { + "$schema": "https://json.schemastore.org/package.json", "name": "@vonage/server-client", - "version": "1.8.2", - "description": "Core services related to talking to the Vonage APIs", + "version": "1.9.0", + "description": "The Vonage Server Client provides core functionalities for interacting with Vonage APIs, ensuring a standardized response regardless of the underlying HTTP adapter.", "homepage": "https://developer.vonage.com", "bugs": { "url": "https://github.com/Vonage/vonage-node-sdk/issues" @@ -11,7 +12,16 @@ "url": "git+https://github.com/Vonage/vonage-node-sdk.git" }, "license": "Apache-2.0", - "author": "Chris Tankersley ", + "contributors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com" + }, + { + "name": "Chuck \"MANCHUCK\" Reeves", + "email": "chuck@manchuck.com" + } + ], "main": "dist/index.js", "types": "dist/index.d.ts", "directories": { @@ -37,6 +47,13 @@ "lodash.snakecase": "^4.1.1" }, "devDependencies": { + "@types/lodash.camelcase": "4.3.7", + "@types/lodash.isobject": "3.0.7", + "@types/lodash.kebabcase": "4.1.7", + "@types/lodash.merge": "4.6.7", + "@types/lodash.partial": "4.2.7", + "@types/lodash.snakecase": "4.1.7", + "@types/node-fetch": "2.6.6", "nock": "^13.3.4" }, "publishConfig": { diff --git a/packages/server-client/typedoc.json b/packages/server-client/typedoc.json new file mode 100644 index 00000000..50447966 --- /dev/null +++ b/packages/server-client/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["lib/index.ts"], + "name": "Vonage Server Client" +} diff --git a/packages/vetch/__tests__/index.test.ts b/packages/vetch/__tests__/index.test.ts deleted file mode 100644 index 62bc97af..00000000 --- a/packages/vetch/__tests__/index.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as main from '../lib'; - -test('main exports', () => { - expect(main.Vetch).toBeDefined(); -}); diff --git a/packages/vetch/__tests__/vetch.test.ts b/packages/vetch/__tests__/vetch.test.ts deleted file mode 100644 index cce67c3d..00000000 --- a/packages/vetch/__tests__/vetch.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { Vetch, request } from '../lib'; -import { stringify } from 'querystring'; -import nock from 'nock'; -import { HTTPMethods } from '../lib/enums/HTTPMethods'; - -nock.disableNetConnect(); - -afterEach(() => { - nock.cleanAll(); -}); - -const url = 'https://just-testing.com'; - -describe('option validation', () => { - test('should throw error if no url provided', async () => { - expect.assertions(1); - await expect(request({})).rejects.toEqual(new Error('URL is required.')); - }); -}); - -describe('error handling', () => { - test('should throw on non-2xx responses by default', async () => { - expect.assertions(1); - nock(url).get('/').reply(500); - await expect(request({ url })).rejects.toEqual( - expect.objectContaining({ code: '500' }), - ); - }); -}); - -describe('option configuration', () => { - test('should use options passed in constructor', async () => { - expect.assertions(1); - nock(url).head('/').reply(200); - const inst = new Vetch({ method: HTTPMethods.HEAD }); - const res = await inst.request({ url }); - expect(res.config.method).toEqual('HEAD'); - }); - - test('should allow nested options passed in constructor', async () => { - expect.assertions(2); - nock(url).get('/').reply(200); - const inst = new Vetch({ headers: { spiderman: 'Norman Osbourne' } }); - const res = await inst.request({ - url, - headers: { ironMan: 'Mandarin' }, - }); - expect(res.config.headers?.spiderman).toEqual('Norman Osbourne'); - expect(res.config.headers?.ironMan).toEqual('Mandarin'); - }); - - test('should allow setting a base url', async () => { - expect.assertions(1); - nock(url).get('/v1/thor').reply(200, {}); - const inst = new Vetch({ baseURL: `${url}/v1` }); - const res = await inst.request({ url: '/thor' }); - expect(res.data).toEqual({}); - }); - - test('should allow URL params', async () => { - expect.assertions(2); - const qs = '?black=panther&captain=america'; - const opts = { url: `${url}/${qs}` }; - nock(url).get(`/${qs}`).reply(200, {}); - const res = await request(opts); - expect(res.status).toEqual(200); - expect(res.config.url).toEqual(`${url}/${qs}`); - }); - - test('should encode params from object', async () => { - const opts = { - url, - params: { - black: 'panther', - captain: 'america', - }, - }; - const qs = '?black=panther&captain=america'; - nock(url).get(`/${qs}`).reply(200, {}); - const res = await request(opts); - expect(res.status).toEqual(200); - expect(res.config.url).toEqual(`${url}${qs}`); - }); - - test('should merge URL with params from options', async () => { - const opts = { - url: `${url}?black=panther`, - params: { - captain: 'america', - }, - }; - const qs = '?black=panther&captain=america'; - nock(url).get(`/${qs}`).reply(200, {}); - const res = await request(opts); - expect(res.status).toEqual(200); - expect(res.config.url).toEqual(`${url}${qs}`); - }); - - test('should return json by default', async () => { - const body = { avengers: 'assemble' }; - nock(url).get('/').reply(200, body); - const res = await request({ url }); - expect(res.data).toEqual(body); - }); - - test('should default to application/json', async () => { - nock(url).matchHeader('accept', 'application/json').get('/').reply(200, {}); - const res = await request({ url }); - expect(res.data).toEqual({}); - }); - - test('should include the request data in the response config', async () => { - const body = { avengers: 'assemble' }; - nock(url).post('/', body).reply(200); - const res = await request({ url, method: HTTPMethods.POST, data: body }); - expect(res.config.data).toEqual(body); - }); - - test('should allow for our custom user agent', async () => { - const options = { - reqheaders: { - 'user-agent': (val: string) => { - return /^@vonage\/server-sdk\/[\d].[\d].[\d].* node\/.*$/.test(val); - }, - }, - }; - - nock(url, options).get('/').reply(200); - const inst = new Vetch(); - await inst.request({ url }); - expect(nock.isDone()).toBeTruthy(); - }); - - test('should append to user agent', async () => { - const options = { - reqheaders: { - 'user-agent': (val: string) => { - return /^@vonage\/server-sdk\/[\d].[\d].[\d].* node\/.* foo$/.test( - val, - ); - }, - }, - }; - - nock(url, options).get('/').reply(200); - const inst = new Vetch(); - await inst.request({ url, appendUserAgent: 'foo' }); - expect(nock.isDone()).toBeTruthy(); - }); - - test('should timeout', async () => { - nock(url).get('/').delayConnection(5).reply(200); - const inst = new Vetch({ timeout: 1 }); - await expect(inst.request({ url })).rejects.toThrow( - 'The user aborted a request', - ); - }); -}); - -describe('data handling', () => { - test('should accept a string in the request data', async () => { - const body = { avengers: 'assemble' }; - const encoded = stringify(body); - nock(url) - .matchHeader('content-type', 'application/x-www-form-urlencoded') - .post('/', encoded) - .reply(200, {}); - const res = await request({ - url, - method: HTTPMethods.POST, - data: encoded, - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - }); - expect(res.data).toEqual({}); - }); - - test('should default to application/json content-type with object request', async () => { - const body = { avengers: 'assemble' }; - nock(url) - .matchHeader('Content-Type', 'application/json') - .post('/', JSON.stringify(body)) - .reply(200, {}); - const res = await request({ - url, - method: HTTPMethods.POST, - data: body, - }); - - expect(res.data).toEqual({}); - }); -}); - -describe('defaults and instances', () => { - test('should allow creating a new instance', () => { - const vetch = new Vetch(); - expect(typeof vetch.request).toEqual('function'); - }); - - it('should allow passing empty options', async () => { - const body = { avengers: 'assemble' }; - nock(url).get('/').reply(200, body); - const vetch = new Vetch({ url }); - const res = await vetch.request(); - - expect(res.data).toEqual(body); - }); -}); diff --git a/packages/vetch/lib/enums/HTTPMethods.ts b/packages/vetch/lib/enums/HTTPMethods.ts index 9439eebd..bf629d63 100644 --- a/packages/vetch/lib/enums/HTTPMethods.ts +++ b/packages/vetch/lib/enums/HTTPMethods.ts @@ -1,11 +1,21 @@ +/** + * Enum representing the HTTP methods that can be used for Vonage API requests. + * + * @enum {string} + */ export enum HTTPMethods { + /** @description Represents an HTTP GET request. */ GET = 'GET', - HEAD = 'HEAD', + + /** @description Represents an HTTP POST request. */ POST = 'POST', + + /** @description Represents an HTTP DELETE request. */ DELETE = 'DELETE', + + /** @description Represents an HTTP PUT request. */ PUT = 'PUT', - CONNECT = 'CONNECT', - OPTIONS = 'OPTIONS', - TRACE = 'TRACE', + + /** @description Represents an HTTP PATCH request. */ PATCH = 'PATCH', } diff --git a/packages/vetch/lib/enums/contentType.ts b/packages/vetch/lib/enums/contentType.ts new file mode 100644 index 00000000..5f5cb77e --- /dev/null +++ b/packages/vetch/lib/enums/contentType.ts @@ -0,0 +1,19 @@ +/** + * Enum representing possible MIME types for the 'content-type' HTTP header. + * + * @enum {string} + */ +export enum ContentType { + /** @description Represents the MIME type for JSON data. */ + JSON = 'application/json', + + /** @description Represents the MIME type for URL-encoded form data. */ + FORM_URLENCODED = 'application/x-www-form-urlencoded', + + /** @description Represents the MIME type for XML data. */ + XML = 'application/xml', + + /** @description Represents the MIME type for CSV data. */ + CSV = 'text/csv' +} + diff --git a/packages/vetch/lib/enums/index.ts b/packages/vetch/lib/enums/index.ts new file mode 100644 index 00000000..f509c08d --- /dev/null +++ b/packages/vetch/lib/enums/index.ts @@ -0,0 +1,3 @@ +export * from './HTTPMethods'; +export * from './contentType'; +export * from './responseTypes'; diff --git a/packages/vetch/lib/enums/responseTypes.ts b/packages/vetch/lib/enums/responseTypes.ts index 2fa140c2..fffc7ebd 100644 --- a/packages/vetch/lib/enums/responseTypes.ts +++ b/packages/vetch/lib/enums/responseTypes.ts @@ -1,5 +1,25 @@ +/** + * @deprecated + * Enum representing the expected response types for API requests. + * + * @example + * if (responseType === ResponseTypes.json) { + * // Handle JSON response + * } + */ export enum ResponseTypes { + /** + * @description Represents a JSON-formatted response. + */ json = 'json', + + /** + * @description Represents a stream response, typically for handling large data or files. + */ stream = 'stream', + + /** + * @description Represents a plain text response. + */ text = 'text', } diff --git a/packages/vetch/lib/errors/vetchError.ts b/packages/vetch/lib/errors/vetchError.ts new file mode 100644 index 00000000..c4d3a0ac --- /dev/null +++ b/packages/vetch/lib/errors/vetchError.ts @@ -0,0 +1,29 @@ +import { VetchResponse, VetchOptions } from '../types'; + +/** + * Class representing an error from a Vetch API request. Extends the built-in + * Error class and adds additional properties related to the API request and + * response. + * + * @template T - The type of the data payload in the VetchResponse, expected to be an object that has been decoded from JSON or WebForm. + * + * @property {string} [code] - An optional error code. + * @property {VetchOptions} config - Configuration options for the API request. + * @property {VetchResponse} response - The API response that resulted in the error. + */ +export class VetchError extends Error { + code?: string; + config: VetchOptions; + response!: VetchResponse; + + /** + * Creates an instance of VetchError. + * + * @param {string} message - The error message. + * @param {VetchOptions} options - Configuration options for the API request. + */ + constructor(message: string, options: VetchOptions) { + super(message); + this.config = options; + } +} diff --git a/packages/vetch/lib/index.ts b/packages/vetch/lib/index.ts index 697cc381..0d30b0c3 100644 --- a/packages/vetch/lib/index.ts +++ b/packages/vetch/lib/index.ts @@ -1,16 +1,3 @@ -import { Vetch } from './vetch'; -import { VetchOptions } from './interfaces/vetchOptions'; - -export { Vetch, VetchOptions }; - -export const instance = new Vetch(); - -export async function request(opts: VetchOptions) { - return instance.request(opts); -} -export { VetchError } from './types/vetchError'; -export { Headers } from './interfaces/headers'; -export { VetchPromise } from './types/vetchPromise'; -export { VetchResponse } from './interfaces/vetchResponse'; -export { ResponseTypes } from './enums/responseTypes'; -export * from './enums/HTTPMethods'; +export * from './types'; +export * from './enums'; +export * from './errors/vetchError'; diff --git a/packages/vetch/lib/interfaces/headers.ts b/packages/vetch/lib/interfaces/headers.ts deleted file mode 100644 index b90f8111..00000000 --- a/packages/vetch/lib/interfaces/headers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Headers { - [index: string]: any -} diff --git a/packages/vetch/lib/interfaces/vetchHttpRequest.ts b/packages/vetch/lib/interfaces/vetchHttpRequest.ts deleted file mode 100644 index 54e14902..00000000 --- a/packages/vetch/lib/interfaces/vetchHttpRequest.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface VetchHttpRequest { - responseUrl: string -} diff --git a/packages/vetch/lib/interfaces/vetchOptions.ts b/packages/vetch/lib/interfaces/vetchOptions.ts deleted file mode 100644 index d320ea31..00000000 --- a/packages/vetch/lib/interfaces/vetchOptions.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { HTTPMethods } from '../enums/HTTPMethods'; -import { Headers } from './headers'; -import { ResponseTypes } from '../enums/responseTypes'; -import http from 'http'; -import https from 'https'; -import URL from 'url'; -import { VetchPromise } from '../types/vetchPromise'; - -export interface VetchOptions { - adapter?: ( - options: VetchOptions, - defaultAdapter: (options: VetchOptions) => VetchPromise - ) => VetchPromise; - url?: string; - baseUrl?: string; - baseURL?: string; - method?: HTTPMethods; - headers?: Headers; - data?: any; - body?: any; - params?: any; - responseType?: ResponseTypes; - checkStatus?: (status: number) => boolean; - size?: number; - timeout?: number; - appendUserAgent?: string; - agent?: - | boolean - | http.Agent - | https.Agent - | ((parsedUrl: URL) => boolean | https.Agent | http.Agent); -} diff --git a/packages/vetch/lib/interfaces/vetchResponse.ts b/packages/vetch/lib/interfaces/vetchResponse.ts deleted file mode 100644 index 0b1af586..00000000 --- a/packages/vetch/lib/interfaces/vetchResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Headers } from './headers'; -import { VetchHttpRequest } from './vetchHttpRequest'; -import { VetchOptions } from './vetchOptions'; - -export interface VetchResponse { - config: VetchOptions - data: T - error?: true - status: number - statusText: string - headers: Headers - request: VetchHttpRequest -} diff --git a/packages/vetch/lib/types/headers.ts b/packages/vetch/lib/types/headers.ts new file mode 100644 index 00000000..3f8782b4 --- /dev/null +++ b/packages/vetch/lib/types/headers.ts @@ -0,0 +1,15 @@ +/** + * Interface representing HTTP headers as a string-keyed object. Each property + * represents a header name, and the associated value can be a string or an + * array of strings. Includes optional 'authorization' and 'content-type' + * properties with restricted possible values for 'content-type'. + * + * @example + * const headers: Headers = { + * 'Content-Type': ContentType.JSON, + * 'Accept-Encoding': ['gzip', 'deflate'] + * }; + */ +export type Headers = { + [index: string]: string, +} diff --git a/packages/vetch/lib/types/index.ts b/packages/vetch/lib/types/index.ts new file mode 100644 index 00000000..285cee8d --- /dev/null +++ b/packages/vetch/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './headers'; +export * from './vetchOptions'; +export * from './vetchPromise'; +export * from './vetchResponse'; diff --git a/packages/vetch/lib/types/vetchError.ts b/packages/vetch/lib/types/vetchError.ts deleted file mode 100644 index 629c2ec4..00000000 --- a/packages/vetch/lib/types/vetchError.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { VetchResponse } from '../interfaces/vetchResponse'; -import { VetchOptions } from '../interfaces/vetchOptions'; - -export class VetchError extends Error { - code?: string; - config: VetchOptions; - response: VetchResponse; - - constructor(message: string, options: VetchOptions) { - super(message); - this.config = options; - } -} diff --git a/packages/vetch/lib/types/vetchOptions.ts b/packages/vetch/lib/types/vetchOptions.ts new file mode 100644 index 00000000..3dcad5c9 --- /dev/null +++ b/packages/vetch/lib/types/vetchOptions.ts @@ -0,0 +1,41 @@ +import { ContentType, HTTPMethods, ResponseTypes } from '../enums'; +import { Headers } from './headers'; + +/** + * Options to configure the Vetch request. + * + * @property {ContentType} type - The content type for the request, e.g., JSON, WebForm, etc. + * @property {HTTPMethods} method - The HTTP method to use for the request, e.g., GET, POST, etc. + * @property {string} url - The URL endpoint for the request. + * @property {Record | string} [data] - The request payload. + * @property {string | Record} [params] - Query parameters for the request. + * @property {ResponseTypes} [responseType] - Expected response type. + * @property {(status: number) => boolean} [checkStatus] - A function to check the response status. + * @property {string} [appendUserAgent] - Additional user agent string to append. + * @property {Headers} headers - The headers to be sent with the request. + * @property {number} [timeout] - Time in milliseconds to wait for the request to complete. + * @property {Record} [body] - Request body data. Use 'data' instead of 'body' for newer implementations. + * @property {...RequestInit} ... - Additional properties from the `RequestInit` type from 'node-fetch'. + * + * @example + * const options: VetchOptions = { + * type: ContentType.JSON, + * method: HTTPMethods.GET, + * url: 'https://api.example.com/data' + * }; + */ +export type VetchOptions = { + type: ContentType; + method: HTTPMethods; + url: string; + data?: Record; + /** @deprecated Use 'data' instead of 'body' */ + body?: Record; + params?: Record; + /** @deprecated `ResponseTypes` is deprecated and will be removed in future versions. */ + responseType?: ResponseTypes; + checkStatus?: (status: number) => boolean; + appendUserAgent?: string; + headers: Headers; + timeout?: number; +} diff --git a/packages/vetch/lib/types/vetchPromise.ts b/packages/vetch/lib/types/vetchPromise.ts index f71a2f32..34942934 100644 --- a/packages/vetch/lib/types/vetchPromise.ts +++ b/packages/vetch/lib/types/vetchPromise.ts @@ -1,3 +1,12 @@ -import { VetchResponse } from '../interfaces/vetchResponse'; +import { VetchResponse } from './vetchResponse'; +/** + * Type representing a promise that resolves with a standardized Vetch API + * response. Vetch ("Vonage Fetch") ensures a consistent API response + * structure, irrespective of the HTTP adapter utilized by the developer. + * + * @deprecated + * + * @template T - The type of the data payload in the VetchResponse, expected to be an object that has been decoded from JSON or WebForm. + */ export type VetchPromise = Promise> diff --git a/packages/vetch/lib/types/vetchResponse.ts b/packages/vetch/lib/types/vetchResponse.ts new file mode 100644 index 00000000..e765b7a1 --- /dev/null +++ b/packages/vetch/lib/types/vetchResponse.ts @@ -0,0 +1,27 @@ +import { VetchOptions } from './vetchOptions'; +import { Headers } from '../types'; + +/** + * Represents the response returned by a Vetch request. + * + * @template T - The type of the response data. + * + * @property {VetchOptions} config - The configuration options used for the request. + * @property {T} data - The parsed response data. + * @property {Headers} headers - The response headers. + * @property {number} status - The HTTP status code of the response. + * @property {string} statusText - The HTTP status text of the response. + * @property {VetchOptions} request - The configuration options for the request (same as 'config'). + * + * @example + * const response: VetchResponse = await vetchRequest(options); + * console.log(response.data); // Parsed data of type MyDataType + */ +export type VetchResponse = { + config: VetchOptions + data?: T + headers: Headers, + status: number; + statusText: string; + request: VetchOptions, +} diff --git a/packages/vetch/lib/vetch.ts b/packages/vetch/lib/vetch.ts deleted file mode 100644 index fc486e06..00000000 --- a/packages/vetch/lib/vetch.ts +++ /dev/null @@ -1,177 +0,0 @@ -import fetch, { Response as fetchResponse, options } from 'node-fetch'; -import { stringify } from 'querystring'; -import merge from 'lodash.merge'; -import http from 'http'; -import https from 'https'; -import { VetchError } from './types/vetchError'; -import { Headers } from './interfaces/headers'; -import { VetchResponse } from './interfaces/vetchResponse'; -import { ResponseTypes } from './enums/responseTypes'; -import { VetchOptions } from './interfaces/vetchOptions'; -import debug from 'debug'; - -const log = debug('vonage:vetch'); - -export class Vetch { - defaults: VetchOptions; - - constructor(defaults?: VetchOptions) { - this.defaults = defaults || { responseType: ResponseTypes.json }; - if (!this.defaults.responseType) { - this.defaults.responseType = ResponseTypes.json; - } - } - - private async _defaultAdapter( - opts: VetchOptions, - ): Promise> { - const { timeout } = opts; - let timeoutId = null; - const fetchConfig: options = opts; - if (timeout) { - const controller = new AbortController(); - timeoutId = setTimeout(() => controller.abort(), timeout); - fetchConfig.signal = controller.signal; - } - - try { - const res = await fetch(opts.url, fetchConfig); - const data = await this.getResponseData(opts, res); - return this.createResponse(opts, res, data); - } finally { - clearTimeout(timeoutId); - } - } - - async request(opts: VetchOptions = {}): Promise> { - opts = this.validateOpts(opts); - - log('api request', opts); - - const formattedResponse = await this._defaultAdapter(opts); - - log('api response', formattedResponse); - if (!opts.checkStatus(formattedResponse.status)) { - const err = new VetchError( - `Request failed with status code ${formattedResponse.status}`, - opts, - ); - err.code = String(formattedResponse.status); - err.response = formattedResponse; - throw err; - } - - return formattedResponse; - } - - private async getResponseData( - opts: VetchOptions, - res: fetchResponse, - ): Promise { - switch (opts.responseType) { - case 'stream': - return res.buffer(); - case 'json': { - let data = await res.text(); - try { - data = JSON.parse(data); - } catch { - // continue - } - return data; - } - default: - return res.text(); - } - } - - private validateOpts(options: VetchOptions): VetchOptions { - const opts = merge({}, this.defaults, options); - - opts.headers = opts.headers || {}; - opts.checkStatus = this.checkStatus; - - if (!opts.url) { - throw new Error('URL is required.'); - } - - const baseUrl = opts.baseUrl || opts.baseURL; - if (baseUrl) { - opts.url = baseUrl + opts.url; - } - - if (opts.params) { - let queryParams = stringify(opts.params); - if (queryParams.startsWith('?')) { - queryParams = queryParams.slice(1); - } - - const prefix = opts.url.includes('?') ? '&' : '?'; - opts.url = `${opts.url}${prefix}${queryParams}`; - } - - if (opts.data) { - if (typeof opts.data === 'object') { - opts.body = JSON.stringify(opts.data); - opts.headers['Content-Type'] = 'application/json'; - } else { - opts.body = opts.data; - } - } - - if (!opts.headers.Accept && opts.responseType === 'json') { - opts.headers.Accept = 'application/json'; - } - - // Set our user agent - opts.headers['user-agent'] = [ - `@vonage/server-sdk/3.0.0`, - ` node/${process.version.replace('v', '')}`, - opts.appendUserAgent ? ` ${opts.appendUserAgent}` : '', - ].join(''); - - // Allow a custom timeout to be used - const httpAgent = new http.Agent({ - timeout: opts.timeout, - }); - const httpsAgent = new https.Agent({ - timeout: opts.timeout, - }); - opts.agent = (parsedUrl: URL): https.Agent | http.Agent => { - if (parsedUrl.protocol === 'http:') { - return httpAgent; - } else { - return httpsAgent; - } - }; - - return opts; - } - - private checkStatus(status: number) { - return status >= 200 && status < 300; - } - - private createResponse( - opts: VetchOptions, - res: fetchResponse, - data?: T, - ): VetchResponse { - const headers = {} as Headers; - - res.headers.forEach((value, key) => { - headers[key] = value; - }); - - return { - config: opts, - data: data as T, - headers, - status: res.status, - statusText: res.statusText, - request: { - responseUrl: res.url, - }, - }; - } -} diff --git a/packages/vetch/package.json b/packages/vetch/package.json index bb2cb53d..853a9230 100644 --- a/packages/vetch/package.json +++ b/packages/vetch/package.json @@ -1,7 +1,8 @@ { + "$schema": "https://json.schemastore.org/package.json", "name": "@vonage/vetch", - "version": "1.5.1", - "description": "Vonage package for server side fetch.", + "version": "1.6.0", + "description": "Vonage's type and enum definitions module for server-side HTTP interactions.", "homepage": "https://github.com/vonage/vonage-node-sdk/tree/master/packages/vetch#readme", "bugs": { "url": "https://github.com/Vonage/vonage-node-sdk/issues" @@ -11,25 +12,31 @@ "url": "git+https://github.com/Vonage/vonage-node-sdk.git" }, "license": "Apache-2.0", - "author": "Kelly J Andrews ", + "contributors": [ + { + "name": "Kelly J Andrews", + "email": "kelly@kellyjandrews.com" + }, + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com" + }, + { + "name": "Chuck \"MANCHUCK\" Reeves", + "email": "chuck@manchuck.com" + } + ], "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "/dist" ], "scripts": { + "prepublishOnly": "npm run build", "build": "npm run clean && npm run compile", "clean": "npx shx rm -rf dist tsconfig.tsbuildinfo", "compile": "npx tsc --build --verbose" }, - "dependencies": { - "debug": "^4.3.4", - "lodash.merge": "^4.6.2", - "node-fetch": "^2.2" - }, - "devDependencies": { - "nock": "^13.3.4" - }, "publishConfig": { "directory": "dist" }