diff --git a/src/constants.js b/src/constants.ts similarity index 100% rename from src/constants.js rename to src/constants.ts diff --git a/src/easypost.js b/src/easypost.ts similarity index 82% rename from src/easypost.js rename to src/easypost.ts index b86082806..40f6c9fe1 100644 --- a/src/easypost.js +++ b/src/easypost.ts @@ -1,7 +1,6 @@ import util from 'util'; import { v4 as uuid } from 'uuid'; -import pkg from '../package.json'; import Constants from './constants'; import ErrorHandler from './errors/error_handler'; import MissingParameterError from './errors/general/missing_parameter_error'; @@ -39,6 +38,35 @@ import UserService from './services/user_service'; import WebhookService from './services/webhook_service'; import Utils from './utils/util'; +const pkgVersion = process.env.npm_package_version ?? 'unknown'; + +type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; +type RequestHeaders = Record; + +type ClientOptions = { + apiKey?: string; + useProxy?: boolean; + timeout?: number; + baseUrl?: string; + httpMiddleware?: any; + requestMiddleware?: any; + httpClient?: any; +}; + +type HookValue = { + method: string; + path: string; + requestBody: unknown; + headers: RequestHeaders; + requestTimestamp: number; + requestUUID: string; + httpStatus?: number; + responseBody?: unknown; + responseTimestamp?: number; +}; + +type HookHandler = any; + /** * The client used to access services of the EasyPost API. * This client is configured to use the latest production version of the EasyPost API. @@ -46,7 +74,25 @@ import Utils from './utils/util'; * @param {Object} [options] Additional options to use for the underlying HTTP client (e.g. middleware, proxy configuration). */ export default class EasyPostClient { - constructor(key, options = {}) { + static MS_SECOND: number; + static DEFAULT_TIMEOUT: number; + static DEFAULT_BASE_URL: string; + static DEFAULT_HEADERS: RequestHeaders; + static METHODS: Record<'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', HttpMethod>; + static SERVICES: Record; + + [key: string]: any; + key?: string; + useProxy?: boolean; + timeout: number; + baseUrl: string; + httpClient: any; + requestMiddleware?: any; + requestHooks: HookHandler[]; + responseHooks: HookHandler[]; + Utils: Utils; + + constructor(key?: string, options: ClientOptions = {}) { const { useProxy, timeout, baseUrl, httpMiddleware, requestMiddleware, httpClient } = options; if (!key && !useProxy) { @@ -59,7 +105,13 @@ export default class EasyPostClient { this.timeout = timeout || EasyPostClient.DEFAULT_TIMEOUT; this.baseUrl = baseUrl || EasyPostClient.DEFAULT_BASE_URL; this.httpClient = - httpClient || (typeof fetch === 'function' ? (...args) => fetch(...args) : undefined); + httpClient || + (typeof fetch === 'function' + ? (input: RequestInfo | URL, init?: RequestInit) => fetch(input, init) + : async () => { + throw new Error('No global fetch implementation found. Node 18+ is required.'); + }); + this.useProxy = useProxy; this.requestMiddleware = requestMiddleware; this.requestHooks = []; this.responseHooks = []; @@ -80,20 +132,20 @@ export default class EasyPostClient { * Add a request hook function. * @param {(config: object) => void} hook */ - addRequestHook(hook) { + addRequestHook(hook: HookHandler): void { this.requestHooks = [...this.requestHooks, hook]; } /** * Remove a request hook function. * @param {(config: object) => void} hook */ - removeRequestHook(hook) { + removeRequestHook(hook: HookHandler): void { this.requestHooks = this.requestHooks.filter((h) => h !== hook); } /** * Clear all request hooks. */ - clearRequestHooks() { + clearRequestHooks(): void { this.requestHooks = []; } @@ -101,20 +153,20 @@ export default class EasyPostClient { * Add a response hook function. * @param {(config: object) => void} hook */ - addResponseHook(hook) { + addResponseHook(hook: HookHandler): void { this.responseHooks = [...this.responseHooks, hook]; } /** * Remove a response hook function. * @param {(config: object) => void} hook */ - removeResponseHook(hook) { + removeResponseHook(hook: HookHandler): void { this.responseHooks = this.responseHooks.filter((h) => h !== hook); } /** * Clear all response hooks. */ - clearResponseHooks() { + clearResponseHooks(): void { this.responseHooks = []; } @@ -129,7 +181,11 @@ export default class EasyPostClient { * @param {Object} [params] - The parameters to send with the request. * @returns {Promise} The response from the API call. */ - async makeApiCall(method, endpoint, params = {}) { + async makeApiCall( + method: string, + endpoint: string, + params: Record = {}, + ): Promise { const response = await this._request(endpoint, method, params); return response.body; @@ -141,7 +197,7 @@ export default class EasyPostClient { * @param {Object} [options] The options to override. * @returns {EasyPostClient} A new `EasyPostClient` instance. */ - static copyClient(client, options = {}) { + static copyClient(client: EasyPostClient, options: ClientOptions = {}): EasyPostClient { const { apiKey, useProxy, timeout, baseUrl, httpMiddleware, requestMiddleware, httpClient } = options; const nextHttpClient = @@ -161,7 +217,7 @@ export default class EasyPostClient { * @param {string} method - The method passed in by callers. * @returns {string} lowercase method suitable for fetch. */ - static _normalizeMethod(method = EasyPostClient.METHODS.GET) { + static _normalizeMethod(method: string = EasyPostClient.METHODS.GET): string { return method.toLowerCase(); } @@ -169,7 +225,7 @@ export default class EasyPostClient { * Executes a fetch request with timeout support. * @private */ - async _fetchWithTimeout(url, init) { + async _fetchWithTimeout(url: string, init: RequestInit): Promise { if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') { return this.httpClient(url, { ...init, @@ -194,7 +250,7 @@ export default class EasyPostClient { * Parse an HTTP response body. * @private */ - async _parseResponseBody(response) { + async _parseResponseBody(response: Response): Promise { const text = await response.text(); if (!text) { return {}; @@ -211,7 +267,7 @@ export default class EasyPostClient { * Encodes a string to base64 in both Node and edge runtimes. * @private */ - static _toBase64(value) { + static _toBase64(value: string): string { if (typeof Buffer !== 'undefined') { return Buffer.from(value).toString('base64'); } @@ -224,7 +280,7 @@ export default class EasyPostClient { * @param {Object} [additionalHeaders] Additional headers to combine or override with the default headers. * @returns {Object} The headers to use for the request. */ - static _buildHeaders(additionalHeaders = {}) { + static _buildHeaders(additionalHeaders: RequestHeaders = {}): RequestHeaders { return { ...EasyPostClient.DEFAULT_HEADERS, 'User-Agent': EasyPostClient._buildUserAgent(), @@ -237,7 +293,7 @@ export default class EasyPostClient { * do not expose Node globals/modules. * @returns {string} The default User-Agent header value. */ - static _buildUserAgent() { + static _buildUserAgent(): string { let nodeVersion = 'unknown'; let osName = 'unknown'; let osVersion = 'unknown'; @@ -252,14 +308,14 @@ export default class EasyPostClient { osVersion; } - return `EasyPost/v2 NodejsClient/${pkg.version} Nodejs/${nodeVersion} OS/${osName} OSVersion/${osVersion} OSArch/${osArch}`; + return `EasyPost/v2 NodejsClient/${pkgVersion} Nodejs/${nodeVersion} OS/${osName} OSVersion/${osVersion} OSArch/${osArch}`; } /** * Attach services to an {@link EasyPostClient} instance. * @param {Map} services - A map of {@link BaseService}-based service classes to construct and attach to the client. */ - _attachServices(services) { + _attachServices(services: Record): void { Object.keys(services).forEach((s) => { this[s] = services[s](this); }); @@ -270,7 +326,7 @@ export default class EasyPostClient { * @param {string} path - The path to build. * @returns {string} The full path to use for the HTTP request. */ - _buildPath(path = '') { + _buildPath(path = ''): string { if (path.indexOf('http') === 0) { return path; } @@ -290,7 +346,7 @@ export default class EasyPostClient { * @param {Object} response - the response from the HTTP request * @returns {Object} - the value to be passed to the responseHooks */ - _createResponseHooksValue(baseHooksValue, response) { + _createResponseHooksValue(baseHooksValue: HookValue, response: any): HookValue { return { ...baseHooksValue, httpStatus: response.status, @@ -309,7 +365,12 @@ export default class EasyPostClient { * @returns {*} The response from the HTTP request. * @throws {ApiError} If the request fails. */ - async _request(path = '', method = EasyPostClient.METHODS.GET, params = {}, headers = {}) { + async _request( + path = '', + method: string = EasyPostClient.METHODS.GET, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { const urlPath = this._buildPath(path); const normalizedMethod = EasyPostClient._normalizeMethod(method); const requestHeaders = EasyPostClient._buildHeaders(headers); @@ -322,7 +383,7 @@ export default class EasyPostClient { if (params !== undefined) { if (isQueryMethod) { Object.entries(params).forEach(([key, value]) => { - url.searchParams.append(key, value); + url.searchParams.append(key, String(value)); }); } else { requestBody = params; @@ -343,7 +404,7 @@ export default class EasyPostClient { }, query: (queryParams = {}) => { Object.entries(queryParams).forEach(([key, value]) => { - url.searchParams.append(key, value); + url.searchParams.append(key, String(value)); }); compatibilityRequest.url = url.toString(); return compatibilityRequest; @@ -386,7 +447,7 @@ export default class EasyPostClient { middlewareResponse = middlewareRequest.send(params); } - const baseHooksValue = { + const baseHooksValue: HookValue = { method, path: middlewareRequest.url || url.toString(), requestBody: middlewareRequest._data, @@ -446,16 +507,21 @@ export default class EasyPostClient { return response; } catch (error) { - if (error.statusCode && error.body) { - const responseHooksValue = this._createResponseHooksValue(baseHooksValue, error); + const handledError = error as any; + + if (handledError.statusCode && handledError.body) { + const responseHooksValue = this._createResponseHooksValue(baseHooksValue, handledError); this.responseHooks.forEach((fn) => fn(responseHooksValue)); - throw ErrorHandler.handleApiError(error); - } else if (error.response && error.response.body) { - const responseHooksValue = this._createResponseHooksValue(baseHooksValue, error.response); + throw ErrorHandler.handleApiError(handledError); + } else if (handledError.response && handledError.response.body) { + const responseHooksValue = this._createResponseHooksValue( + baseHooksValue, + handledError.response, + ); this.responseHooks.forEach((fn) => fn(responseHooksValue)); - throw ErrorHandler.handleApiError(error.response); + throw ErrorHandler.handleApiError(handledError.response); } else { - throw error; + throw handledError; } } } @@ -467,7 +533,11 @@ export default class EasyPostClient { * @param {Object} [headers] - Additional headers to send with the request. * @returns {*} The response from the HTTP request. */ - _get(path, params = {}, headers = {}) { + _get( + path: string, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { return this._request(path, EasyPostClient.METHODS.GET, params, headers); } @@ -478,7 +548,11 @@ export default class EasyPostClient { * @param {Object} [headers] - Additional headers to send with the request. * @returns {*} The response from the HTTP request. */ - _post(path, params = {}, headers = {}) { + _post( + path: string, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { return this._request(path, EasyPostClient.METHODS.POST, params, headers); } @@ -489,7 +563,11 @@ export default class EasyPostClient { * @param {Object} [headers] - Additional headers to send with the request. * @returns {*} The response from the HTTP request. */ - _put(path, params = {}, headers = {}) { + _put( + path: string, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { return this._request(path, EasyPostClient.METHODS.PUT, params, headers); } @@ -500,7 +578,11 @@ export default class EasyPostClient { * @param {Object} [headers] - Additional headers to send with the request. * @returns {*} The response from the HTTP request. */ - _patch(path, params = {}, headers = {}) { + _patch( + path: string, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { return this._request(path, EasyPostClient.METHODS.PATCH, params, headers); } @@ -511,7 +593,11 @@ export default class EasyPostClient { * @param {Object} [headers] - Additional headers to send with the request. * @returns {*} The response from the HTTP request. */ - _delete(path, params = {}, headers = {}) { + _delete( + path: string, + params: Record = {}, + headers: RequestHeaders = {}, + ): Promise { return this._request(path, EasyPostClient.METHODS.DELETE, params, headers); } } diff --git a/src/utils/internal_util.js b/src/utils/internal_util.ts similarity index 100% rename from src/utils/internal_util.js rename to src/utils/internal_util.ts diff --git a/src/utils/util.js b/src/utils/util.ts similarity index 87% rename from src/utils/util.js rename to src/utils/util.ts index ab26d7804..c355313cc 100644 --- a/src/utils/util.js +++ b/src/utils/util.ts @@ -6,6 +6,17 @@ import FilteringError from '../errors/general/filtering_error'; import InvalidParameterError from '../errors/general/invalid_parameter_error'; import SignatureVerificationError from '../errors/general/signature_verification_error'; +type SmartRate = { + rate: string; + time_in_transit: Record; +}; + +type Rate = { + rate: string; + carrier: string; + service: string; +}; + /** * Utility class of various publicly-available helper functions. * @public @@ -22,7 +33,11 @@ export default class Utils { * @throws {FilteringError} - If no applicable rates are found * @throws {InvalidParameterError} - If the deliveryAccuracy value is invalid */ - getLowestSmartRate(smartrates, deliveryDays, deliveryAccuracy) { + getLowestSmartRate( + smartrates: SmartRate[], + deliveryDays: number | string, + deliveryAccuracy: string, + ): SmartRate { const validDeliveryAccuracyValues = new Set([ 'percentile_50', 'percentile_75', @@ -32,13 +47,13 @@ export default class Utils { 'percentile_97', 'percentile_99', ]); - let lowestSmartRate = null; + let lowestSmartRate: SmartRate | null = null; const lowercaseDeliveryAccuracy = deliveryAccuracy.toLowerCase(); if (!validDeliveryAccuracyValues.has(lowercaseDeliveryAccuracy)) { throw new InvalidParameterError({ - message: `Invalid deliveryAccuracy value, must be one of: ${new Array( - ...validDeliveryAccuracyValues, + message: `Invalid deliveryAccuracy value, must be one of: ${Array.from( + validDeliveryAccuracyValues, ).join(', ')}`, }); } @@ -46,7 +61,7 @@ export default class Utils { for (let i = 0; i < smartrates.length; i += 1) { const rate = smartrates[i]; - if (rate.time_in_transit[lowercaseDeliveryAccuracy] > parseInt(deliveryDays, 10)) { + if (rate.time_in_transit[lowercaseDeliveryAccuracy] > parseInt(String(deliveryDays), 10)) { // eslint-disable-next-line no-continue continue; } else if ( @@ -73,7 +88,11 @@ export default class Utils { * @returns {Rate} - The lowest rate * @throws {FilteringError} - If no applicable rates are found */ - getLowestRate(rates, carriers = null, services = null) { + getLowestRate( + rates: Rate[], + carriers: string[] | null = null, + services: string[] | null = null, + ): Rate { if (carriers) { const carriersLower = carriers.map((carrier) => carrier.toLowerCase()); // eslint-disable-next-line no-param-reassign @@ -111,8 +130,12 @@ export default class Utils { * @returns {object} - The JSON-parsed webhook event body if the signature could be verified * @throws {SignatureVerificationError} - If the signature could not be verified */ - validateWebhook(eventBody, headers, webhookSecret) { - let webhook = {}; + validateWebhook( + eventBody: Buffer | string, + headers: Record, + webhookSecret: string, + ): Record { + let webhook: Record = {}; const easypostHmacSignature = headers['X-Hmac-Signature'] ?? headers['x-hmac-signature'] ?? null; diff --git a/tsconfig.build.json b/tsconfig.build.json index e301a68b4..f4bfc21ac 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,6 +3,7 @@ "compilerOptions": { "allowJs": true, "checkJs": false, + "noImplicitAny": false, "declaration": true, "noEmit": true }, diff --git a/vite.config.js b/vite.config.js index 901662487..653e064df 100644 --- a/vite.config.js +++ b/vite.config.js @@ -10,7 +10,7 @@ export default defineConfig({ // drop node 12 support in the future, change this to the next min version target: 'node12', lib: { - entry: path.resolve(__dirname, 'src/easypost.js'), + entry: path.resolve(__dirname, 'src/easypost.ts'), fileName: 'easypost', }, sourcemap: isDev, @@ -32,7 +32,7 @@ export default defineConfig({ }, resolve: { - extensions: ['.js'], + extensions: ['.ts', '.js'], alias: { '@': path.resolve(__dirname, 'src'), },