aboutsummaryrefslogtreecommitdiff
path: root/apps/proxy/src/supabase.js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/proxy/src/supabase.js')
-rw-r--r--apps/proxy/src/supabase.js217
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"],
+ });