1 | import { createClient, SupabaseClient } from "@supabase/[email protected]" |
2 |
|
3 | |
4 | * @param on_conflict Comma-separated UNIQUE column(s) to specify how duplicate |
5 | * rows are determined. Two rows are duplicates if all the onConflict columns are equal. |
6 | * |
7 | * @param ignore_duplicates If true, duplicate rows are ignored. If false, duplicate |
8 | * rows are merged with existing rows. |
9 | * |
10 | * @param token Supabase `access_token` and `refresh_token`. `expires_at` (optional) is a UNIX |
11 | * timestamp in seconds. |
12 | * |
13 | * @param count Count algorithm to use to count rows in the table or view. |
14 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the hood. |
15 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres statistics under the hood. |
16 | * `"estimated"`: Uses exact count for low numbers and planned count for high numbers. |
17 | */ |
18 | type Supabase = { |
19 | url: string; |
20 | key: string; |
21 | }; |
22 | export async function main( |
23 | auth: Supabase, |
24 | table: string, |
25 | values: any, |
26 | on_conflict?: string, |
27 | ignore_duplicates: boolean = true, |
28 | return_updated: boolean = false, |
29 | token?: { |
30 | access: string; |
31 | refresh: string; |
32 | expires_at?: number; |
33 | }, |
34 | count?: "exact" | "planned" | "estimated", |
35 | ) { |
36 | return await refreshAndRetryIfExpired(auth, token, async (client) => { |
37 | let query: any = client |
38 | .from(table) |
39 | .upsert( |
40 | values, |
41 | removeObjectEmptyFields({ |
42 | onConflict: on_conflict, |
43 | ignoreDuplicates: ignore_duplicates, |
44 | count, |
45 | }), |
46 | ); |
47 | if (return_updated) { |
48 | query = query.select(); |
49 | } |
50 |
|
51 | return query; |
52 | }); |
53 | } |
54 |
|
55 | function removeObjectEmptyFields( |
56 | object?: Record<string, any>, |
57 | removeEmptyArraysAndObjects = true, |
58 | createNewObject = true, |
59 | ) { |
60 | if (!object || typeof object !== "object") return {} |
61 | const obj = createNewObject ? { ...object } : object |
62 | const emptyValues = [undefined, null, ""] |
63 | for (const key in obj) { |
64 | const value = obj[key] |
65 | if (emptyValues.includes(value)) { |
66 | delete obj[key] |
67 | } else if (typeof value === "object") { |
68 | if (Object.keys(value).length) { |
69 | obj[key] = removeObjectEmptyFields(value, removeEmptyArraysAndObjects, false) |
70 | } |
71 | if (!Object.keys(value).length && removeEmptyArraysAndObjects) { |
72 | delete obj[key] |
73 | } |
74 | } |
75 | } |
76 | return obj |
77 | } |
78 |
|
79 | async function refreshAndRetryIfExpired( |
80 | auth: { url: string; key: string }, |
81 | token: { access: string; refresh: string; expires_at?: number } | undefined, |
82 | fn: (client: SupabaseClient) => Promise<{ data: any; error?: any }>, |
83 | ): Promise<{ data: any; error?: any; token?: { access: string; refresh: string; expires_at?: number } }> { |
84 | const makeClient = async (autoRefreshToken: boolean) => { |
85 | const client = createClient(auth.url, auth.key, { |
86 | auth: { autoRefreshToken, persistSession: false }, |
87 | }) |
88 | if (token) { |
89 | await client.auth.setSession({ access_token: token.access, refresh_token: token.refresh }) |
90 | } |
91 | return client |
92 | } |
93 | try { |
94 | let result = await fn(await makeClient(false)) |
95 | if (result?.error?.code === "PGRST301" && token) { |
96 | const client = await makeClient(true) |
97 | result = await fn(client) |
98 | const { data } = await client.auth.getSession() |
99 | if (data?.session) { |
100 | token = { |
101 | access: data.session.access_token, |
102 | refresh: data.session.refresh_token, |
103 | expires_at: data.session.expires_at, |
104 | } |
105 | } |
106 | } |
107 | return { ...result, token } |
108 | } catch (error) { |
109 | return { data: null, error } |
110 | } |
111 | } |
112 |
|