File size: 4,159 Bytes
3bad34d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710ace6
3bad34d
 
 
710ace6
 
 
 
3bad34d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
type SupabaseEnv = {
    url: string;
    anonKey: string;
    serviceRoleKey?: string;
};

function getSupabaseEnv(): SupabaseEnv {
    const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
    const anonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY || "";
    const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY || "";

    if (!url || !anonKey) {
        throw new Error("Supabase env vars missing: NEXT_PUBLIC_SUPABASE_URL and/or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY");
    }

    return { url, anonKey, serviceRoleKey: serviceRoleKey || undefined };
}

export type SupabaseRestError = {
    message: string;
    details?: string;
    hint?: string;
    code?: string;
};

export async function supabaseRest<T>(
    path: string,
    init: RequestInit & {
        query?: Record<string, string | number | boolean | undefined | null>;
        preferReturn?: "representation" | "minimal";
        acceptObject?: boolean;
        count?: "exact" | "planned" | "estimated";
        requireServiceRole?: boolean;
    } = {}
): Promise<{ data: T | null; error: SupabaseRestError | null; status: number; count: number | null }> {
    const { url, anonKey, serviceRoleKey } = getSupabaseEnv();
    if (init.requireServiceRole && !serviceRoleKey) {
        throw new Error("Supabase env var missing: SUPABASE_SERVICE_ROLE_KEY (required for this operation)");
    }
    const apiKey = init.requireServiceRole ? (serviceRoleKey as string) : anonKey;

    const endpoint = new URL(path.startsWith("/") ? path : `/${path}`, url);
    if (init.query) {
        for (const [k, v] of Object.entries(init.query)) {
            if (v === undefined || v === null) continue;
            endpoint.searchParams.set(k, String(v));
        }
    }

    const headers = new Headers(init.headers);
    headers.set("apikey", apiKey);
    headers.set("Authorization", `Bearer ${apiKey}`);

    if (!headers.has("Content-Type") && init.body) {
        headers.set("Content-Type", "application/json");
    }

    if (init.preferReturn) {
        const existing = headers.get("Prefer");
        const next = `return=${init.preferReturn}`;
        headers.set("Prefer", existing ? `${existing}, ${next}` : next);
    }

    if (init.count) {
        const existing = headers.get("Prefer");
        const next = `count=${init.count}`;
        headers.set("Prefer", existing ? `${existing}, ${next}` : next);
    }

    if (init.acceptObject) {
        headers.set("Accept", "application/vnd.pgrst.object+json");
    }

    const res = await fetch(endpoint.toString(), {
        ...init,
        headers,
    });

    const status = res.status;

    const contentRange = res.headers.get("content-range") || res.headers.get("Content-Range");
    let count: number | null = null;
    if (contentRange) {
        const slash = contentRange.lastIndexOf("/");
        if (slash !== -1) {
            const total = Number(contentRange.slice(slash + 1));
            if (Number.isFinite(total)) count = total;
        }
    }

    const text = await res.text();

    if (!res.ok) {
        let err: SupabaseRestError = { message: `Supabase REST error: ${status}` };
        try {
            const parsed = JSON.parse(text);
            if (parsed && typeof parsed === "object") {
                err = {
                    message: typeof (parsed as any).message === "string" ? (parsed as any).message : err.message,
                    details: typeof (parsed as any).details === "string" ? (parsed as any).details : undefined,
                    hint: typeof (parsed as any).hint === "string" ? (parsed as any).hint : undefined,
                    code: typeof (parsed as any).code === "string" ? (parsed as any).code : undefined,
                };
            }
        } catch {
            if (text) err = { message: text };
        }
        return { data: null, error: err, status, count };
    }

    if (!text) {
        return { data: null, error: null, status, count };
    }

    try {
        return { data: JSON.parse(text) as T, error: null, status, count };
    } catch {
        return { data: null, error: null, status, count };
    }
}