/* eslint-disable @typescript-eslint/no-explicit-any */ import { constant, flow } from "@effect-ts-app/boilerplate-prelude/Function" import type { ReqRes, RequestSchemed } from "@effect-ts-app/boilerplate-prelude/schema" import { StringId } from "@effect-ts-app/boilerplate-prelude/schema" import * as H from "@effect-ts-app/core/http/http-client" import { Path } from "path-parser" import qs from "query-string" import { getConfig } from "./config.js" export type FetchError = H.HttpError export class ResponseError { public readonly _tag = "ResponseError" constructor(public readonly error: unknown) {} } export function fetchApi(method: H.Method, path: string, body?: unknown) { const request = H.request(method, "JSON", "JSON") return getConfig(({ apiUrl, headers }) => H.withHeaders({ "request-id": headers.flatMap(_ => _.get("request-id")).value ?? StringId.make(), ...headers.map(_ => Object.fromEntries(_)).value })(request(`${apiUrl}${path}`, body)) .map(x => ({ ...x, body: x.body.value ?? null })) ) } export function fetchApi2S( encodeRequest: (a: RequestA) => RequestE, decodeResponse: (u: unknown) => Effect ) { const decodeRes = (u: unknown) => decodeResponse(u).mapError(err => new ResponseError(err)) return (method: H.Method, path: Path) => (req: RequestA) => fetchApi( method, method === "DELETE" ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument makePathWithQuery(path, req as any) : makePathWithBody(path, req as any), encodeRequest(req) ) .flatMap(mapResponseM(decodeRes)) .map(i => ({ ...i, // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion body: i.body as ResponseA })) } export function fetchApi3S({ Request, Response }: { // eslint-disable-next-line @typescript-eslint/ban-types Request: RequestSchemed // eslint-disable-next-line @typescript-eslint/ban-types Response: ReqRes }) { const encodeRequest = Request.Encoder const decodeResponse = Parser.for(Response)["|>"](condemnCustom) return fetchApi2S(encodeRequest, decodeResponse)( Request.method, new Path(Request.path) ) } export function fetchApi3SE({ Request, Response }: { // eslint-disable-next-line @typescript-eslint/ban-types Request: RequestSchemed // eslint-disable-next-line @typescript-eslint/ban-types Response: ReqRes }) { const encodeRequest = Request.Encoder const encodeResponse = Encoder.for(Response) const decodeResponse = flow(Parser.for(Response)["|>"](condemnCustom), x => x.map(encodeResponse)) return fetchApi2S(encodeRequest, decodeResponse)( Request.method, new Path(Request.path) ) } export function makePathWithQuery( path: Path, pars: Record< string, | string | number | boolean | readonly string[] | readonly number[] | readonly boolean[] | null > ) { return ( path.build(pars, { ignoreSearch: true, ignoreConstraints: true }) + (Object.keys(pars).length ? "?" + qs.stringify(pars) : "") ) } export function makePathWithBody( path: Path, pars: Record< string, | string | number | boolean | readonly string[] | readonly number[] | readonly boolean[] | null > ) { return path.build(pars, { ignoreSearch: true, ignoreConstraints: true }) } export function mapResponse(map: (t: T) => A) { return (r: FetchResponse): FetchResponse => { return { ...r, body: map(r.body) } } } export function mapResponseM(map: (t: T) => Effect) { return (r: FetchResponse): Effect> => { return Effect.struct({ body: map(r.body), headers: Effect(r.headers), status: Effect(r.status) }) } } export type FetchResponse = { body: T; headers: H.Headers; status: number } export const EmptyResponse = Object.freeze({ body: null, headers: {}, status: 404 }) export const EmptyResponseM = Effect(EmptyResponse) const EmptyResponseMThunk_ = constant(EmptyResponseM) export function EmptyResponseMThunk(): Effect< unknown, never, Readonly<{ body: null | T // eslint-disable-next-line @typescript-eslint/ban-types headers: {} status: 404 }> > { return EmptyResponseMThunk_() } export function getBody(eff: Effect>) { return eff.flatMap(r => r.body === null ? Effect.die("Not found") : Effect(r.body)) }