Skip to content

Commit

Permalink
Merge pull request #18 from deco-cx/feat/clients
Browse files Browse the repository at this point in the history
feat: use the new http client implementation
  • Loading branch information
tlgimenes authored Aug 31, 2023
2 parents 7597e37 + 60c0ce8 commit 878f301
Show file tree
Hide file tree
Showing 42 changed files with 813 additions and 732 deletions.
6 changes: 3 additions & 3 deletions import_map.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"imports": {
"$live/": "https://denopkg.com/deco-cx/deco@1.32.0/",
"$live/": "https://denopkg.com/deco-cx/deco@1.32.1/",
"$fresh/": "https://denopkg.com/denoland/fresh@1.4.2/",
"preact": "https://esm.sh/preact@10.15.1",
"preact/": "https://esm.sh/preact@10.15.1/",
Expand All @@ -9,7 +9,7 @@
"@preact/signals-core": "https://esm.sh/@preact/signals-core@1.3.0",
"std/": "https://deno.land/std@0.190.0/",
"partytown/": "https://deno.land/x/partytown@0.3.4/",
"deco-sites/std/": "https://denopkg.com/deco-sites/std@1.20.15/",
"deco/": "https://denopkg.com/deco-cx/deco@1.32.0/"
"deco-sites/std/": "https://denopkg.com/deco-sites/std@1.21.3/",
"deco/": "https://denopkg.com/deco-cx/deco@1.32.1/"
}
}
145 changes: 145 additions & 0 deletions utils/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,151 @@
import { RequestInit } from "$live/runtime/fetch/mod.ts";
import { fetchSafe } from "./fetch.ts";

const HTTP_VERBS = new Set(
[
"GET",
"PUT",
"POST",
"DELETE",
"PATCH",
"HEAD",
] as const,
);

export class HttpError extends Error {
constructor(public status: number, message?: string, options?: ErrorOptions) {
super(message, options);
this.name = `HttpError ${status}`;
}
}

export interface TypedRequestInit<T> extends Omit<RequestInit, "body"> {
body: T;
}

export interface TypedResponse<T> extends Response {
json: () => Promise<T>;
}

type HttpVerb = typeof HTTP_VERBS extends Set<infer Verb> ? Verb : never;

type URLPatternParam = string | number;

type URLPatternParams<URL extends string> = URL extends
`/:${infer param}/${infer rest}`
? { [key in param]: URLPatternParam } & URLPatternParams<`/${rest}`>
: URL extends `/:${infer param}?` ? { [key in param]?: URLPatternParam }
: URL extends `/:${infer param}` ? { [key in param]: URLPatternParam }
: URL extends `/*?` ? { "*"?: URLPatternParam | URLPatternParam[] }
: URL extends `/*` ? { "*": URLPatternParam | URLPatternParam[] }
: URL extends `/*${infer param}?`
? { [key in param]: URLPatternParam | URLPatternParam[] }
: URL extends `/*${infer param}`
? { [key in param]: URLPatternParam | URLPatternParam[] }
: URL extends `/${string}/${infer rest}` ? URLPatternParams<`/${rest}`>
: {};

type ClientOf<T> = {
[key in (keyof T) & `${HttpVerb} /${string}`]: key extends
`${HttpVerb} /${infer path}` ? T[key] extends {
response?: infer ResBody;
body: infer ReqBody;
searchParams?: infer Params;
} ? (
params: URLPatternParams<`/${path}`> & Params,
init: TypedRequestInit<ReqBody>,
) => Promise<TypedResponse<ResBody>>
: T[key] extends {
response?: infer ResBody;
searchParams?: infer Params;
} ? (
params: URLPatternParams<`/${path}`> & Params,
init?: Omit<RequestInit, "body">,
) => Promise<TypedResponse<ResBody>>
: never
: never;
};

export interface HttpClientOptions {
base: string;
headers?: Headers;
fetcher?: typeof fetch;
}

export const createHttpClient = <T>({
base,
headers: defaultHeaders,
fetcher = fetchSafe,
}: HttpClientOptions): ClientOf<T> =>
new Proxy({} as ClientOf<T>, {
get: (_target, prop) => {
if (typeof prop !== "string") {
throw new TypeError(`HttpClient: Uknown path ${typeof prop}`);
}

const [method, path] = prop.split(" ");

// @ts-expect-error if not inside, throws
if (!HTTP_VERBS.has(method)) {
throw new TypeError(`HttpClient: Verb ${method} is not allowed`);
}

return (
params: Record<string, string | number | string[] | number[]>,
init: RequestInit,
) => {
const mapped = new Map(Object.entries(params));

const compiled = path
.split("/")
.flatMap((segment) => {
const isTemplate = segment.at(0) === ":" || segment.at(0) === "*";
const isRequred = segment.at(-1) !== "?";

if (!isTemplate) {
return segment;
}

const name = segment.slice(1, !isRequred ? -1 : undefined);
const param = mapped.get(name);
mapped.delete(name);

if (param === undefined && isRequred) {
throw new TypeError(`HttpClient: Missing ${name} at ${path}`);
}

return param;
})
.filter((s) => s !== undefined)
.join("/");

const url = new URL(compiled, base);
mapped.forEach((value, key) => {
const arrayed = Array.isArray(value) ? value : [value];
arrayed.forEach((item) => url.searchParams.append(key, `${item}`));
});

const shouldStringifyBody = init.body != null &&
typeof init.body !== "string" &&
!(init.body instanceof ReadableStream) &&
!(init.body instanceof FormData) &&
!(init.body instanceof URLSearchParams) &&
!(init.body instanceof Blob) &&
!(init.body instanceof ArrayBuffer);

const headers = new Headers(init.headers);
defaultHeaders?.forEach((value, key) => headers.set(key, value));

const body = shouldStringifyBody
? JSON.stringify(init.body)
: undefined;

return fetcher(url, {
...init,
headers,
method,
body,
});
};
},
});
27 changes: 11 additions & 16 deletions vtex/actions/analytics/sendEvent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Intelligent Search analytics integration
import { fetchSafe } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import { getOrSetISCookie } from "../../utils/intelligentSearch.ts";
import { paths } from "../../utils/paths.ts";

