diff --git a/src/fetch/debug.ts b/src/fetch/debug.ts index cdd4d89..7c1f037 100644 --- a/src/fetch/debug.ts +++ b/src/fetch/debug.ts @@ -8,19 +8,19 @@ export const DEFAULT_DEBUG_OPTS: DebugOpts = { includeVerbose: false, }; -type FetchDebugOpts = { +type FetchDebugOpts = { message: any; /** Is the message being logged verbose? */ isVerbose?: boolean; - parsedOpts: ParsedFetchOpts; + parsedOpts: ParsedFetchOpts; }; -export const fetchDebug = ({ +export const fetchDebug = ({ message, isVerbose = false, parsedOpts: { debug: { enabled, logToPersistedLog, includeVerbose }, }, -}: FetchDebugOpts) => { +}: FetchDebugOpts) => { if (!enabled) return; if (isVerbose && !includeVerbose) return; return logToPersistedLog ? PersistedLog.log(message) : tidyLog(message); diff --git a/src/fetch/fetchBase.ts b/src/fetch/fetchBase.ts index a3205a5..93df3de 100644 --- a/src/fetch/fetchBase.ts +++ b/src/fetch/fetchBase.ts @@ -5,14 +5,14 @@ import { DEFAULT_DEBUG_OPTS, fetchDebug } from './debug'; import { FetchOpts, ParsedFetchOpts } from './types'; import { maybeCensorHeaders } from './utils'; -const getBody = ({ body, contentType }: ParsedFetchOpts) => { +const getBody = ({ body, contentType }: ParsedFetchOpts) => { if (!body) return null; if (isString(body)) return body; const isBodyUrlEncoded = contentType === 'application/x-www-form-urlencoded'; return isBodyUrlEncoded ? getUrlEncodedParams(body) : JSON.stringify(body); }; -const getRequestAttributes = (opts: ParsedFetchOpts) => ({ +const getRequestAttributes = (opts: ParsedFetchOpts) => ({ body: getBody(opts), headers: { 'Content-Type': opts.contentType, @@ -21,7 +21,7 @@ const getRequestAttributes = (opts: ParsedFetchOpts) => ({ }); /** Gets an object which describes the ongoing request for debug logging. */ -const getRequestLogMessage = (opts: ParsedFetchOpts) => { +const getRequestLogMessage = (opts: ParsedFetchOpts) => { const { body, headers } = getRequestAttributes(opts); return { ...opts, @@ -30,7 +30,7 @@ const getRequestLogMessage = (opts: ParsedFetchOpts) => { }; }; -const getRequest = (opts: ParsedFetchOpts) => { +const getRequest = (opts: ParsedFetchOpts) => { const { url, method } = opts; const { body, headers } = getRequestAttributes(opts); const req = new Request(url); @@ -40,10 +40,10 @@ const getRequest = (opts: ParsedFetchOpts) => { return req; }; -const parseFetchOpts = ({ +const parseFetchOpts = ({ debug = DEFAULT_DEBUG_OPTS, ...restOpts -}: FetchOpts): ParsedFetchOpts => ({ debug, ...restOpts }); +}: FetchOpts): ParsedFetchOpts => ({ debug, ...restOpts }); const isStatusCodeError = (code: unknown) => { const str = String(code); @@ -76,19 +76,20 @@ const getResponseError = (response: unknown): string | null => { } }; -export default async (opts: FetchOpts) => { +export default async (opts: FetchOpts) => { const parsedOpts = parseFetchOpts(opts); const log = (message: any, isVerbose = false) => fetchDebug({ message: String(message), parsedOpts, isVerbose }); - await log(`Initiating request to "${opts.url}"...`); + const { url, fetchFnKey, responseValidator } = parsedOpts; + + await log(`Initiating request to "${url}"...`); log(getRequestLogMessage(parsedOpts)); const requestObj = getRequest(parsedOpts); - // Intentionally not trying or catching here -- this is the responsibility of - // the calling function. // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const response = (await requestObj[opts.fetchFnKey]()) as unknown; + const response = (await requestObj[fetchFnKey]()) as unknown; await log({ response }, true); + const { statusCode } = requestObj.response; const isErrorCode = isStatusCodeError(statusCode); if (isErrorCode) { @@ -99,5 +100,20 @@ export default async (opts: FetchOpts) => { log({ error }); throw error; } + + // Check that response type is as expected + if (responseValidator) { + try { + const { isValid, errorMessage } = responseValidator(response as Returns); + if (!isValid) throw new Error(errorMessage ?? 'validation failed'); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'can not parse error'; + throw new Error( + `Error during responseValidator execution: ${errorMessage}` + ); + } + } + return response as Returns; }; diff --git a/src/fetch/multipart/types.ts b/src/fetch/multipart/types.ts index 0017691..9c98abe 100644 --- a/src/fetch/multipart/types.ts +++ b/src/fetch/multipart/types.ts @@ -7,7 +7,7 @@ export type MultipartRequest = Pick & { }; export type MultipartOpts = Omit< - FetchImplementationOpts, + FetchImplementationOpts, 'body' | 'contentType' > & { requests: MultipartRequest[]; diff --git a/src/fetch/types.ts b/src/fetch/types.ts index 9658fd8..bac7a2a 100644 --- a/src/fetch/types.ts +++ b/src/fetch/types.ts @@ -9,21 +9,25 @@ export type DebugOpts = { includeVerbose?: boolean; }; -export type FetchOpts = Pick & { +export type FetchOpts = Pick & { contentType: string; fetchFnKey: FunctionKeys; body?: string | AnyObj; debug?: DebugOpts; + responseValidator?: (response: R) => { + isValid: boolean; + errorMessage?: string; + }; }; -export type ParsedFetchOpts = MakeSomeReqd; +export type ParsedFetchOpts = MakeSomeReqd, 'debug'>; /** * When implemented, the fetch fn and method are known, and content type * has a default value */ -export type FetchImplementationOpts = Omit< - FetchOpts, +export type FetchImplementationOpts = Omit< + FetchOpts, 'fetchFnKey' | 'method' | 'contentType' > & - Partial>; + Partial, 'contentType'>>; diff --git a/src/fetch/utils.ts b/src/fetch/utils.ts index 8cbd59d..bd5752f 100644 --- a/src/fetch/utils.ts +++ b/src/fetch/utils.ts @@ -2,9 +2,9 @@ import { AnyObj } from '../types/utilTypes'; import { ParsedFetchOpts } from './types'; // Don't log auth details in persisted log -export const maybeCensorHeaders = ( +export const maybeCensorHeaders = ( headers: AnyObj, - { debug: { logToPersistedLog } }: ParsedFetchOpts + { debug: { logToPersistedLog } }: ParsedFetchOpts ) => { if (!logToPersistedLog || !headers.Authorization) return headers; return { ...headers, Authorization: 'Censored for security' }; diff --git a/src/fetch/verbs/get.ts b/src/fetch/verbs/get.ts index 2a45686..7be96fc 100644 --- a/src/fetch/verbs/get.ts +++ b/src/fetch/verbs/get.ts @@ -1,18 +1,18 @@ import fetchBase from '../fetchBase'; import { FetchImplementationOpts, FetchOpts } from '../types'; -const getArgs = ( - contentType: FetchOpts['contentType'], - fetchFnKey: FetchOpts['fetchFnKey'], - opts: FetchImplementationOpts -): FetchOpts => ({ +const getArgs = ( + contentType: FetchOpts['contentType'], + fetchFnKey: FetchOpts['fetchFnKey'], + opts: FetchImplementationOpts +): FetchOpts => ({ method: 'GET', contentType, fetchFnKey, ...opts, }); -export const getJson = (opts: FetchImplementationOpts) => +export const getJson = (opts: FetchImplementationOpts) => fetchBase(getArgs('application/json', 'loadJSON', opts)); // export const getString = (opts: FetchImplementationOpts) => diff --git a/src/fetch/verbs/post.ts b/src/fetch/verbs/post.ts index 7806a48..e78b178 100644 --- a/src/fetch/verbs/post.ts +++ b/src/fetch/verbs/post.ts @@ -1,19 +1,19 @@ import fetchBase from '../fetchBase'; import { FetchImplementationOpts, FetchOpts } from '../types'; -const getArgs = ( - contentType: FetchOpts['contentType'], - fetchFnKey: FetchOpts['fetchFnKey'], - opts: FetchImplementationOpts -): FetchOpts => ({ +const getArgs = ( + contentType: FetchOpts['contentType'], + fetchFnKey: FetchOpts['fetchFnKey'], + opts: FetchImplementationOpts +): FetchOpts => ({ method: 'POST', contentType, fetchFnKey, ...opts, }); -export const postJson = (opts: FetchImplementationOpts) => +export const postJson = (opts: FetchImplementationOpts) => fetchBase(getArgs('application/json', 'loadJSON', opts)); -export const postString = (opts: FetchImplementationOpts) => +export const postString = (opts: FetchImplementationOpts) => fetchBase(getArgs('application/json', 'loadString', opts)); diff --git a/src/fetch/verbs/put.ts b/src/fetch/verbs/put.ts index 647d7e9..1b0966f 100644 --- a/src/fetch/verbs/put.ts +++ b/src/fetch/verbs/put.ts @@ -1,13 +1,13 @@ import fetchBase from '../fetchBase'; import { FetchImplementationOpts, FetchOpts } from '../types'; -const getArgs = ( - contentType: FetchOpts['contentType'], - fetchFnKey: FetchOpts['fetchFnKey'], - opts: FetchImplementationOpts -): FetchOpts => ({ method: 'PUT', contentType, fetchFnKey, ...opts }); +const getArgs = ( + contentType: FetchOpts['contentType'], + fetchFnKey: FetchOpts['fetchFnKey'], + opts: FetchImplementationOpts +): FetchOpts => ({ method: 'PUT', contentType, fetchFnKey, ...opts }); -export const putJson = (opts: FetchImplementationOpts) => +export const putJson = (opts: FetchImplementationOpts) => fetchBase(getArgs('application/json', 'loadJSON', opts)); // export const putString = (opts: FetchImplementationOpts) =>