Files
plunk/apps/web/src/lib/network.ts
T

120 lines
3.7 KiB
TypeScript

import type {infer as ZodInfer, ZodSchema} from 'zod';
import {API_URI} from './constants';
interface Json {
[x: string]: string | number | boolean | Date | Json | JsonArray;
}
type JsonArray = (string | number | boolean | Date | Json | JsonArray)[];
interface TypedSchema extends ZodSchema {
_type: unknown;
}
interface ApiResponse {
message?: string;
error?: {
message?: string;
code?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
export class network {
public static async fetch<T, Schema extends TypedSchema | void = void>(
method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH',
path: string,
body?: Schema extends TypedSchema ? ZodInfer<Schema> : never,
): Promise<T> {
const url = path.startsWith('http') ? path : API_URI + path;
// Get active project ID from localStorage
const activeProjectId = typeof window !== 'undefined' ? localStorage.getItem('activeProjectId') : null;
const headers: Record<string, string> = {};
if (body) {
headers['Content-Type'] = 'application/json';
}
if (activeProjectId) {
headers['X-Project-Id'] = activeProjectId;
}
const response = await fetch(url, {
method,
body: body && JSON.stringify(body),
headers,
credentials: 'include',
});
// Handle 204 No Content responses (no body to parse)
if (response.status === 204) {
return {} as T;
}
const res = (await response.json()) as ApiResponse;
if (response.status >= 400) {
// Check if this is an email verification required error
if (res.error?.code === 'EMAIL_VERIFICATION_REQUIRED') {
// Redirect to verification page
if (typeof window !== 'undefined' && !window.location.href.includes('/auth/verify-email')) {
window.location.href = '/auth/verify-email';
}
throw new Error(res.error.message ?? 'Please verify your email address to continue');
}
// Extract error message from standardized error response or fall back to direct message property
const errorMessage = res.error?.message ?? res.message ?? 'Something went wrong!';
throw new Error(errorMessage);
}
return res as T;
}
/**
* Upload file using FormData (multipart/form-data)
* Used for file uploads where Content-Type must be set by browser
*/
public static async upload<T>(method: 'POST' | 'PUT' | 'PATCH', path: string, formData: FormData): Promise<T> {
const url = path.startsWith('http') ? path : API_URI + path;
// Get active project ID from localStorage
const activeProjectId = typeof window !== 'undefined' ? localStorage.getItem('activeProjectId') : null;
const headers: Record<string, string> = {};
// DO NOT set Content-Type - browser will set it automatically with boundary
if (activeProjectId) {
headers['X-Project-Id'] = activeProjectId;
}
const response = await fetch(url, {
method,
body: formData,
headers,
credentials: 'include',
});
const res = (await response.json()) as ApiResponse;
if (response.status >= 400) {
// Check if this is an email verification required error
if (res.error?.code === 'EMAIL_VERIFICATION_REQUIRED') {
// Redirect to verification page
if (typeof window !== 'undefined') {
window.location.href = '/auth/verify-email';
}
throw new Error(res.error.message ?? 'Please verify your email address to continue');
}
// Extract error message from standardized error response or fall back to direct message property
const errorMessage = res.error?.message ?? res.message ?? 'Something went wrong!';
throw new Error(errorMessage);
}
return res as T;
}
}