diff options
Diffstat (limited to 'apps/proxy/src/supabase.js')
| -rw-r--r-- | apps/proxy/src/supabase.js | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/apps/proxy/src/supabase.js b/apps/proxy/src/supabase.js new file mode 100644 index 00000000..314caf59 --- /dev/null +++ b/apps/proxy/src/supabase.js @@ -0,0 +1,217 @@ +const INDEX_COLUMNS = [ + "anilist_id", + "mangadex_id", + "latest_en_chapter_text", + "latest_en_chapter_number", + "latest_en_volume_text", + "latest_en_chapter_id", + "latest_chapter_updated_at", + "last_seen_at", + "last_indexed_at", + "is_releasing", + "created_at", + "updated_at", +].join(","); + +const chunk = (items, size) => { + const chunks = []; + + for (let index = 0; index < items.length; index += size) + chunks.push(items.slice(index, index + size)); + + return chunks; +}; + +const supabaseUrl = (env, path) => + `${String(env.SUPABASE_URL || "").replace(/\/$/, "")}/rest/v1/${path}`; + +const supabaseHeaders = (env, headers = {}) => ({ + apikey: env.SUPABASE_SERVICE_ROLE_KEY, + Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`, + ...headers, +}); + +const supabaseRequest = async ( + env, + path, + { + method = "GET", + query = {}, + body, + headers = {}, + prefer = [], + returnNullOn404 = false, + } = {}, +) => { + const url = new URL(supabaseUrl(env, path)); + + for (const [key, value] of Object.entries(query)) + if (value !== undefined && value !== null) url.searchParams.set(key, value); + + const response = await fetch(url, { + method, + headers: supabaseHeaders(env, { + ...(body ? { "Content-Type": "application/json" } : {}), + ...(prefer.length ? { Prefer: prefer.join(",") } : {}), + ...headers, + }), + body: body ? JSON.stringify(body) : undefined, + }); + + if (returnNullOn404 && response.status === 404) return null; + + if (!response.ok) { + const message = await response.text(); + + throw new Error(`Supabase request failed (${response.status}): ${message}`); + } + + if (response.status === 204) return null; + + const text = await response.text(); + + if (!text.length) return null; + + return JSON.parse(text); +}; + +const buildInFilter = (values) => `in.(${values.join(",")})`; + +export const hasSupabaseConfig = (env) => + Boolean(env.SUPABASE_URL && env.SUPABASE_SERVICE_ROLE_KEY); + +export const getMangadexRowsByAniListIds = async (env, anilistIds) => { + const rows = []; + + for (const ids of chunk([...new Set(anilistIds)], 100)) + rows.push( + ...((await supabaseRequest(env, "mangadex_manga_index", { + query: { + select: INDEX_COLUMNS, + anilist_id: buildInFilter(ids), + }, + })) || []), + ); + + return rows; +}; + +export const getMangadexRowsByMangadexIds = async (env, mangadexIds) => { + const rows = []; + + for (const ids of chunk([...new Set(mangadexIds)], 100)) + rows.push( + ...((await supabaseRequest(env, "mangadex_manga_index", { + query: { + select: INDEX_COLUMNS, + mangadex_id: buildInFilter(ids), + }, + })) || []), + ); + + return rows; +}; + +export const getMangadexFailureRowsByAniListIds = async (env, anilistIds) => { + const rows = []; + + for (const ids of chunk([...new Set(anilistIds)], 100)) + rows.push( + ...((await supabaseRequest(env, "mangadex_resolution_failures", { + query: { + select: "anilist_id,last_attempted_at,updated_at", + anilist_id: buildInFilter(ids), + }, + returnNullOn404: true, + })) || []), + ); + + return rows; +}; + +export const upsertMangadexRows = async (env, rows) => { + if (!rows.length) return []; + + return supabaseRequest(env, "mangadex_manga_index", { + method: "POST", + query: { + on_conflict: "anilist_id", + }, + body: rows, + prefer: ["resolution=merge-duplicates", "return=representation"], + }); +}; + +export const touchMangadexRows = async (env, rows) => { + if (!rows.length) return []; + + return upsertMangadexRows( + env, + rows.map((row) => ({ + anilist_id: row.anilist_id, + mangadex_id: row.mangadex_id, + is_releasing: row.is_releasing, + last_seen_at: row.last_seen_at, + updated_at: row.updated_at, + })), + ); +}; + +export const upsertMangadexFailureRows = async (env, anilistIds) => { + if (!anilistIds.length) return null; + + return supabaseRequest(env, "mangadex_resolution_failures", { + method: "POST", + query: { + on_conflict: "anilist_id", + }, + body: [...new Set(anilistIds)].map((anilistId) => ({ + anilist_id: anilistId, + last_attempted_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + })), + prefer: ["resolution=merge-duplicates", "return=minimal"], + returnNullOn404: true, + }); +}; + +export const deleteMangadexFailureRows = async (env, anilistIds) => { + if (!anilistIds.length) return null; + + return supabaseRequest(env, "mangadex_resolution_failures", { + method: "DELETE", + query: { + anilist_id: buildInFilter([...new Set(anilistIds)]), + }, + prefer: ["return=minimal"], + returnNullOn404: true, + }); +}; + +export const getSyncState = async (env, name) => { + const rows = await supabaseRequest(env, "mangadex_sync_state", { + query: { + select: "name,cursor_updated_at,updated_at", + name: `eq.${name}`, + limit: "1", + }, + }); + + return rows?.[0] || null; +}; + +export const upsertSyncState = async (env, name, cursorUpdatedAt) => + supabaseRequest(env, "mangadex_sync_state", { + method: "POST", + query: { + on_conflict: "name", + }, + body: [ + { + name, + cursor_updated_at: cursorUpdatedAt, + updated_at: new Date().toISOString(), + }, + ], + prefer: ["resolution=merge-duplicates", "return=minimal"], + }); |