mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
feat(backend): add standalone data plane
Replace the Supabase application data plane with the Node/PostgreSQL API, migrate mobile, admin, web, and MCP callers, add controlled cutover tooling, and define hardened Dokploy infrastructure.
This commit is contained in:
1 parent
01bb8225ae
commit
006761d3b0
294 files changed
+16259
-6138
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import { authClient } from "./authClient";
|
||||
|
||||
const isTestEnv = process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined;
|
||||
const configuredApiUrl = process.env.EXPO_PUBLIC_API_URL;
|
||||
|
||||
if (!isTestEnv && !configuredApiUrl) {
|
||||
throw new Error("Missing required environment variable: EXPO_PUBLIC_API_URL. App cannot start.");
|
||||
}
|
||||
|
||||
export const apiUrl = (configuredApiUrl || "http://127.0.0.1:3003").replace(/\/$/, "");
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
readonly details?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
type ApiRequestOptions = Omit<RequestInit, "body"> & {
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
async function parseResponse(response: Response): Promise<unknown> {
|
||||
if (response.status === 204) return undefined;
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) return undefined;
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: ApiRequestOptions = {}): Promise<T> {
|
||||
const { data, error } = await authClient.auth.getSession();
|
||||
const accessToken = data.session?.access_token;
|
||||
|
||||
if (error || !accessToken) {
|
||||
throw new ApiError("An authenticated session is required", 401, error);
|
||||
}
|
||||
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set("Accept", "application/json");
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
|
||||
let body: BodyInit | undefined;
|
||||
if (options.body !== undefined) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}${path.startsWith("/") ? path : `/${path}`}`, {
|
||||
...options,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
const responseBody = await parseResponse(response);
|
||||
|
||||
if (!response.ok) {
|
||||
let message = `API request failed with status ${response.status}`;
|
||||
if (typeof responseBody === "object" && responseBody !== null && "error" in responseBody) {
|
||||
const apiError = (responseBody as { error: unknown }).error;
|
||||
if (typeof apiError === "string") {
|
||||
message = apiError;
|
||||
} else if (
|
||||
typeof apiError === "object" &&
|
||||
apiError !== null &&
|
||||
"message" in apiError &&
|
||||
typeof (apiError as { message: unknown }).message === "string"
|
||||
) {
|
||||
message = (apiError as { message: string }).message;
|
||||
}
|
||||
}
|
||||
throw new ApiError(message, response.status, responseBody);
|
||||
}
|
||||
|
||||
return responseBody as T;
|
||||
}
|
||||
|
||||
export async function authenticatedFetch(path: string, init: RequestInit = {}): Promise<Response> {
|
||||
const { data, error } = await authClient.auth.getSession();
|
||||
const accessToken = data.session?.access_token;
|
||||
if (error || !accessToken) throw new ApiError("An authenticated session is required", 401, error);
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
return fetch(`${apiUrl}${path.startsWith("/") ? path : `/${path}`}`, { ...init, headers });
|
||||
}
|
||||
|
||||
export const apiClient = {
|
||||
get: <T>(path: string) => apiRequest<T>(path),
|
||||
post: <T>(path: string, body?: unknown) => apiRequest<T>(path, { method: "POST", body }),
|
||||
put: <T>(path: string, body?: unknown) => apiRequest<T>(path, { method: "PUT", body }),
|
||||
patch: <T>(path: string, body?: unknown) => apiRequest<T>(path, { method: "PATCH", body }),
|
||||
delete: <T>(path: string) => apiRequest<T>(path, { method: "DELETE" }),
|
||||
};
|
||||
Reference in new issue
Block a user