UNPKG

@daveyplate/supabase-swr-entities

Version:

An entity management library for Supabase and SWR

399 lines (398 loc) 22.9 kB
import { useEffect, useMemo, useCallback } from "react"; import { useSession, useSupabaseClient } from "@supabase/auth-helpers-react"; import { v4 } from "uuid"; import { usePeers } from "./use-peers"; import { apiPath } from "./client-utils"; import { useCreateEntity, useDeleteEntity, useMutateEntity, useUpdateEntity } from "./use-entity-helpers"; import { useCache, useInfiniteCache } from "./use-cache-hooks"; /** * Hook for fetching an entity by `id` or params */ export function useEntity(table, id, params, swrConfig) { const updateEntity = useUpdateEntity(); const deleteEntity = useDeleteEntity(); const mutateEntity = useMutateEntity(); const path = apiPath(table, id, params); const swr = useCache(path, swrConfig); const { data } = swr; const entity = useMemo(() => { var _a; return id ? data : (_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a[0]; }, [data]); const update = useCallback(async (fields) => updateEntity(table, id, fields, params), [table, id, entity, JSON.stringify(params)]); const doDelete = useCallback(async () => deleteEntity(table, id, params), [table, id, entity, JSON.stringify(params)]); // Pre-mutate the entity for ID for "me" or in case that ID isn't set useEffect(() => { if (!entity) return; if (id == (entity === null || entity === void 0 ? void 0 : entity.id)) return; mutateEntity(table, entity.id, entity, params); }, [entity]); return Object.assign({ entity: entity, updateEntity: update, deleteEntity: doDelete }, swr); } /** * Hook for fetching entities * @param {string} table - The table name * @param {Record<string, any>} [params] - The query parameters * @param {SWRConfiguration} [swrConfig] - The SWR config * @param {RealtimeOptions} [realtimeOptions] - The Realtime options * @param {boolean} [realtimeOptions.enabled] - Whether Realtime is enabled * @param {string} [realtimeOptions.provider] - The Realtime provider * @param {string} [realtimeOptions.room] - The Realtime room * @param {boolean} [realtimeOptions.listenOnly=false] - Whether to only listen for Realtime data */ export function useEntities(table, params, swrConfig, realtimeOptions) { const session = useSession(); const supabase = useSupabaseClient(); const createEntity = useCreateEntity(); const updateEntity = useUpdateEntity(); const deleteEntity = useDeleteEntity(); const mutateChild = useMutateEntity(); const path = apiPath(table, null, params); const swr = useCache(path, swrConfig); const { data, mutate } = swr; const { data: entities, count, limit, offset, has_more: hasMore } = useMemo(() => data || {}, [data]); // Reload the entities whenever realtime data is received const onData = useCallback(() => { mutate(); }, [mutate]); // Clean out the params for the room name const roomNameParams = params ? Object.assign({}, params) : null; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.lang; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.offset; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.limit; const room = useMemo(() => { return (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.room) || (Object.keys(roomNameParams || {}).length ? `${table}:${JSON.stringify(roomNameParams)}` : table) || undefined; }, [realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.room, JSON.stringify(params)]); const peersResult = (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" ? usePeers({ enabled: realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, onData, room }) : { sendData: () => { } }; const { sendData } = peersResult; // Mutate & precache all children entities on change useEffect(() => { entities === null || entities === void 0 ? void 0 : entities.forEach((entity) => { mutateChild(table, entity.id, entity, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.user) && mutateChild("profiles", entity.user.id, entity.user, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.sender) && mutateChild("profiles", entity.sender.id, entity.sender, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.recipient) && mutateChild("profiles", entity.recipient.id, entity.recipient, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); }); }, [entities, mutateChild, table, JSON.stringify(params)]); // Append an entity to the data & filter out duplicates const appendEntity = useCallback((data, newEntity) => { const filteredData = removeEntity(data, newEntity.id); filteredData.data.push(newEntity); return Object.assign(Object.assign({}, filteredData), { count: filteredData.data.count }); }, []); const removeEntity = useCallback((data, id) => { const filteredEntities = data.data.filter((entity) => entity.id != id); return Object.assign(Object.assign({}, data), { data: filteredEntities, count: filteredEntities.count }); }, []); const mutateEntity = useCallback((entity) => { if (!entity) return; mutate((prev) => appendEntity(prev, entity), false); }, [mutate]); // Supabase Realtime useEffect(() => { if (!(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) || !room) return; if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) != "supabase") return; const channelA = supabase.channel(room, { config: { private: true } }); // Subscribe to the Channel channelA.on('broadcast', { event: '*' }, () => mutate()).subscribe(); return () => { channelA.unsubscribe(); }; }, [realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, mutate, room]); const create = useCallback(async (entity, optimisticFields) => { const newEntity = Object.assign(Object.assign({ id: v4() }, entity), { locale: params === null || params === void 0 ? void 0 : params.lang }); try { const entity = await mutate(async () => { const { entity, error } = await createEntity(table, newEntity, params, optimisticFields); if (error) throw error; return entity; }, { populateCache: (entity, currentData) => appendEntity(currentData, entity), optimisticData: (currentData) => appendEntity(currentData, Object.assign(Object.assign({ created_at: new Date() }, newEntity), optimisticFields)), revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "create_entity" }); } return { entity }; } catch (error) { return { error: error }; } }, [ session, mutate, createEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); const update = useCallback(async (id, fields) => { try { const entity = await mutate(async () => { const { entity, error } = await updateEntity(table, id, fields, params); if (error) throw error; return entity; }, { populateCache: (entity, currentData) => appendEntity(currentData, entity), optimisticData: (currentData) => { const entity = currentData.data.find((e) => e.id == id); if (!entity) return data; return appendEntity(currentData, Object.assign(Object.assign({ updated_at: new Date() }, entity), fields)); }, revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "update_entity" }); } return { entity }; } catch (error) { return { error: error }; } }, [ mutate, updateEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); const doDelete = useCallback(async (id) => { try { await mutate(async () => { const { error } = await deleteEntity(table, id, params); if (error) throw error; }, { populateCache: (_, currentData) => removeEntity(currentData, id), optimisticData: (currentData) => removeEntity(currentData, id), revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "delete_entity" }); } } catch (error) { return { error: error }; } return { success: true }; }, [ mutate, deleteEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); return Object.assign(Object.assign(Object.assign({}, swr), peersResult), { entities: entities, count, limit, offset, hasMore, createEntity: create, updateEntity: update, deleteEntity: doDelete, mutateEntity }); } /** * Hook for fetching entities with infinite scrolling support * @param {string} table - The table name * @param {SWRConfiguration} [swrConfig] - The SWR config * @param {RealtimeOptions} [realtimeOptions] - The Realtime options * @param {boolean} [realtimeOptions.enabled] - Whether Realtime is enabled * @param {string} [realtimeOptions.provider] - The Realtime provider * @param {string} [realtimeOptions.room] - The Realtime room * @param {boolean} [realtimeOptions.listenOnly=false] - Whether to only listen for Realtime data */ export function useInfiniteEntities(table, params, swrConfig, realtimeOptions) { const session = useSession(); const supabase = useSupabaseClient(); const createEntity = useCreateEntity(); const updateEntity = useUpdateEntity(); const deleteEntity = useDeleteEntity(); const mutateChild = useMutateEntity(); // Load the entity pages using SWR const path = apiPath(table, null, params); const swr = useInfiniteCache(path, swrConfig); const { data, mutate } = swr; // Memoize the merged pages into entities and filter out duplicates const entities = useMemo(() => (data === null || data === void 0 ? void 0 : data.map(page => page.data).flat().filter((entity, index, self) => index === self.findIndex((t) => (t.id === entity.id)))) || [], [data]); // Set the other vars from the final page const { offset, limit, has_more: hasMore, count } = useMemo(() => (data === null || data === void 0 ? void 0 : data[data.length - 1]) || {}, [data]); // Reload the entities whenever realtime data is received const onData = useCallback(() => { mutate(); }, [mutate]); // Clean out the params for the room name const roomNameParams = params ? Object.assign({}, params) : null; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.lang; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.offset; roomNameParams === null || roomNameParams === void 0 ? true : delete roomNameParams.limit; const room = useMemo(() => { return (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.room) || (Object.keys(roomNameParams || {}).length ? `${table}:${JSON.stringify(roomNameParams)}` : table); }, [realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.room, JSON.stringify(params)]); const peersResult = (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" ? usePeers({ enabled: realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, onData, room }) : { sendData: () => { } }; const { sendData } = peersResult; // Mutate all children entities after each validation useEffect(() => { entities === null || entities === void 0 ? void 0 : entities.forEach((entity) => { mutateChild(table, entity.id, entity, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.user) && mutateChild("profiles", entity.user.id, entity.user, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.sender) && mutateChild("profiles", entity.sender.id, entity.sender, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); (entity === null || entity === void 0 ? void 0 : entity.recipient) && mutateChild("profiles", entity.recipient.id, entity.recipient, (params === null || params === void 0 ? void 0 : params.lang) ? { lang: params.lang } : null); }); }, [entities, mutateChild, table, JSON.stringify(params)]); // Append an entity to the data & filter out duplicates const appendEntity = useCallback((data, newEntity) => { // Filter this entity from all pages then push it to the first page const filteredPages = removeEntity(data, newEntity.id); filteredPages[0].data.push(newEntity); return filteredPages; }, []); const amendEntity = useCallback((data, newEntity) => { // Find this entity in a page and replace it with newEntity const amendedPages = data.map((page) => { const amendedData = page.data.map((entity) => entity.id == newEntity.id ? newEntity : entity); return Object.assign(Object.assign({}, page), { data: amendedData }); }); return amendedPages; }, []); const removeEntity = useCallback((data, id) => { // Filter this entity from all pages return data.map((page) => { const filteredData = page.data.filter((entity) => entity.id != id); return Object.assign(Object.assign({}, page), { data: filteredData }); }); }, []); const mutateEntity = useCallback((entity) => { entity && mutate((prev) => amendEntity(prev, entity), false); }, [mutate]); // Supabase Realtime useEffect(() => { if (!(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) || !room) return; if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) != "supabase") return; const channelA = supabase.channel(room, { config: { private: true } }); // Subscribe to the Channel channelA.on('broadcast', { event: '*' }, () => mutate()).subscribe(); return () => { channelA.unsubscribe(); }; }, [realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, room, mutate]); const create = useCallback(async (entity, optimisticFields) => { // Mutate the new entity directly to the parent cache const newEntity = Object.assign(Object.assign({ id: v4() }, entity), { locale: params === null || params === void 0 ? void 0 : params.lang }); try { const entity = await mutate(async () => { const { entity, error } = await createEntity(table, newEntity, params, optimisticFields); if (error || !entity) throw error; return [entity]; }, { populateCache: (entities, currentData) => appendEntity(currentData, entities[0]), optimisticData: (currentData) => { return appendEntity(currentData, Object.assign(Object.assign({ created_at: new Date() }, newEntity), optimisticFields)); }, revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "create_entity" }); } return { entity }; } catch (error) { return { error: error }; } }, [ session, mutate, createEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); const update = useCallback(async (id, fields) => { try { const entity = await mutate(async () => { const { entity, error } = await updateEntity(table, id, fields, params); if (error) throw error; return [entity]; }, { populateCache: (entities, currentData) => amendEntity(currentData, entities[0]), optimisticData: (currentData) => { const entity = currentData === null || currentData === void 0 ? void 0 : currentData.map(page => page.data).flat().find((e) => e.id == id); if (!entity) return currentData; return amendEntity(currentData, Object.assign(Object.assign({ updated_at: new Date() }, entity), fields)); }, revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "update_entity" }); } return { entity }; } catch (error) { return { error: error }; } }, [ session, mutate, updateEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); const doDelete = useCallback(async (id) => { try { await mutate(async () => { const { error } = await deleteEntity(table, id, params); if (error) throw error; return []; }, { populateCache: (_, currentData) => removeEntity(currentData, id), optimisticData: (currentData) => removeEntity(currentData, id), revalidate: false }); if ((realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled) && (realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider) == "peerjs" && !(realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly)) { sendData({ event: "delete_entity" }); } } catch (error) { return { error: error }; } return { success: true }; }, [ mutate, deleteEntity, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.enabled, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.provider, realtimeOptions === null || realtimeOptions === void 0 ? void 0 : realtimeOptions.listenOnly, sendData, JSON.stringify(params) ]); return Object.assign(Object.assign(Object.assign({}, swr), peersResult), { entities: entities, count, limit, offset, hasMore, createEntity: create, updateEntity: update, deleteEntity: doDelete, mutateEntity }); }