UNPKG

@adventurelabs/scout-core

Version:

Core utilities and helpers for Adventure Labs Scout applications

389 lines (388 loc) 17.7 kB
import { createApi, fakeBaseQuery } from "@reduxjs/toolkit/query/react"; import { generateSignedUrlsBatch } from "../helpers/storage"; // Custom serialize function to exclude supabase client const serializeQueryArgs = ({ queryArgs, endpointDefinition, endpointName, }) => { const { supabase, ...serializableArgs } = queryArgs; return JSON.stringify({ endpointName, args: serializableArgs }); }; // Create the API slice export const scoutApi = createApi({ reducerPath: "scoutApi", baseQuery: fakeBaseQuery(), tagTypes: ["Session", "Event", "Artifact"], endpoints: (builder) => ({ // ===================================================== // SESSIONS INFINITE QUERIES // ===================================================== getSessionsInfiniteByHerd: builder.query({ serializeQueryArgs, async queryFn({ herdId, limit = 20, cursor, supabase }) { try { if (!herdId) { return { error: { status: "CUSTOM_ERROR", error: "Herd ID is required" }, }; } const { data, error } = await supabase.rpc("get_sessions_infinite_by_herd", { herd_id_caller: herdId, limit_caller: limit + 1, // Fetch one extra to determine if there are more cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const sessions = data || []; const hasMore = sessions.length > limit; const resultSessions = hasMore ? sessions.slice(0, limit) : sessions; const nextCursor = hasMore && resultSessions.length > 0 ? { timestamp: resultSessions[resultSessions.length - 1].timestamp_start || "", id: resultSessions[resultSessions.length - 1].id || 0, } : null; return { data: { sessions: resultSessions, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.sessions.map(({ id }) => ({ type: "Session", id: id || "unknown", })), { type: "Session", id: "LIST" }, ] : [{ type: "Session", id: "LIST" }], }), getSessionsInfiniteByDevice: builder.query({ serializeQueryArgs, async queryFn({ deviceId, limit = 20, cursor, supabase }) { try { if (!deviceId) { return { error: { status: "CUSTOM_ERROR", error: "Device ID is required" }, }; } const { data, error } = await supabase.rpc("get_sessions_infinite_by_device", { device_id_caller: deviceId, limit_caller: limit + 1, cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const sessions = data || []; const hasMore = sessions.length > limit; const resultSessions = hasMore ? sessions.slice(0, limit) : sessions; const nextCursor = hasMore && resultSessions.length > 0 ? { timestamp: resultSessions[resultSessions.length - 1].timestamp_start || "", id: resultSessions[resultSessions.length - 1].id || 0, } : null; return { data: { sessions: resultSessions, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.sessions.map(({ id }) => ({ type: "Session", id: id || "unknown", })), { type: "Session", id: "LIST" }, ] : [{ type: "Session", id: "LIST" }], }), // ===================================================== // EVENTS INFINITE QUERIES // ===================================================== getEventsInfiniteByHerd: builder.query({ serializeQueryArgs, async queryFn({ herdId, limit = 20, cursor, supabase }) { try { if (!herdId) { return { error: { status: "CUSTOM_ERROR", error: "Herd ID is required" }, }; } const { data, error } = await supabase.rpc("get_events_infinite_by_herd", { herd_id_caller: herdId, limit_caller: limit + 1, cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const events = data || []; const hasMore = events.length > limit; const resultEvents = hasMore ? events.slice(0, limit) : events; const nextCursor = hasMore && resultEvents.length > 0 ? { timestamp: resultEvents[resultEvents.length - 1] .timestamp_observation || "", id: resultEvents[resultEvents.length - 1].id || 0, } : null; return { data: { events: resultEvents, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.events.map(({ id }) => ({ type: "Event", id: id || "unknown", })), { type: "Event", id: "LIST" }, ] : [{ type: "Event", id: "LIST" }], }), getEventsInfiniteByDevice: builder.query({ serializeQueryArgs, async queryFn({ deviceId, limit = 20, cursor, supabase }) { try { if (!deviceId) { return { error: { status: "CUSTOM_ERROR", error: "Device ID is required" }, }; } const { data, error } = await supabase.rpc("get_events_infinite_by_device", { device_id_caller: deviceId, limit_caller: limit + 1, cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const events = data || []; const hasMore = events.length > limit; const resultEvents = hasMore ? events.slice(0, limit) : events; const nextCursor = hasMore && resultEvents.length > 0 ? { timestamp: resultEvents[resultEvents.length - 1] .timestamp_observation || "", id: resultEvents[resultEvents.length - 1].id || 0, } : null; return { data: { events: resultEvents, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.events.map(({ id }) => ({ type: "Event", id: id || "unknown", })), { type: "Event", id: "LIST" }, ] : [{ type: "Event", id: "LIST" }], }), // ===================================================== // ARTIFACTS INFINITE QUERIES // ===================================================== getArtifactsInfiniteByHerd: builder.query({ serializeQueryArgs, async queryFn({ herdId, limit = 20, cursor, supabase }) { try { if (!herdId) { return { error: { status: "CUSTOM_ERROR", error: "Herd ID is required" }, }; } const { data, error } = await supabase.rpc("get_artifacts_infinite_by_herd", { herd_id_caller: herdId, limit_caller: limit + 1, cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const artifacts = data || []; const hasMore = artifacts.length > limit; const resultArtifacts = hasMore ? artifacts.slice(0, limit) : artifacts; // Generate signed URLs for artifacts const uniqueFilePaths = Array.from(new Set(resultArtifacts .map((artifact) => artifact.file_path) .filter((path) => path !== null && path !== undefined))); let urlMap = new Map(); if (uniqueFilePaths.length > 0) { try { const urlResults = await generateSignedUrlsBatch(uniqueFilePaths); urlResults.forEach((url, index) => { if (url) { urlMap.set(uniqueFilePaths[index], url); } }); } catch (urlError) { console.warn("Failed to generate signed URLs for artifacts:", urlError); } } const artifactsWithUrls = resultArtifacts.map((artifact) => ({ ...artifact, media_url: artifact.file_path ? urlMap.get(artifact.file_path) || null : null, })); const nextCursor = hasMore && resultArtifacts.length > 0 ? { timestamp: resultArtifacts[resultArtifacts.length - 1].created_at, id: resultArtifacts[resultArtifacts.length - 1].id, } : null; return { data: { artifacts: artifactsWithUrls, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.artifacts.map(({ id }) => ({ type: "Artifact", id, })), { type: "Artifact", id: "LIST" }, ] : [{ type: "Artifact", id: "LIST" }], }), getArtifactsInfiniteByDevice: builder.query({ serializeQueryArgs, async queryFn({ deviceId, limit = 20, cursor, supabase }) { try { if (!deviceId) { return { error: { status: "CUSTOM_ERROR", error: "Device ID is required" }, }; } const { data, error } = await supabase.rpc("get_artifacts_infinite_by_device", { device_id_caller: deviceId, limit_caller: limit + 1, cursor_timestamp: cursor?.timestamp || null, cursor_id: cursor?.id || null, }); if (error) { return { error: { status: "SUPABASE_ERROR", error: error.message }, }; } const artifacts = data || []; const hasMore = artifacts.length > limit; const resultArtifacts = hasMore ? artifacts.slice(0, limit) : artifacts; // Generate signed URLs for artifacts const uniqueFilePaths = Array.from(new Set(resultArtifacts .map((artifact) => artifact.file_path) .filter((path) => path !== null && path !== undefined))); let urlMap = new Map(); if (uniqueFilePaths.length > 0) { try { const urlResults = await generateSignedUrlsBatch(uniqueFilePaths); urlResults.forEach((url, index) => { if (url) { urlMap.set(uniqueFilePaths[index], url); } }); } catch (urlError) { console.warn("Failed to generate signed URLs for artifacts:", urlError); } } const artifactsWithUrls = resultArtifacts.map((artifact) => ({ ...artifact, media_url: artifact.file_path ? urlMap.get(artifact.file_path) || null : null, })); const nextCursor = hasMore && resultArtifacts.length > 0 ? { timestamp: resultArtifacts[resultArtifacts.length - 1].created_at, id: resultArtifacts[resultArtifacts.length - 1].id, } : null; return { data: { artifacts: artifactsWithUrls, nextCursor, hasMore, }, }; } catch (err) { return { error: { status: "FETCH_ERROR", error: String(err) } }; } }, providesTags: (result) => result ? [ ...result.artifacts.map(({ id }) => ({ type: "Artifact", id, })), { type: "Artifact", id: "LIST" }, ] : [{ type: "Artifact", id: "LIST" }], }), }), }); // Export hooks for usage in functional components export const { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, } = scoutApi;