diff --git a/index.ts b/index.ts index 1317244..c38226d 100644 --- a/index.ts +++ b/index.ts @@ -1 +1 @@ -export { sendPost, sendGet, sendPut, sendDelete, sendPatch } from './src/client.js' +export { sendPost, sendGet, sendPut, sendDelete, sendPatch, UNKNOWN_SCHEMA } from './src/client.js' diff --git a/src/client.test.ts b/src/client.test.ts index 3f5ec83..55b2ec9 100644 --- a/src/client.test.ts +++ b/src/client.test.ts @@ -817,16 +817,22 @@ describe('frontend-http-client', () => { const response = await sendGet(client, { path: '/', - responseBodySchema: z.null(), + responseBodySchema: z.object({ + id: z.string(), + }), isEmptyResponseExpected: true, }) - // This is for checking TS types, we are checking if it infers that the type is not WretchResponse correctly if (response) { - // @ts-expect-error WretchResponse has this field, ResponseBody does not + // @ts-expect-error WretchResponse has this field, null does not expect(response.ok).toBe(true) } + // This is to test TS types: it should correctly infer that value is null or defined schema + if (response) { + expect(response.id).toBeDefined() + } + expect(response).toBe(null) }) @@ -857,9 +863,7 @@ describe('frontend-http-client', () => { }) // This is for checking TS types, we are checking if it infers the responseBody type as null | WretchResponse correctly - if (responseBody) { - expect(responseBody.ok).toBe(true) - } + expect(responseBody.ok).toBe(true) expect(responseBody).containSubset({ status: 200, diff --git a/src/client.ts b/src/client.ts index 926d970..8cb6296 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,3 @@ -import { stringify } from 'fast-querystring' -import type { WretchResponse } from 'wretch' -import { WretchError } from 'wretch/resolver' import { z } from 'zod' import type { @@ -12,89 +9,31 @@ import type { ResourceChangeParams, WretchInstance, } from './types.js' -import { tryToResolveJsonBody } from './utils/bodyUtils.js' -import { type Either, failure, success, isFailure } from './utils/either.js' - -const UNKNOWN_SCHEMA = z.unknown() - -function parseRequestBody({ - body, - requestBodySchema, - path, -}: { - body: unknown - requestBodySchema?: RequestBodySchema - path: string -}): Either> { - if (!body) { - return success(body) - } - - if (!requestBodySchema) { - return success(body) - } - - const result = requestBodySchema.safeParse(body) - - if (!result.success) { - console.error({ - path, - body, - error: result.error, - }) - return failure(result.error) - } - - return success(body) -} - -function parseQueryParams({ - queryParams, - queryParamsSchema, - path, -}: { - queryParams: unknown - queryParamsSchema?: RequestQuerySchema - path: string -}): Either { - if (!queryParams) { - return success('') - } +import { parseRequestBody, tryToResolveJsonBody } from './utils/bodyUtils.js' +import { isFailure } from './utils/either.js' +import { buildWretchError } from './utils/errorUtils.js' +import { parseQueryParams } from './utils/queryUtils.js' - if (!queryParamsSchema) { - return success(`?${stringify(queryParams)}`) - } - - const result = queryParamsSchema.safeParse(queryParams) - - if (!result.success) { - console.error({ - path, - queryParams, - error: result.error, - }) - return failure(result.error) - } - - return success(`?${stringify(queryParams)}`) -} +export const UNKNOWN_SCHEMA = z.unknown() async function sendResourceChange< T extends WretchInstance, ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, RequestBodySchema extends z.Schema | undefined = undefined, RequestQuerySchema extends z.Schema | undefined = undefined, - IsNonJSONResponseExpected extends boolean = false, >( wretch: T, method: 'post' | 'put' | 'patch', params: ResourceChangeParams< RequestBodySchema, ResponseBody, - RequestQuerySchema, - IsNonJSONResponseExpected + IsNonJSONResponseExpected, + IsEmptyResponseExpected, + RequestQuerySchema >, -): Promise> { +): Promise> { const body = parseRequestBody({ body: params.body, requestBodySchema: params.requestBodySchema, @@ -154,16 +93,7 @@ async function sendResourceChange< return bodyParseResult.result }, - ) as Promise> -} - -function buildWretchError(message: string, response: WretchResponse): WretchError { - const error = new WretchError(message) - error.response = response - error.status = response.status - error.url = response.url - - return error + ) as Promise> } /* METHODS */ @@ -175,12 +105,18 @@ export async function sendGet< ResponseBody, RequestQuerySchema extends z.Schema | undefined = undefined, IsNonJSONResponseExpected extends boolean = false, + IsEmptyResponseExpected extends boolean = false, >( wretch: T, params: RequestQuerySchema extends z.Schema - ? QueryParams - : FreeQueryParams, -): Promise> { + ? QueryParams< + RequestQuerySchema, + ResponseBody, + IsNonJSONResponseExpected, + IsEmptyResponseExpected + > + : FreeQueryParams, +): Promise> { const queryParams = parseQueryParams({ queryParams: params.queryParams, queryParamsSchema: params.queryParamsSchema, @@ -227,7 +163,7 @@ export async function sendGet< } return bodyParseResult.result - }) as RequestResultType + }) as RequestResultType } /* POST */ @@ -238,15 +174,17 @@ export function sendPost< RequestBodySchema extends z.Schema | undefined = undefined, RequestQuerySchema extends z.Schema | undefined = undefined, IsNonJSONResponseExpected extends boolean = false, + IsEmptyResponseExpected extends boolean = false, >( wretch: T, params: ResourceChangeParams< RequestBodySchema, ResponseBody, - RequestQuerySchema, - IsNonJSONResponseExpected + IsNonJSONResponseExpected, + IsEmptyResponseExpected, + RequestQuerySchema >, -): Promise> { +): Promise> { return sendResourceChange(wretch, 'post', params) } @@ -258,15 +196,17 @@ export function sendPut< RequestBodySchema extends z.Schema | undefined = undefined, RequestQuerySchema extends z.Schema | undefined = undefined, IsNonJSONResponseExpected extends boolean = false, + IsEmptyResponseExpected extends boolean = false, >( wretch: T, params: ResourceChangeParams< RequestBodySchema, ResponseBody, - RequestQuerySchema, - IsNonJSONResponseExpected + IsNonJSONResponseExpected, + IsEmptyResponseExpected, + RequestQuerySchema >, -): Promise> { +): Promise> { return sendResourceChange(wretch, 'put', params) } @@ -278,15 +218,17 @@ export function sendPatch< RequestBodySchema extends z.Schema | undefined = undefined, RequestQuerySchema extends z.Schema | undefined = undefined, IsNonJSONResponseExpected extends boolean = false, + IsEmptyResponseExpected extends boolean = false, >( wretch: T, params: ResourceChangeParams< RequestBodySchema, ResponseBody, - RequestQuerySchema, - IsNonJSONResponseExpected + IsNonJSONResponseExpected, + IsEmptyResponseExpected, + RequestQuerySchema >, -): Promise> { +): Promise> { return sendResourceChange(wretch, 'patch', params) } @@ -297,12 +239,18 @@ export function sendDelete< ResponseBody, RequestQuerySchema extends z.Schema | undefined = undefined, IsNonJSONResponseExpected extends boolean = false, + IsEmptyResponseExpected extends boolean = true, >( wretch: T, params: RequestQuerySchema extends z.Schema - ? DeleteParams - : FreeDeleteParams, -): Promise> { + ? DeleteParams< + RequestQuerySchema, + ResponseBody, + IsNonJSONResponseExpected, + IsEmptyResponseExpected + > + : FreeDeleteParams, +): Promise> { const queryParams = parseQueryParams({ queryParams: params.queryParams, queryParamsSchema: params.queryParamsSchema, @@ -351,5 +299,5 @@ export function sendDelete< } return bodyParseResult.result - }) as Promise> + }) as Promise> } diff --git a/src/types.ts b/src/types.ts index 5156b73..73ae5c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,10 +3,14 @@ import type { ZodSchema, z } from 'zod' type FreeformRecord = Record -export type CommonRequestParams = { +export type CommonRequestParams< + ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, +> = { path: string responseBodySchema: ZodSchema - isEmptyResponseExpected?: boolean // 204 is considered a success. Default is "false" for GET operations and "true" for everything else + isEmptyResponseExpected?: IsEmptyResponseExpected // 204 is considered a success. Default is "false" for GET operations and "true" for everything else isNonJSONResponseExpected?: IsNonJSONResponseExpected // Do not throw an error if not receiving 'application/json' content-type. Default is "false" for GET operations and "true" for everything else } @@ -14,64 +18,98 @@ export type BodyRequestParams< RequestBodySchema extends z.ZodSchema, ResponseBody, IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, > = { body: z.input | undefined requestBodySchema: RequestBodySchema | undefined -} & CommonRequestParams +} & CommonRequestParams -export type FreeBodyRequestParams = { +export type FreeBodyRequestParams< + ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, +> = { body?: FreeformRecord requestBodySchema?: never -} & CommonRequestParams +} & CommonRequestParams export type QueryParams< RequestQuerySchema extends z.ZodSchema, ResponseBody, IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, > = { queryParams: z.input | undefined queryParamsSchema: RequestQuerySchema | undefined -} & CommonRequestParams +} & CommonRequestParams -export type FreeQueryParams = { +export type FreeQueryParams< + ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, +> = { queryParams?: FreeformRecord queryParamsSchema?: never -} & CommonRequestParams +} & CommonRequestParams export type DeleteParams< RequestQuerySchema extends z.ZodSchema, ResponseBody, IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, > = { queryParams: z.input | undefined queryParamsSchema: RequestQuerySchema | undefined -} & Omit, 'responseBodySchema'> & { +} & Omit< + CommonRequestParams, + 'responseBodySchema' +> & { responseBodySchema?: ZodSchema } -export type FreeDeleteParams = { +export type FreeDeleteParams< + ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, +> = { queryParams?: FreeformRecord queryParamsSchema?: never -} & Omit, 'responseBodySchema'> & { +} & Omit< + CommonRequestParams, + 'responseBodySchema' +> & { responseBodySchema?: ZodSchema } export type RequestResultType< ResponseBody, - isNonJSONResponseExpected extends boolean | undefined, -> = isNonJSONResponseExpected extends true ? WretchResponse | null : ResponseBody | null + isNonJSONResponseExpected extends boolean, + isEmptyResponseExpected extends boolean, +> = isEmptyResponseExpected extends true + ? isNonJSONResponseExpected extends true + ? WretchResponse | null + : ResponseBody | null + : isNonJSONResponseExpected extends true + ? WretchResponse + : ResponseBody export type ResourceChangeParams< RequestBody, ResponseBody, + IsNonJSONResponseExpected extends boolean, + IsEmptyResponseExpected extends boolean, RequestQuerySchema extends z.Schema | undefined = undefined, - IsNonJSONResponseExpected extends boolean = false, > = (RequestBody extends z.Schema - ? BodyRequestParams - : FreeBodyRequestParams) & + ? BodyRequestParams + : FreeBodyRequestParams) & (RequestQuerySchema extends z.Schema - ? QueryParams - : FreeQueryParams) + ? QueryParams< + RequestQuerySchema, + ResponseBody, + IsNonJSONResponseExpected, + IsEmptyResponseExpected + > + : FreeQueryParams) // We don't know which addons Wretch will have, and we don't really care, hence any // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/utils/bodyUtils.ts b/src/utils/bodyUtils.ts index e7b8208..9d83726 100644 --- a/src/utils/bodyUtils.ts +++ b/src/utils/bodyUtils.ts @@ -55,3 +55,34 @@ function parseResponseBody({ return success(response) } + +export function parseRequestBody({ + body, + requestBodySchema, + path, +}: { + body: unknown + requestBodySchema?: RequestBodySchema + path: string +}): Either> { + if (!body) { + return success(body) + } + + if (!requestBodySchema) { + return success(body) + } + + const result = requestBodySchema.safeParse(body) + + if (!result.success) { + console.error({ + path, + body, + error: result.error, + }) + return failure(result.error) + } + + return success(body) +} diff --git a/src/utils/errorUtils.ts b/src/utils/errorUtils.ts new file mode 100644 index 0000000..6f98f88 --- /dev/null +++ b/src/utils/errorUtils.ts @@ -0,0 +1,11 @@ +import type { WretchResponse } from 'wretch' +import { WretchError } from 'wretch/resolver' + +export function buildWretchError(message: string, response: WretchResponse): WretchError { + const error = new WretchError(message) + error.response = response + error.status = response.status + error.url = response.url + + return error +} diff --git a/src/utils/queryUtils.ts b/src/utils/queryUtils.ts new file mode 100644 index 0000000..d7d5a6b --- /dev/null +++ b/src/utils/queryUtils.ts @@ -0,0 +1,36 @@ +import { stringify } from 'fast-querystring' +import type { z } from 'zod' + +import type { Either } from './either.js' +import { failure, success } from './either.js' + +export function parseQueryParams({ + queryParams, + queryParamsSchema, + path, +}: { + queryParams: unknown + queryParamsSchema?: RequestQuerySchema + path: string +}): Either { + if (!queryParams) { + return success('') + } + + if (!queryParamsSchema) { + return success(`?${stringify(queryParams)}`) + } + + const result = queryParamsSchema.safeParse(queryParams) + + if (!result.success) { + console.error({ + path, + queryParams, + error: result.error, + }) + return failure(result.error) + } + + return success(`?${stringify(queryParams)}`) +}