UNPKG

supastash

Version:

Offline-first sync engine for Supabase in React Native using SQLite

172 lines (171 loc) 7.15 kB
import { getSupastashConfig } from "../../../core/config"; import { getSupastashDb } from "../../../db/dbInitializer"; import { getQueryStatusFromDb } from "../../../utils/sync/queryStatus"; import { supastashEventBus } from "../../events/eventBus"; import { normalizeForSupabase } from "../../getSafeValues"; import { refreshScreen } from "../../refreshScreenCalls"; import { getSafeValue } from "../../serializer"; import { updateLocalSyncedAt } from "../../sync/status/syncUpdate"; import { buildWhereClause } from "../helpers/remoteDb/queryFilterBuilder"; import { operatorMap } from "../helpers/remoteDb/queryUtils"; import { permanentlyDeleteData } from "../localDbQuery/delete"; /** * Queries the supabase database * @param state - The state of the query * @param isBatched - Whether the query is batched * @returns The result of the query */ export async function querySupabase(state, isBatched = false) { const { table, method, payload, filters, limit, select, isSingle, type, onConflictKeys, viewRemoteResult, preserveTimestamp, } = state; const config = getSupastashConfig(); let newPayload; const timeStamp = new Date().toISOString(); if (Array.isArray(payload) && payload.length > 0) { newPayload = payload.map((item) => { const { synced_at, ...rest } = item; const newItem = normalizeForSupabase({ ...rest }); if (!preserveTimestamp) { const userUpdatedAt = item.updated_at; newItem.updated_at = userUpdatedAt !== undefined ? userUpdatedAt : timeStamp; } if (method === "insert") { const userCreatedAt = item.created_at; newItem.created_at = userCreatedAt !== undefined ? userCreatedAt : timeStamp; } return newItem; }); } else if (payload) { const { synced_at, ...rest } = payload; newPayload = normalizeForSupabase({ ...rest }); if (!preserveTimestamp) { const userUpdatedAt = payload.updated_at; newPayload.updated_at = userUpdatedAt !== undefined ? userUpdatedAt : timeStamp; } if (method === "insert") { const userCreatedAt = payload.created_at; newPayload.created_at = userCreatedAt !== undefined ? userCreatedAt : timeStamp; } } if (!config.supabaseClient) { throw new Error("[Supastash] Supabase Client is required to perform this operation."); } const upsertOrInsertPayload = Array.isArray(newPayload) ? newPayload : [newPayload]; if (method === "upsert" && newPayload && onConflictKeys && onConflictKeys.length > 0) { for (const item of upsertOrInsertPayload) { const colArray = Object.keys(item); const includesConflictKeys = onConflictKeys.some((key) => colArray.includes(key)); if (!includesConflictKeys) { throw new Error(`onConflictKeys must include at least one key from the payload. Payload: ${JSON.stringify(newPayload)}`); } } } const supabase = config.supabaseClient; let query = supabase.from(table); let filterQuery; switch (method) { case "select": filterQuery = query.select(select || "*"); break; case "insert": if (!newPayload) throw new Error("[Supastash] Insert payload is missing."); filterQuery = query.insert(newPayload); break; case "update": if (!newPayload) throw new Error("[Supastash] Update payload is missing."); filterQuery = query.update(newPayload); break; case "upsert": const conflictKey = Array.isArray(onConflictKeys) && onConflictKeys.length > 0 ? onConflictKeys.join(",") : "id"; if (!newPayload) throw new Error("[Supastash] Upsert payload is missing."); filterQuery = query.upsert(newPayload, { onConflict: conflictKey, }); break; case "delete": filterQuery = query.update({ deleted_at: timeStamp, }); break; default: throw new Error(`[Supastash] Unsupported method "${method}"`); } if (method !== "upsert") { // Apply filters if (filters?.length && method !== "insert") { for (const { column, operator, value } of filters) { const op = operatorMap(operator); filterQuery = filterQuery[op](column, value); } } if (limit != null) { filterQuery = filterQuery.limit(limit); } if (isSingle) { filterQuery = filterQuery.single(); } } if (!isBatched && newPayload && (Array.isArray(newPayload) ? newPayload.length > 0 : true) && (viewRemoteResult || type === "remoteOnly")) { filterQuery = filterQuery.select(); } const result = await filterQuery; const db = await getSupastashDb(); if (result.error) { await getQueryStatusFromDb(table); supastashEventBus.emit("updateSyncStatus"); } if (!result.error && method !== "select" && type !== "remoteOnly" && type !== "remoteFirst") { if (method === "insert" && newPayload) { for (const item of upsertOrInsertPayload) { await updateLocalSyncedAt(table, [item.id]); } } if (method === "upsert" && newPayload) { const onConflictKeysArray = onConflictKeys && onConflictKeys.length > 0 ? onConflictKeys : ["id"]; for (const item of upsertOrInsertPayload) { const colArray = Object.keys(item); const includesConflictKeys = onConflictKeysArray.every((key) => colArray.includes(key)); if (!includesConflictKeys) { throw new Error(`Upsert failed: Conflict keys [${onConflictKeysArray.join(", ")}] must exist in payload. Received: ${JSON.stringify(item)}`); } const whereClause = onConflictKeysArray .map((key) => `${key} = ?`) .join(" AND "); const conflictValues = onConflictKeysArray.map((key) => getSafeValue(item[key])); await db.runAsync(`UPDATE ${table} SET synced_at = ? WHERE ${whereClause}`, [timeStamp, ...conflictValues]); } } if ((method === "update" || method === "delete") && filters?.length) { const { clause, values: filterValues } = buildWhereClause(filters); await db.runAsync(`UPDATE ${table} SET synced_at = ? ${clause}`, [ timeStamp, ...filterValues, ]); if (method === "delete") { await permanentlyDeleteData(table, filters); } } refreshScreen(table); } return result; }