Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .husky/pre-commit

This file was deleted.

2 changes: 0 additions & 2 deletions .husky/pre-push

This file was deleted.

3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["carb", "CATEG", "headlessui", "tiktok"]
}
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
95 changes: 49 additions & 46 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type ApiConfig = RequestInit & {
timeout?: number;
};

type ExtendedRequestInit = RequestInit & { includeHeaders?: boolean };

export class Api {
private baseUrl: string;
private defaultOptions: RequestInit;
Expand Down Expand Up @@ -55,7 +57,7 @@ export class Api {
method: string,
endpoint: string,
response: Response,
duration: number,
duration: number
) {
if (!this.debugMode) return;

Expand All @@ -65,15 +67,15 @@ export class Api {
headers: Object.fromEntries(response.headers.entries()),
statusText: response.statusText,
body: await response.clone().json(),
},
}
);
}

private logError(
method: string,
endpoint: string,
error: Error,
duration: number,
duration: number
) {
if (!this.debugMode) return;

Expand All @@ -88,39 +90,39 @@ export class Api {
endpoint: string,
options: RequestInit,
schema: SchemaType,
includeHeaders: true,
includeHeaders: true
): Promise<{ data: ZodInfer<SchemaType>; headers: Headers }>;

async request<SchemaType extends ZodSchema>(
endpoint: string,
options: RequestInit,
schema: SchemaType,
includeHeaders?: false,
includeHeaders?: false
): Promise<ZodInfer<SchemaType>>;

async request(
endpoint: string,
options: RequestInit,
includeHeaders: true,
includeHeaders: true
): Promise<{ data: unknown; headers: Headers }>;

async request<DataType extends object>(
endpoint: string,
options: RequestInit,
includeHeaders: true,
includeHeaders: true
): Promise<{ data: DataType; headers: Headers }>;

async request(
endpoint: string,
options?: RequestInit,
includeHeaders?: boolean,
includeHeaders?: boolean
): Promise<unknown>;

async request<SchemaType extends ZodSchema>(
endpoint: string,
options: RequestInit = {},
schemaOrIncludeHeaders?: SchemaType | boolean,
includeHeadersFlag?: boolean,
includeHeadersFlag?: boolean
): Promise<unknown> {
const url = new URL(endpoint, this.baseUrl);
const controller = new AbortController();
Expand Down Expand Up @@ -170,7 +172,7 @@ export class Api {
throw new ApiError(
`Non-JSON response from ${url.href}`,
500,
"Invalid Content-Type",
"Invalid Content-Type"
);
}

Expand All @@ -183,7 +185,7 @@ export class Api {
if (!parseResult.success) {
throw new ValidationError(
`Response validation failed for ${url.href}`,
parseResult.error.issues,
parseResult.error.issues
);
}
parsedData = parseResult.data;
Expand Down Expand Up @@ -212,7 +214,7 @@ export class Api {
"Unknown error occurred",
0,
"UNKNOWN_ERROR",
String(error),
String(error)
);
} finally {
controller.abort();
Expand All @@ -223,25 +225,25 @@ export class Api {

async get<T>(
endpoint: string,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async get<T>(
endpoint: string,
schema: ZodSchema<T>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async get<T>(
endpoint: string,
schema: ZodSchema<T>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async get<T>(
endpoint: string,
arg1?: ZodSchema<T> | RequestInit,
arg2?: RequestInit,
arg2?: RequestInit
): Promise<T | { data: T; headers: Headers }> {
let options: RequestInit = {};
let includeHeaders = false;
Expand All @@ -264,59 +266,57 @@ export class Api {
async post<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async post<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async post<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
schema: ZodSchema<T>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async post<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
schema: ZodSchema<T>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async post<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
arg1?: ZodSchema<T> | RequestInit,
arg2?: RequestInit,
body: BodyInit | Record<string, unknown> | FormData,
arg1?: ZodSchema<T> | ExtendedRequestInit,
arg2?: ExtendedRequestInit
): Promise<T | { data: T; headers: Headers }> {
let schema: ZodSchema<T> | undefined;
let options: RequestInit = {};
let options: ExtendedRequestInit = {};
let includeHeaders = false;

if (arg1 instanceof ZodSchema) {
if (this.isZodSchema<T>(arg1)) {
schema = arg1;
options = arg2 ?? {};
includeHeaders =
(arg2 as { includeHeaders?: boolean })?.includeHeaders ?? false;
} else {
options = arg1 ?? {};
includeHeaders =
(arg1 as { includeHeaders?: boolean })?.includeHeaders ?? false;
}

includeHeaders = options.includeHeaders ?? false;

const headers = new Headers(
this.cleanHeaders({
...this.defaultOptions.headers,
...options.headers,
}),
})
);

const isJson = !(body instanceof FormData) && !headers.has("Content-Type");
const bodyPayload = JSON.stringify(body);
const isFormData = body instanceof FormData;
const isJson = !isFormData && !headers.has("Content-Type");

if (isJson) {
headers.set("Content-Type", "application/json");
Expand All @@ -325,46 +325,49 @@ export class Api {
const mergedOptions: RequestInit = {
...options,
method: "POST",
body: bodyPayload,
body: isJson ? JSON.stringify(body) : (body as BodyInit),
headers,
};

return this.request(endpoint, mergedOptions, includeHeaders) as Promise<
T | { data: T; headers: Headers }
>;
}
isZodSchema<T>(arg: any): arg is ZodSchema<T> {
return typeof arg?.safeParse === "function";
}

async put<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async put<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async put<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
schema: ZodSchema<T>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async put<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
schema: ZodSchema<T>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async put<T>(
endpoint: string,
body: BodyInit | Record<string, unknown>,
arg1?: ZodSchema<T> | RequestInit,
arg2?: RequestInit,
arg2?: RequestInit
): Promise<T | { data: T; headers: Headers }> {
let schema: ZodSchema<T> | undefined;
let options: RequestInit = {};
Expand All @@ -385,7 +388,7 @@ export class Api {
this.cleanHeaders({
...this.defaultOptions.headers,
...options.headers,
}),
})
);

const isJson = !(body instanceof FormData) && !headers.has("Content-Type");
Expand All @@ -411,25 +414,25 @@ export class Api {

async delete<T>(
endpoint: string,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async delete<T>(
endpoint: string,
schema: ZodSchema<T>,
options?: RequestInit,
options?: RequestInit
): Promise<T>;

async delete<T>(
endpoint: string,
schema: ZodSchema<T>,
options: RequestInit & { includeHeaders: true },
options: RequestInit & { includeHeaders: true }
): Promise<{ data: T; headers: Headers }>;

async delete<T>(
endpoint: string,
arg1?: ZodSchema<T> | RequestInit,
arg2?: RequestInit,
arg2?: RequestInit
): Promise<T | { data: T; headers: Headers }> {
let schema: ZodSchema<T> | undefined;
let options: RequestInit = {};
Expand All @@ -449,20 +452,20 @@ export class Api {
return this.request(
endpoint,
{ ...options, method: "DELETE" },
includeHeaders,
includeHeaders
) as Promise<T | { data: T; headers: Headers }>;
}

private cleanHeaders(
headers: Record<string, unknown>,
headers: Record<string, unknown>
): Record<string, string> {
return Object.fromEntries(
Object.entries(headers)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => [
key,
String(value as NonNullable<typeof value>),
]),
])
);
}
}
Expand Down
Loading
Loading