export type Props =
| {
Expand Down Expand Up @@ -32,23 +30,20 @@ const action = async (
req: Request,
ctx: AppContext,
): Promise<null> => {
const { sp } = ctx;
const { anonymous, session } = getOrSetISCookie(req, ctx.response.headers);

await fetchSafe(
paths(ctx)["event-api"].v1.account.event,
{
method: "POST",
body: JSON.stringify({
...props,
agent: "deco-sites/std",
anonymous,
session,
}),
headers: {
"content-type": "application/json",
},
await sp["POST /event-api/v1/:account/event"]({ account: ctx.account }, {
body: {
...props,
anonymous,
session,
agent: "deco-sites/apps",
},
);
headers: {
"content-type": "application/json",
},
});

return null;
};
Expand Down
52 changes: 22 additions & 30 deletions vtex/actions/cart/addItems.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { fetchSafe } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import { proxySetCookie } from "../../utils/cookies.ts";
import { parseCookie } from "../../utils/orderForm.ts";
import { paths } from "../../utils/paths.ts";
import type { OrderForm } from "../../utils/types.ts";

export interface Item {
Expand All @@ -26,41 +24,35 @@ const action = async (
req: Request,
ctx: AppContext,
): Promise<OrderForm> => {
const { vcs } = ctx;
const {
orderItems,
allowedOutdatedData = ["paymentData"],
} = props;
const { orderFormId, cookie } = parseCookie(req.headers);
const url = new URL(
`${
paths(ctx).api.checkout.pub.orderForm
.orderFormId(orderFormId)
.items
}`,
);

if (allowedOutdatedData) {
for (const it of allowedOutdatedData) {
url.searchParams.append("allowedOutdatedData", it);
}
try {
const response = await vcs
["POST /api/checkout/pub/orderForm/:orderFormId/items"]({
orderFormId,
allowedOutdatedData,
}, {
body: { orderItems },
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
},
});

proxySetCookie(response.headers, ctx.response.headers, req.url);

return response.json();
} catch (error) {
console.error(error);

throw error;
}

const response = await fetchSafe(
url,
{
method: "POST",
body: JSON.stringify({ orderItems }),
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
},
},
);

proxySetCookie(response.headers, ctx.response.headers, req.url);

return response.json();
};

export default action;
24 changes: 6 additions & 18 deletions vtex/actions/cart/getInstallment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { fetchSafe } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import { proxySetCookie } from "../../utils/cookies.ts";
import { parseCookie } from "../../utils/orderForm.ts";
import { paths } from "../../utils/paths.ts";
import type { OrderForm } from "../../utils/types.ts";

export interface Props {
Expand All @@ -17,25 +15,15 @@ const action = async (
req: Request,
ctx: AppContext,
): Promise<OrderForm> => {
const { vcs } = ctx;
const { paymentSystem } = props;
const { orderFormId, cookie } = parseCookie(req.headers);
const url = new URL(
paths(ctx).api.checkout.pub.orderForm
.orderFormId(orderFormId)
.installments,
);

url.searchParams.set("paymentSystem", `${paymentSystem}`);

const response = await fetchSafe(
url,
{
headers: {
accept: "application/json",
cookie,
},
},
);
const response = await vcs
["GET /api/checkout/pub/orderForm/:orderFormId/installments"](
{ orderFormId, paymentSystem },
{ headers: { accept: "application/json", cookie } },
);

proxySetCookie(response.headers, ctx.response.headers, req.url);

Expand Down
29 changes: 12 additions & 17 deletions vtex/actions/cart/removeItemAttachment.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { fetchSafe } from "../../../utils/fetch.ts";
import { AppContext } from "../../mod.ts";
import { proxySetCookie } from "../../utils/cookies.ts";
import { parseCookie } from "../../utils/orderForm.ts";
import { paths } from "../../utils/paths.ts";
import type { OrderForm } from "../../utils/types.ts";

export interface Props {
Expand Down Expand Up @@ -38,6 +36,7 @@ const action = async (
req: Request,
ctx: AppContext,
): Promise<OrderForm> => {
const { vcs } = ctx;
const {
index,
attachment,
Expand All @@ -46,23 +45,19 @@ const action = async (
expectedOrderFormSections = DEFAULT_EXPECTED_SECTIONS,
} = props;
const { orderFormId, cookie } = parseCookie(req.headers);
const url = new URL(
paths(ctx).api.checkout.pub.orderForm.orderFormId(orderFormId).items
.index(index).attachments.attachment(attachment),
);

const response = await fetchSafe(
url,
{
method: "DELETE",
body: JSON.stringify({ content, noSplitItem, expectedOrderFormSections }),
headers: {
accept: "application/json",
"content-type": "application/json",
cookie,
const response = await vcs
["DELETE /api/checkout/pub/orderForm/:orderFormId/items/:index/attachments/:attachment"](
{ orderFormId, attachment, index },
{
body: { content, noSplitItem, expectedOrderFormSections },
headers: {
accept: "application/json",
"content-type": "application/json",
cookie,
},
},
},
);
);

proxySetCookie(response.headers, ctx.response.headers, req.url);

Expand Down
Loading

0 comments on commit 878f301

Please sign in to comment.