From 75e793b4328a5b79f97e3a42626cb68c8c46b46c Mon Sep 17 00:00:00 2001 From: Dominik Hryshaiev Date: Tue, 22 Oct 2024 14:46:05 +0200 Subject: [PATCH] refactor(habits): unify state provider --- package.json | 4 +- .../habit/add-habit/AddHabitDialogButton.tsx | 38 +- .../habit/edit-habit/EditHabitDialog.tsx | 24 +- .../habit/habits-page/HabitIconCell.tsx | 61 +-- .../habit/habits-page/HabitsPage.tsx | 9 +- src/context/Habits/HabitsContext.ts | 17 +- src/context/Habits/HabitsProvider.tsx | 269 +++++++------ src/context/Traits/TraitsContext.ts | 3 +- src/context/Traits/TraitsProvider.tsx | 42 +- src/models/habit.model.ts | 24 +- src/models/trait.model.ts | 4 +- src/services/habit.ts | 8 +- src/services/storage.ts | 17 +- src/services/traits.ts | 7 +- src/utils/getErrorMessage.ts | 27 ++ src/utils/index.ts | 1 + src/utils/transformEntity.ts | 3 +- supabase/database.types.ts | 372 +----------------- yarn.lock | 8 +- 19 files changed, 298 insertions(+), 640 deletions(-) create mode 100644 src/utils/getErrorMessage.ts diff --git a/package.json b/package.json index b9c730e..6b2f3d1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "db:diff": "supabase db diff", "db:migration:up": "supabase migration up", "db:migration:new": "supabase migration new", - "db:gen-types": "supabase gen types --lang=typescript --local > supabase/database.types.ts" + "db:gen-types": "supabase gen types --lang=typescript --schema public auth storage --local > supabase/database.types.ts" }, "lint-staged": { "**/*.{ts,tsx}": [ @@ -109,7 +109,7 @@ "prettier": "3.1.1", "prettier-plugin-tailwindcss": "^0.6.6", "rollup-plugin-visualizer": "^5.12.0", - "supabase": "1.206.0", + "supabase": "1.207.9", "tailwindcss": "^3.4.10", "ts-jest": "^29.1.2", "typescript": "5.6.3", diff --git a/src/components/habit/add-habit/AddHabitDialogButton.tsx b/src/components/habit/add-habit/AddHabitDialogButton.tsx index 6ff45c2..0456238 100644 --- a/src/components/habit/add-habit/AddHabitDialogButton.tsx +++ b/src/components/habit/add-habit/AddHabitDialogButton.tsx @@ -1,5 +1,5 @@ import { AddCustomTraitModal, VisuallyHiddenInput } from '@components'; -import { useHabits, useSnackbar, useTraits } from '@context'; +import { useHabits, useTraits } from '@context'; import { useTextField, useFileField } from '@hooks'; import { Button, @@ -14,13 +14,11 @@ import { Textarea, } from '@nextui-org/react'; import { CloudArrowUp, Plus } from '@phosphor-icons/react'; -import { StorageBuckets, uploadFile } from '@services'; import { useUser } from '@supabase/auth-helpers-react'; import React from 'react'; const AddHabitDialogButton = () => { const user = useUser(); - const { showSnackbar } = useSnackbar(); const { traits } = useTraits(); const { fetchingHabits, addingHabit, addHabit } = useHabits(); const [open, setOpen] = React.useState(false); @@ -44,31 +42,21 @@ const AddHabitDialogButton = () => { }; const handleAdd = async () => { - try { - const habit = { + if (!user) { + return null; + } + + await addHabit( + { name, description, - userId: user?.id || '', - traitId: traitId as unknown as number, - }; - - let iconPath = ''; + userId: user.id, + traitId: +traitId, + }, + icon + ); - if (icon) { - iconPath = `${user?.id}/icon.name`; - await uploadFile(StorageBuckets.HABIT_ICONS, iconPath, icon); - } - - await addHabit(habit); - } catch (error) { - console.error(error); - - showSnackbar('Something went wrong while adding your habit', { - color: 'danger', - }); - } finally { - handleDialogClose(); - } + handleDialogClose(); }; return ( diff --git a/src/components/habit/edit-habit/EditHabitDialog.tsx b/src/components/habit/edit-habit/EditHabitDialog.tsx index d7163e5..9a264f4 100644 --- a/src/components/habit/edit-habit/EditHabitDialog.tsx +++ b/src/components/habit/edit-habit/EditHabitDialog.tsx @@ -32,8 +32,7 @@ const EditHabitDialog = ({ const [description, handleDescriptionChange, , setDescription] = useTextField(); const [traitId, setTraitId] = React.useState(''); - const [isUpdating, setIsUpdating] = React.useState(false); - const { updateHabit } = useHabits(); + const { updateHabit, habitIdBeingUpdated } = useHabits(); const { traits } = useTraits(); const user = useUser(); @@ -59,24 +58,21 @@ const EditHabitDialog = ({ }; const handleSubmit = async () => { - const updatedAt = new Date(); - updatedAt.setMilliseconds(0); - updatedAt.setSeconds(0); - setIsUpdating(true); - const newHabit = { + if (!user) { + return null; + } + + await updateHabit(habit.id, user.id, { name, description, traitId: +traitId, - userId: user?.id as string, - iconPath: habit.iconPath, - createdAt: habit.createdAt, - updatedAt: updatedAt.toISOString(), - }; - await updateHabit(habit.id, newHabit); - setIsUpdating(false); + }); + handleClose(); }; + const isUpdating = habitIdBeingUpdated === habit.id; + return ( { - const { showSnackbar } = useSnackbar(); const { updateHabit } = useHabits(); const user = useUser(); const iconUrl = getHabitIconUrl(habit.iconPath); - const handleFileChange: React.ChangeEventHandler = async ( - event - ) => { - const iconFile = event.target.files?.[0]; - if (iconFile) { - const existingIconPath = habit.iconPath; - - try { - const split = iconFile.name.split('.'); - const extension = split[split.length - 1]; - const iconPath = `${user?.id}/habit-id-${habit.id}.${extension}`; - - if (existingIconPath) { - const { error } = await updateFile( - StorageBuckets.HABIT_ICONS, - iconPath, - iconFile - ); - - if (error) { - throw error; - } - - await updateHabit(habit.id, { ...habit, iconPath }); - - showSnackbar('Icon replaced!', { - color: 'success', - }); - } else { - const { data, error } = await uploadFile( - StorageBuckets.HABIT_ICONS, - iconPath, - iconFile - ); - - if (error) { - throw error; - } + const handleFileChange: React.ChangeEventHandler = async ({ + target: { files }, + }) => { + if (!user || !files) { + return null; + } - await updateHabit(habit.id, { ...habit, iconPath: data.path }); + const [iconFile] = files; - showSnackbar('Icon uploaded!', { - color: 'success', - }); - } - } catch (e) { - showSnackbar((e as Error).message || 'Failed to upload icon', { - color: 'danger', - }); - } - } + await updateHabit(habit.id, user.id, {}, iconFile); }; return ( diff --git a/src/components/habit/habits-page/HabitsPage.tsx b/src/components/habit/habits-page/HabitsPage.tsx index d1902f5..5701fc4 100644 --- a/src/components/habit/habits-page/HabitsPage.tsx +++ b/src/components/habit/habits-page/HabitsPage.tsx @@ -64,11 +64,10 @@ const habitColumns = [ const HabitsPage = () => { const user = useUser(); - const { habits, removeHabit } = useHabits(); + const { habits, removeHabit, habitIdBeingDeleted } = useHabits(); const { removeOccurrencesByHabitId } = useOccurrences(); const [habitToEdit, setHabitToEdit] = React.useState(null); const [habitToRemove, setHabitToRemove] = React.useState(null); - const [isRemovingHabit, setIsRemovingHabit] = React.useState(false); useDocumentTitle('My Habits | Habitrack'); @@ -85,11 +84,9 @@ const HabitsPage = () => { return null; } - setIsRemovingHabit(true); - await removeHabit(habitToRemove.id); + await removeHabit(habitToRemove); removeOccurrencesByHabitId(habitToRemove.id); setHabitToRemove(null); - setIsRemovingHabit(false); }; const handleEditStart = (habit: Habit) => { @@ -201,7 +198,7 @@ const HabitsPage = () => { heading="Delete habit" onConfirm={handleRemovalConfirmed} onCancel={handleRemovalCancel} - loading={isRemovingHabit} + loading={habitIdBeingDeleted === habitToRemove?.id} >
Are you sure you want to delete {habitToRemove?.name}{' '} diff --git a/src/context/Habits/HabitsContext.ts b/src/context/Habits/HabitsContext.ts index bbb7f95..2cd6181 100644 --- a/src/context/Habits/HabitsContext.ts +++ b/src/context/Habits/HabitsContext.ts @@ -1,15 +1,20 @@ -import type { Habit, HabitsMap } from '@models'; -import { type HabitsInsert, type HabitsUpdate } from '@services'; +import type { Habit, HabitsInsert, HabitsUpdate } from '@models'; import React from 'react'; type HabitsContextType = { + habitIdBeingUpdated: number | null; + habitIdBeingDeleted: number | null; addingHabit: boolean; fetchingHabits: boolean; habits: Habit[]; - habitsMap: HabitsMap; - addHabit: (habit: HabitsInsert) => Promise; - removeHabit: (habitId: number) => Promise; - updateHabit: (habitId: number, habit: HabitsUpdate) => Promise; + addHabit: (habit: HabitsInsert, icon?: File | null) => Promise; + removeHabit: (habit: Habit) => Promise; + updateHabit: ( + habitId: number, + userId: string, + habit: HabitsUpdate, + icon?: File | null + ) => Promise; }; export const HabitsContext = React.createContext( diff --git a/src/context/Habits/HabitsProvider.tsx b/src/context/Habits/HabitsProvider.tsx index 3e4a8cb..b205030 100644 --- a/src/context/Habits/HabitsProvider.tsx +++ b/src/context/Habits/HabitsProvider.tsx @@ -1,45 +1,52 @@ import { HabitsContext, useSnackbar } from '@context'; import { useDataFetch } from '@hooks'; -import type { Habit, HabitsMap } from '@models'; +import type { Habit, HabitsInsert, HabitsUpdate } from '@models'; import { createHabit, deleteFile, destroyHabit, - type HabitsInsert, - type HabitsUpdate, listHabits, patchHabit, StorageBuckets, + uploadFile, } from '@services'; import { makeTestHabit } from '@tests'; +import { getErrorMessage } from '@utils'; import React, { type ReactNode } from 'react'; const HabitsProvider = ({ children }: { children: ReactNode }) => { const { showSnackbar } = useSnackbar(); - const [addingHabit, setAddingHabit] = React.useState(false); const [fetchingHabits, setFetchingHabits] = React.useState(false); const [habits, setHabits] = React.useState([makeTestHabit()]); - const [habitsMap, setHabitsMap] = React.useState({}); + const [habitIdBeingUpdated, setHabitIdBeingUpdated] = React.useState< + number | null + >(null); + const [habitIdBeingDeleted, setHabitIdBeingDeleted] = React.useState< + number | null + >(null); const fetchHabits = React.useCallback(async () => { - setFetchingHabits(true); - - const habits = await listHabits(); - setHabits(habits); - - const habitsMap = habits.reduce((acc, habit) => { - return { ...acc, [habit.id]: habit }; - }, {}); - - setHabitsMap(habitsMap); - - setFetchingHabits(false); - }, []); + try { + setFetchingHabits(true); + setHabits(await listHabits()); + } catch (error) { + console.error(error); + showSnackbar( + 'Something went wrong while fetching your habits. Please try reloading the page.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + setFetchingHabits(false); + } + }, [showSnackbar]); const clearHabits = React.useCallback(() => { setHabits([]); - setHabitsMap({}); }, []); useDataFetch({ @@ -47,116 +54,150 @@ const HabitsProvider = ({ children }: { children: ReactNode }) => { load: fetchHabits, }); - React.useEffect(() => { - setHabitsMap( - habits.reduce((acc, habit) => { - return { ...acc, [habit.id]: habit }; - }, {}) - ); - }, [habits]); + const uploadHabitIcon = async (userId: string, icon?: File | null) => { + let iconPath = ''; - const addHabit = async (habit: HabitsInsert) => { - try { - setAddingHabit(true); - - const newHabit = await createHabit(habit); - - setHabits((prevHabits) => [...prevHabits, newHabit]); - setHabitsMap((prevHabits) => ({ - ...prevHabits, - [newHabit.id]: newHabit, - })); - showSnackbar('Your habit has been added!', { - color: 'success', - dismissible: true, - dismissText: 'Done', - }); - - return newHabit as Habit; - } catch (error) { - showSnackbar('Something went wrong while adding your habit', { - color: 'danger', - dismissible: true, - }); - - console.error(error); - - return Promise.resolve({} as Habit); - } finally { - setAddingHabit(false); + if (icon) { + iconPath = `${userId}/${Date.now()}-${icon.name}`; + await uploadFile(StorageBuckets.HABIT_ICONS, iconPath, icon); } - }; - - const updateHabit = async (id: number, habit: HabitsUpdate) => { - try { - const updatedHabit = await patchHabit(id, habit); - - setHabits((prevHabits) => { - const habitIndex = prevHabits.findIndex((h) => h.id === id); - const nextHabits = [...prevHabits]; - nextHabits[habitIndex] = updatedHabit; - return nextHabits; - }); - setHabitsMap((prevHabits) => ({ ...prevHabits, [id]: updatedHabit })); - - showSnackbar('Your habit has been updated!', { - color: 'success', - dismissible: true, - }); - - return updatedHabit; - } catch (error) { - showSnackbar('Something went wrong while updating your habit', { - color: 'danger', - dismissible: true, - }); - console.error(error); - - return Promise.resolve({} as Habit); - } + return iconPath; }; - const removeHabit = async (id: number) => { - try { - await destroyHabit(id); - - if (habitsMap[id]?.iconPath) { - await deleteFile(StorageBuckets.HABIT_ICONS, habitsMap[id].iconPath!); + const addHabit = React.useCallback( + async (habit: HabitsInsert, icon?: File | null) => { + try { + setAddingHabit(true); + + const iconPath = await uploadHabitIcon(habit.userId, icon); + + const newHabit = await createHabit({ ...habit, iconPath }); + + setHabits((prevHabits) => [...prevHabits, newHabit]); + + showSnackbar('Your habit has been added!', { + color: 'success', + dismissible: true, + dismissText: 'Done', + }); + } catch (error) { + showSnackbar( + 'Something went wrong while adding your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + + console.error(error); + } finally { + setAddingHabit(false); } + }, + [showSnackbar] + ); - const nextHabits = habits.filter((habit) => habit.id !== id); - setHabits(nextHabits); - - const nextHabitsMap = { ...habitsMap }; - delete nextHabitsMap[id]; - setHabitsMap(nextHabitsMap); - - showSnackbar('Your habit has been deleted!', { - dismissible: true, - }); - } catch (error) { - showSnackbar('Something went wrong while deleting your habit', { - color: 'danger', - dismissible: true, - }); + const updateHabit = React.useCallback( + async ( + id: number, + userId: string, + habit: HabitsUpdate, + icon?: File | null + ) => { + try { + setHabitIdBeingUpdated(id); + + const iconPath = await uploadHabitIcon(userId, icon); + + const updatedHabit = await patchHabit(id, { ...habit, iconPath }); + + setHabits((prevHabits) => { + const habitIndex = prevHabits.findIndex((h) => h.id === id); + const nextHabits = [...prevHabits]; + nextHabits[habitIndex] = updatedHabit; + return nextHabits; + }); + + showSnackbar('Your habit has been updated!', { + color: 'success', + dismissible: true, + }); + } catch (error) { + showSnackbar( + 'Something went wrong while updating your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + + console.error(error); + } finally { + setHabitIdBeingUpdated(null); + } + }, + [showSnackbar] + ); - console.error(error); - } - }; + const removeHabit = React.useCallback( + async ({ id, iconPath }: Habit) => { + try { + setHabitIdBeingDeleted(id); + + await destroyHabit(id); + + if (iconPath) { + await deleteFile(StorageBuckets.HABIT_ICONS, iconPath); + } + + const nextHabits = habits.filter((habit) => habit.id !== id); + setHabits(nextHabits); + + showSnackbar('Your habit has been deleted.', { + dismissible: true, + }); + } catch (error) { + showSnackbar( + 'Something went wrong while deleting your habit. Please try again.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + + console.error(error); + } finally { + setHabitIdBeingDeleted(null); + } + }, + [habits, showSnackbar] + ); - const value = React.useMemo( - () => ({ + const value = React.useMemo(() => { + return { + habitIdBeingUpdated, + habitIdBeingDeleted, addingHabit, fetchingHabits, habits, - habitsMap, addHabit, removeHabit, updateHabit, - }), - [addingHabit, fetchingHabits, habits, habitsMap] // eslint-disable-line react-hooks/exhaustive-deps - ); + }; + }, [ + habitIdBeingUpdated, + habitIdBeingDeleted, + addingHabit, + fetchingHabits, + habits, + addHabit, + removeHabit, + updateHabit, + ]); return ( {children} diff --git a/src/context/Traits/TraitsContext.ts b/src/context/Traits/TraitsContext.ts index 9f44e6e..610d2de 100644 --- a/src/context/Traits/TraitsContext.ts +++ b/src/context/Traits/TraitsContext.ts @@ -1,5 +1,4 @@ -import type { Trait } from '@models'; -import { type TraitsInsert } from '@services'; +import type { Trait, TraitsInsert } from '@models'; import React from 'react'; type TraitsContextType = { diff --git a/src/context/Traits/TraitsProvider.tsx b/src/context/Traits/TraitsProvider.tsx index f24da79..9ffb0bc 100644 --- a/src/context/Traits/TraitsProvider.tsx +++ b/src/context/Traits/TraitsProvider.tsx @@ -1,8 +1,9 @@ import { TraitsContext, useSnackbar } from '@context'; import { useDataFetch } from '@hooks'; -import type { Trait } from '@models'; -import { listTraits, createTrait, type TraitsInsert } from '@services'; +import type { Trait, TraitsInsert } from '@models'; +import { listTraits, createTrait } from '@services'; import { makeTestTrait } from '@tests'; +import { getErrorMessage } from '@utils'; import React, { type ReactNode } from 'react'; const testTraits = [ @@ -11,21 +12,33 @@ const testTraits = [ ]; const TraitsProvider = ({ children }: { children: ReactNode }) => { + const { showSnackbar } = useSnackbar(); const [traits, setTraits] = React.useState(testTraits); const [fetchingTraits, setFetchingTraits] = React.useState(false); const [addingTrait, setAddingTrait] = React.useState(false); - const { showSnackbar } = useSnackbar(); const clearTraits = React.useCallback(() => { setTraits([]); }, []); const fetchTraits = React.useCallback(async () => { - setFetchingTraits(true); - const traits = await listTraits(); - setTraits(traits); - setFetchingTraits(false); - }, []); + try { + setFetchingTraits(true); + setTraits(await listTraits()); + } catch (error) { + console.error(error); + showSnackbar( + 'Something went wrong while fetching your traits. Please try reloading the page.', + { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + } + ); + } finally { + setFetchingTraits(false); + } + }, [showSnackbar]); useDataFetch({ load: fetchTraits, @@ -48,14 +61,11 @@ const TraitsProvider = ({ children }: { children: ReactNode }) => { }); } catch (error) { console.error(error); - showSnackbar( - (error as Error).message || - 'Something went wrong while adding your trait', - { - color: 'danger', - dismissible: true, - } - ); + showSnackbar('Something went wrong while adding your trait', { + description: `Error details: ${getErrorMessage(error)}`, + color: 'danger', + dismissible: true, + }); } finally { setAddingTrait(false); } diff --git a/src/models/habit.model.ts b/src/models/habit.model.ts index d362175..0542d88 100644 --- a/src/models/habit.model.ts +++ b/src/models/habit.model.ts @@ -1,16 +1,18 @@ +import type { CamelCasedPropertiesDeep } from 'type-fest'; + +import type { + Tables, + TablesInsert, + TablesUpdate, +} from '../../supabase/database.types'; + import { type Trait } from './trait.model'; -export type Habit = { - id: number; - name: string; - description: string | null; - userId: string; - createdAt: string; - updatedAt: string | null; - traitId: number; +type RawHabit = CamelCasedPropertiesDeep>; + +export type Habit = RawHabit & { trait: Pick | null; - iconPath: string | null; }; -type HabitId = string; -export type HabitsMap = Record; +export type HabitsInsert = CamelCasedPropertiesDeep>; +export type HabitsUpdate = CamelCasedPropertiesDeep>; diff --git a/src/models/trait.model.ts b/src/models/trait.model.ts index ffb53c2..96135c7 100644 --- a/src/models/trait.model.ts +++ b/src/models/trait.model.ts @@ -1,5 +1,7 @@ import { type CamelCasedPropertiesDeep } from 'type-fest'; -import { type Tables } from '../../supabase/database.types'; +import { type Tables, type TablesInsert } from '../../supabase/database.types'; export type Trait = CamelCasedPropertiesDeep>; + +export type TraitsInsert = CamelCasedPropertiesDeep>; diff --git a/src/services/habit.ts b/src/services/habit.ts index 7d2ec87..35ee9a4 100644 --- a/src/services/habit.ts +++ b/src/services/habit.ts @@ -1,16 +1,10 @@ import { supabaseClient } from '@helpers'; -import { type Habit } from '@models'; +import { type Habit, type HabitsInsert, type HabitsUpdate } from '@models'; import { transformClientEntity, transformServerEntities, transformServerEntity, } from '@utils'; -import { type CamelCasedPropertiesDeep } from 'type-fest'; - -import type { TablesInsert, TablesUpdate } from '../../supabase/database.types'; - -export type HabitsInsert = CamelCasedPropertiesDeep>; -export type HabitsUpdate = CamelCasedPropertiesDeep>; export const createHabit = async (body: HabitsInsert): Promise => { const serverBody = transformClientEntity(body); diff --git a/src/services/storage.ts b/src/services/storage.ts index c2bddbb..4433c1a 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -3,13 +3,6 @@ import { supabaseClient } from '@helpers'; export enum StorageBuckets { HABIT_ICONS = 'habit_icons', } -export const listFiles = async (bucket: StorageBuckets, path: string) => { - return supabaseClient.storage.from(bucket).list(path, { - limit: 100, - offset: 0, - sortBy: { column: 'name', order: 'asc' }, - }); -}; export const uploadFile = async ( bucket: StorageBuckets, @@ -18,7 +11,7 @@ export const uploadFile = async ( ) => { return supabaseClient.storage.from(bucket).upload(path, file, { cacheControl: '3600', - upsert: false, + upsert: true, }); }; @@ -35,11 +28,3 @@ export const updateFile = async ( export const deleteFile = async (bucket: StorageBuckets, path: string) => { return supabaseClient.storage.from(bucket).remove([path]); }; - -export const createSignedUrl = async ( - bucket: StorageBuckets, - path: string, - expiresIn: number -) => { - return supabaseClient.storage.from(bucket).createSignedUrl(path, expiresIn); -}; diff --git a/src/services/traits.ts b/src/services/traits.ts index e8e1fab..d293dee 100644 --- a/src/services/traits.ts +++ b/src/services/traits.ts @@ -1,15 +1,10 @@ import { supabaseClient } from '@helpers'; -import { type Trait } from '@models'; +import type { Trait, TraitsInsert } from '@models'; import { transformClientEntity, transformServerEntities, transformServerEntity, } from '@utils'; -import type { CamelCasedPropertiesDeep } from 'type-fest'; - -import type { TablesInsert } from '../../supabase/database.types'; - -export type TraitsInsert = CamelCasedPropertiesDeep>; export const createTrait = async (body: TraitsInsert): Promise => { const serverBody = transformClientEntity(body); diff --git a/src/utils/getErrorMessage.ts b/src/utils/getErrorMessage.ts new file mode 100644 index 0000000..0d664de --- /dev/null +++ b/src/utils/getErrorMessage.ts @@ -0,0 +1,27 @@ +type ErrorWithMessage = { + message: string; +}; + +const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +}; + +const toErrorWithMessage = (maybeError: unknown): ErrorWithMessage => { + if (isErrorWithMessage(maybeError)) { + return maybeError; + } + + try { + return new Error(JSON.stringify(maybeError)); + } catch { + return new Error(String(maybeError)); + } +}; + +export const getErrorMessage = (error: unknown) => + toErrorWithMessage(error).message; diff --git a/src/utils/index.ts b/src/utils/index.ts index c049582..8e4075b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,4 +2,5 @@ export * from './capitalizeFirstLetter'; export * from './generateCalendarRange'; export * from './transformEntity'; export * from './getHabitIconUrl'; +export * from './getErrorMessage'; export * from './cache'; diff --git a/src/utils/transformEntity.ts b/src/utils/transformEntity.ts index 14ea345..2e69b5e 100644 --- a/src/utils/transformEntity.ts +++ b/src/utils/transformEntity.ts @@ -1,10 +1,9 @@ import { type CamelCasedPropertiesDeep, - type SnakeCase, type SnakeCasedPropertiesDeep, } from 'type-fest'; -const transformServerKey = (key: SnakeCase) => { +const transformServerKey = (key: string) => { return key .split('_') .map((word, index) => { diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 4189662..4a521d7 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -7,31 +7,6 @@ export type Json = | Json[] export type Database = { - graphql_public: { - Tables: { - [_ in never]: never - } - Views: { - [_ in never]: never - } - Functions: { - graphql: { - Args: { - operationName?: string - query?: string - variables?: Json - extensions?: Json - } - Returns: Json - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } public: { Tables: { accounts: { @@ -56,15 +31,7 @@ export type Database = { name?: string | null updated_at?: string | null } - Relationships: [ - { - foreignKeyName: "accounts_id_fkey" - columns: ["id"] - isOneToOne: true - referencedRelation: "users" - referencedColumns: ["id"] - }, - ] + Relationships: [] } habits: { Row: { @@ -195,13 +162,6 @@ export type Database = { referencedRelation: "habits" referencedColumns: ["id"] }, - { - foreignKeyName: "public_occurrences_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] - }, ] } traits: { @@ -269,321 +229,6 @@ export type Database = { [_ in never]: never } } - storage: { - Tables: { - buckets: { - Row: { - allowed_mime_types: string[] | null - avif_autodetection: boolean | null - created_at: string | null - file_size_limit: number | null - id: string - name: string - owner: string | null - owner_id: string | null - public: boolean | null - updated_at: string | null - } - Insert: { - allowed_mime_types?: string[] | null - avif_autodetection?: boolean | null - created_at?: string | null - file_size_limit?: number | null - id: string - name: string - owner?: string | null - owner_id?: string | null - public?: boolean | null - updated_at?: string | null - } - Update: { - allowed_mime_types?: string[] | null - avif_autodetection?: boolean | null - created_at?: string | null - file_size_limit?: number | null - id?: string - name?: string - owner?: string | null - owner_id?: string | null - public?: boolean | null - updated_at?: string | null - } - Relationships: [] - } - migrations: { - Row: { - executed_at: string | null - hash: string - id: number - name: string - } - Insert: { - executed_at?: string | null - hash: string - id: number - name: string - } - Update: { - executed_at?: string | null - hash?: string - id?: number - name?: string - } - Relationships: [] - } - objects: { - Row: { - bucket_id: string | null - created_at: string | null - id: string - last_accessed_at: string | null - metadata: Json | null - name: string | null - owner: string | null - owner_id: string | null - path_tokens: string[] | null - updated_at: string | null - user_metadata: Json | null - version: string | null - } - Insert: { - bucket_id?: string | null - created_at?: string | null - id?: string - last_accessed_at?: string | null - metadata?: Json | null - name?: string | null - owner?: string | null - owner_id?: string | null - path_tokens?: string[] | null - updated_at?: string | null - user_metadata?: Json | null - version?: string | null - } - Update: { - bucket_id?: string | null - created_at?: string | null - id?: string - last_accessed_at?: string | null - metadata?: Json | null - name?: string | null - owner?: string | null - owner_id?: string | null - path_tokens?: string[] | null - updated_at?: string | null - user_metadata?: Json | null - version?: string | null - } - Relationships: [ - { - foreignKeyName: "objects_bucketId_fkey" - columns: ["bucket_id"] - isOneToOne: false - referencedRelation: "buckets" - referencedColumns: ["id"] - }, - ] - } - s3_multipart_uploads: { - Row: { - bucket_id: string - created_at: string - id: string - in_progress_size: number - key: string - owner_id: string | null - upload_signature: string - user_metadata: Json | null - version: string - } - Insert: { - bucket_id: string - created_at?: string - id: string - in_progress_size?: number - key: string - owner_id?: string | null - upload_signature: string - user_metadata?: Json | null - version: string - } - Update: { - bucket_id?: string - created_at?: string - id?: string - in_progress_size?: number - key?: string - owner_id?: string | null - upload_signature?: string - user_metadata?: Json | null - version?: string - } - Relationships: [ - { - foreignKeyName: "s3_multipart_uploads_bucket_id_fkey" - columns: ["bucket_id"] - isOneToOne: false - referencedRelation: "buckets" - referencedColumns: ["id"] - }, - ] - } - s3_multipart_uploads_parts: { - Row: { - bucket_id: string - created_at: string - etag: string - id: string - key: string - owner_id: string | null - part_number: number - size: number - upload_id: string - version: string - } - Insert: { - bucket_id: string - created_at?: string - etag: string - id?: string - key: string - owner_id?: string | null - part_number: number - size?: number - upload_id: string - version: string - } - Update: { - bucket_id?: string - created_at?: string - etag?: string - id?: string - key?: string - owner_id?: string | null - part_number?: number - size?: number - upload_id?: string - version?: string - } - Relationships: [ - { - foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey" - columns: ["bucket_id"] - isOneToOne: false - referencedRelation: "buckets" - referencedColumns: ["id"] - }, - { - foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey" - columns: ["upload_id"] - isOneToOne: false - referencedRelation: "s3_multipart_uploads" - referencedColumns: ["id"] - }, - ] - } - } - Views: { - [_ in never]: never - } - Functions: { - can_insert_object: { - Args: { - bucketid: string - name: string - owner: string - metadata: Json - } - Returns: undefined - } - extension: { - Args: { - name: string - } - Returns: string - } - filename: { - Args: { - name: string - } - Returns: string - } - foldername: { - Args: { - name: string - } - Returns: string[] - } - get_size_by_bucket: { - Args: Record - Returns: { - size: number - bucket_id: string - }[] - } - list_multipart_uploads_with_delimiter: { - Args: { - bucket_id: string - prefix_param: string - delimiter_param: string - max_keys?: number - next_key_token?: string - next_upload_token?: string - } - Returns: { - key: string - id: string - created_at: string - }[] - } - list_objects_with_delimiter: { - Args: { - bucket_id: string - prefix_param: string - delimiter_param: string - max_keys?: number - start_after?: string - next_token?: string - } - Returns: { - name: string - id: string - metadata: Json - updated_at: string - }[] - } - operation: { - Args: Record - Returns: string - } - search: { - Args: { - prefix: string - bucketname: string - limits?: number - levels?: number - offsets?: number - search?: string - sortcolumn?: string - sortorder?: string - } - Returns: { - name: string - id: string - updated_at: string - created_at: string - last_accessed_at: string - metadata: Json - }[] - } - } - Enums: { - [_ in never]: never - } - CompositeTypes: { - [_ in never]: never - } - } } type PublicSchema = Database[Extract] @@ -668,3 +313,18 @@ export type Enums< ? PublicSchema["Enums"][PublicEnumNameOrOptions] : never +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] + ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + diff --git a/yarn.lock b/yarn.lock index a7dccaf..47101e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9046,10 +9046,10 @@ sucrase@^3.32.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" -supabase@1.206.0: - version "1.206.0" - resolved "https://registry.yarnpkg.com/supabase/-/supabase-1.206.0.tgz#0f885bf54eefa03001b778c39b72b1c7e0e09c54" - integrity sha512-zfmFreYATkhWJ8fNpO4DOwz/Ebj+xpbXRM2QJz14H15MJ9rQOpF9M2XdMOjJumyg6iv1M88ekCGk6ZeUZIknnA== +supabase@1.207.9: + version "1.207.9" + resolved "https://registry.yarnpkg.com/supabase/-/supabase-1.207.9.tgz#a03a0d324413f038f2b31a021d9bc17b25488607" + integrity sha512-BJPwsAd2UBIpQawcQV3/xKHEZ8YrrkHYpgibxCZbG+RuxuhTtkHG7zR4I3LylIIEwcKp3hmDKu/hO1m2NT5RXA== dependencies: bin-links "^5.0.0" https-proxy-agent "^7.0.2"