API z bazą danych w kilka minut? – poznaj Supabase

Może jesteś developerem, który ma dość korzystania z serwisów typu MockAPI i poszukujesz narzędzia, które pozwoli stworzyć API z mającymi sens danymi dla wersji poglądowej Twojej aplikacji w kilka chwil?

A może jesteś początkującym programistą front-end, który chciałby, żeby jego projekty w portfolio wyróżniały się, a aplikacja TODO napisana w React’cie zaczęła przechowywać dane w innych miejscach, niż tylko w wewnętrznym stanie?

W obu tych przypadkach potrzebny jest Ci prosty (i najlepiej darmowy) sposób, aby nawet bez znajomości technologii back-endowych, stworzyć zaplecze dla Twojego niesamowitego projektu!

API – czyli interfejs programowania aplikacji, to nic innego jak zestaw reguł opisujący, w jaki sposób poszczególne aplikacje (lub części naszego systemu) komunikują się ze sobą.

Czym jest Supabase?

Supabase, reklamuje się jako open-source’owa alternatywa dla popularnego serwisu Firebase, o którym już mogłeś kiedyś usłyszeć. W istocie usługi oferowane przez oba te serwisy nie są identyczne, jednak dla Nas przede wszystkim liczy się to, co Supabase może dla Nas zrobić (a może naprawdę dużo!). Supabase to serwis udostępniający bazę danych Postgres, wraz z gotowym uwierzytelnianiem, przechowywaniem plików oraz automatycznie generowanym API. Dokumentacja dostępna pod tym linkiem Supabase Docs jest naprawdę obszerna i bardzo przystępnie napisana, dlatego zachęcam Cię do zajrzenia do niej i zobaczenia, co jeszcze można zrobić z wykorzystaniem tego narzędzia. Ponieważ wspomniana już wcześniej aplikacja TODO jest najczęściej wybierana, jako projekt startowy dla wielu przyszłych programistów, to właśnie na jej podstawie, przedstawię w tym artykule, w jaki sposób stworzyć bazę danych i zintegrować Supabase z aplikacją napisaną w React’cie.
Krok 1

Tworzenie projektu w Supabase

Aby rozpocząć pracę z Supabase, należy utworzyć konto. Na szczęście twórcy postawili na wygodę, dlatego bez problemu możesz zarejestrować się z użyciem swojego konta na Githubie.
Jeżeli nie masz tam konta, to zachęcam Cię do utworzenia go. Github, to podstawowe narzędzie, które pozwoli przyszłemu pracodawcy zobaczyć projekty, które tworzyłeś. Poza tym, znajomość git’a, to must have w większości firm, dlatego tym bardziej warto rozpocząć swoją przygodę z nim jak najwcześniej.

W Supabase masz możliwość tworzenia organizacji, a następnie zakładania w nich projektów. Utwórz nową organizację o dowolnej nazwie – w moim przypadku będzie to Dogtronic 🙂 – i przejdź do wybrania jej ustawień.

Wybierz z listy serwer znajdujący się najbliżej Twojej lokacji i podaj nazwę projektu oraz hasło dla swojej bazy danych. Hasło to powinno być silne i przechowywane w bezpiecznym miejscu, aby uniemożliwić innym dostęp do Twojej bazy.

Po zakończeniu konfiguracji, zobaczysz panel główny swojego projektu. Znajdziesz tu klucz publiczny dla swojego API, klucz prywatny, a także listę przykładowych projektów w wielu frameworkach i językach programowania.

Klucz prywatny (oznaczony service_role, secret) pozwala na dokonywanie dowolnych zmian w bazie danych, włącznie z usunięciem całej jej zawartości. Nie udostępniaj go nikomu!
Krok 2

Tworzenie bazy danych

W wyświetlonym panelu, pierwszy z pokazanych kafelków powinien być zatytułowany Database. Z tego miejsca możesz utworzyć nową bazę z użyciem edytora graficznego (zalecany dla początkujących) lub z edytora SQL, gdzie będziesz mógł wykonywać zapytania oraz tworzyć tabelę z użyciem języka SQL.

Masz tutaj także link do dokumentacji, do której warto wracać w momentach zacięcia się podczas pracy. Dla ułatwienia w naszym przypadku wykorzystamy edytor graficzny Table Editor.

Przy okazji, zastanówmy się, jak powinna wyglądać baza danych dla naszego projektu. Pierwszą i najważniejszą z tabel powinniśmy przeznaczyć na nasze zadania na liście. Chcielibyśmy zapewne również, aby użytkownicy mogli przy rejestracji podać swoje podstawowe dane (imię, nazwisko, może pseudonim?).

Dodatkową funkcjonalnością może być także kategoria zadania, którą użytkownik będzie mógł zdefiniować samodzielnie, aby następnie móc pogrupować swoje zadania. Zobaczmy zatem, jak taka baza mogłaby wyglądać na schemacie ERD.

W Supabase, tabela użytkowników, którzy zarejestrują się w naszej aplikacji (nazwana users) jest generowana automatycznie. Aby dodać jednak dodatkowe pola, które mogą być potrzebne w naszej aplikacji, najlepiej jest utworzyć dodatkową tabelę, a następnie powiązać ją relacją z tabelą domyślną.
Schemat ERD utworzonej bazy danych

Przy tworzeniu tabel zostanie pokazana opcja „Enable Row Level Security (RLS)”. Na ten moment proponuję pozostawić ją wyłączoną, ponieważ przygotowaniem polityk Postgres i  zabezpieczeń zajmiemy się innym razem.

Widok tworzenia tabeli
Widok dodawania klucza obcego
Widok dodawania klucza obcego
Po utworzeniu tabel, możemy sprawdzić, czy nasza baza zgadza się z przygotowanym diagramem. Można do tego wykorzystać darmowe narzędzie Supabase Schema utworzone przez @zernonia. Po podaniu linku do naszego API oraz klucza publicznego powinien pojawić się przed Tobą następujący schemat:
Schemat bazy danych wygenerowany przez Supabase Schema
Krok 3

Kodowanie

Nareszcie możemy przejść do części, na którą czekali wszyscy (a przynajmniej ja), czyli pisania kodu!

Nie tracąc czasu, otwórz swój ulubiony edytor kodu i zaczynamy!

Teraz możesz wybrać jedną z dwóch ścieżek: podstawową lub zaawansowaną

Ścieżka podstawowa

Jeżeli chcesz na potrzeby nauki korzystania z Supabase dodatkowo napisać prostą aplikację Todo, przejdź dalej do ścieżki zaawansowanej.

Jeśli jednak nie czujesz takiej potrzeby, gotową aplikację znajdziesz tutaj.

Ścieżka zaawansowana

Jeżeli jednak masz trochę więcej czasu, to zachęcam Cię do samodzielnego pisania kodu, a następnie przejścia do sekcji integracji z Supabase.

Omówmy zatem, co znajduje się w przykładowej aplikacji Todo. Składa się ona z następujących komponentów:

  • Todo
  • TodoList
  • App
  • Login

Póki co ich funkcja jest czysto prezentacyjna, dlatego nie ma w nich nic specjalnego.

Ważniejszy jest natomiast plik supabaseClient.ts. To on pozwoli nam na połączenie się z naszym API poprzez utworzony obiekt klienta. W katalogu głównym możesz zauważyć również plik .env, w którym przechowywany będzie URL do naszego API, a także klucz publiczny.

Zanim zaczniemy, z panelu ustawień Supabase (ikona koła zębatego po lewej stronie) wybierz zakładkę API i skopiuj swój adres URL i klucz publiczny.

import { createClient } from '@supabase/supabase-js'

 const supabaseUrl:string = process.env.REACT_APP_SUPABASE_URL!
 const supabaseAnonKey:string = process.env.REACT_APP_SUPABASE_ANON_KEY!

 export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Panel ustawień Supabase - API
Panel ustawień Supabase - API
Następnie umieść w odpowiednich miejscach skopiowane dane w pliku .env. Możesz teraz uruchomić projekt wywołując:
npm start
Krok 4

Logowanie

Na początku zajmijmy się panelem logowania. W pliku Login.tsx mamy wszystko, co będzie nam potrzebne, aby zalogować się do aplikacji za pomocą „magicznego linku”, czyli po prostu łącza, które wysłane zostanie na wskazany przez nas adres email w panelu logowania.

Dodajmy zatem obsługę wysłania formularza. Wewnątrz funkcji handleLogin zastąpmy wywołanie alerta tak, aby uzyskać:

const handleLogin = async (e) => {  
    e.preventDefault()  
  
    try {  
        setLoading(true)  
        const { error } = await supabase.auth.signIn({ email })  
        if (error) throw error  
            alert('Check your email for the login link!')  
    } catch (error:any) {  
        alert(error.error_description || error.message)  
    } finally {  
        setLoading(false)  
    }  
}
  1. Jeżeli klikniesz teraz na przycisk, zobaczysz inny komunikat.
LocalhostP:3000 - Komunikat 1

2. Wprowadź swój adres email i kliknij ponownie przycisk, po chwili powinieneś zobaczyć następujący alert:

LocalhostP:3000 - Komunikat 1
LocalhostP:3000 - Komunikat 2

Jeżeli sprawdzisz swoją skrzynkę pocztową, powinieneś zobaczyć maila z prośbą o potwierdzenie. Po kliknięciu w link zostaniesz przekierowany na swoję stronę. Możesz także sprawdzić, w zakładce Authentication w panelu Supabase, że twój mail pojawił się w tabeli. Oznacza to, że zostałeś zarejestrowany!

Mamy już działającą możliwość zalogowania się do naszej aplikacji, jednakże po przekierowaniu z magicznego linku nic się nie dzieje i znów widzimy panel logowania. Musimy zatem dodać logikę, która pozwoli nam sprawdzić, czy użytkownik wchodzący na stronę jest zalogowany.

Supabase posługuje się JWT (JSON Web Token) w celu uwierzytelnienia użytkownika. Token ten jest dodawany do każdego zapytania wysłanego do API i pozwala sprawdzić po stronie serwera, czy osoba ma dostęp do oczekiwanego zasobu.

 

Jeżeli chcesz zobaczyć, jak wygląda wygenerowany token, w narzędziach deweloperskich swojej przeglądarki wejdź w zakładkę Aplikacja > Pamięć lokalna > localhost i znajdź rekord supabase.auth.token.

W pliku App.tsx potrzebujemy stanu, który pozwoli nam przechowywać aktualną sesję, a także funkcji, która pozwoli zaktualizować ten stan przy pierwszym uruchomieniu aplikacji. Skorzystamy zatem z dwóch podstawowych hook’ów oferowanych przez React’a, czyli useState oraz useEffect. Dodajmy zatem następujący kod:

const [session, setSession] = useState<Session | null>(null)

 useEffect(() => {
    setSession(supabase.auth.session())
    supabase.auth.onAuthStateChange((_event, session) => {
        setSession(session)
    });
 }, []);

W ten sposób, podczas pierwszego renderowania naszej aplikacji zapisana zostanie aktualna sesja. o ile użytkownik jest zalogowany. Następnie, wykorzystując dobrodziejstwo Supabase, w prosty sposób dodajemy listenner, który uważać będzie na zmiany stanu autoryzacji.

Dzięki temu, w przypadku np. wygaśnięcia tokenu, automatycznie zaktualizowana zostanie zmienna stanu przechowująca sesję.

Ostatnim krokiem jest dodanie logiki, która w zależności od tego, czy użytkownik jest zalogowany, pokaże mu panel logowania lub listę jego zadań. Wewnątrz metody render dodajemy zatem:

{!session ? <Login /> : <TodoList/>}
Jeżeli spojrzysz teraz na aplikację, zobaczysz, że zostaliśmy przeniesieni do listy zadań. Jest to znak, że konfiguracja przebiegła prawidłowo.  Jeżeli w którymś miejscu zgubiłeś się, możesz sprawdzić gotowy kod dla tego etapu na Githubie.
Krok 5

Wyświetlanie zadań

Możemy się już zalogować, ale póki co widzimy po prostu szablonowe zadania, które nie do końca nas przekonują. Dodajmy zatem teraz możliwość oglądania własnych zadań.

Jeżeli w panelu Supabase przejdziesz do zakładki API, zobaczysz coś, co jest jedną z największych zalet oferowanych przez ten serwis – automatyczna dokumentacja API! To właśnie dzięki temu wdrożenie Supabase do swojego projektu jest szybkie i bezbolesne, nawet bez zaawansowanej znajomości JavaScript’a.

Wykorzystajmy zatem to dobrodziejstwo i przygotujmy funkcję, która pobierze listę naszych zadań, a następnie wyświetli je w formie komponentów Task. Nasza lista powinna być pobierana przy pierwszym renderowaniu listy zadań, dlatego w komponencie TaskList dodajmy hook useEfect.

W jego wnętrzu umieśćmy kod funkcji pozwalającej na pobieranie listy zadań, który znajdziesz w zakładce API przechodząc do Tables and Views > Todo. Kod ten powinien wyglądać następująco:

Zwróćmy jednak uwagę, że powyższe polecenie pozwala na pobranie wszystkich zadań, niezależnie od użytkownika. Aby temu zaradzić, wykorzystajmy jedną z wbudowanych metod-filtrów Supabase – eq.

Pozwala ona na wskazanie w zapytaniu jednej z kolumn naszej tabeli oraz wartości, którą powinny posiadać pobierane rekordy w danej kolumnie. Jeżeli miałeś już styczność z językiem SQL, to możesz spojrzeć na tę metodę, jak na klauzulę WHERE w zapytaniu do bazy danych z użyciem SELECT. 

W naszym przypadku chcemy, aby użytkownik miał możliwość zobaczenia wyłącznie swoich własnych zadań, a na to właśnie wskazuje kolumna user_id, przechowująca id użytkownika dodającego zadanie. Nasza funkcja powinna mieć zatem postać:

let { data: Todo, error } = await supabase
    .from('Todo')
    .select('*')
Więcej o wbudowanych metodach Supabase pozwalających na wykonywanie bardziej skomplikowanych zapytań możesz przeczytać w dokumentacji: Używanie filtrów
const { data: Todos, error } = await supabase
    .from('Todo')
    .select('*')
    .eq('user_id', userId)

Możesz teraz pomyśleć „no dobrze, ale skąd będziemy wiedzieć, jakie jest id zalogowanego użytkownika?”. Nie trzymając Cię w niepewności już spieszę z odpowiedzią. 

Tutaj również mamy już gotowe rozwiązanie dla tego problemu. Supabase oferuje metodę pozwalającą na pobranie obiektu zalogowanego użytkownika ze wszystkimi jego danymi, w tym z jego id. Metoda ta ma postać supabase.auth.user() i zwraca obiekt klasy User, którego część z atrybutów możesz sprawdzić poniżej:

export interface User {

    id: string
    app_metadata: {
        provider?: string
        [key: string]: any
    }
    user_metadata: {
        [key: string]: any
    }
    
    /*...*/
}

Po dodaniu pobierania id użytkownika nasza funkcja ma póki co postać:

const userId = supabase.auth.user()!.id

 const { data: Todos, error } = await supabase
    .from('Todo')
    .select('*')
    .eq('user_id', userId)

Znak „!” na końcu metody user() to post-fixowy operator Non-null assertion (nie będę nawet próbował przetłumaczyć tego na polski, żeby bardziej nie kaleczyć języka :P).

 

Wskazuje on kompilatorowi TypeScript, że jego operand ma wartość inną niż null i undefined. Jeżeli piszesz w JavaScripcie, nie dodawaj go tam. Po kompilacji z TS do JS jest on usuwany, więc jest on potrzebny jedynie w przypadku programowania w TS.

Wszystko wygląda świetnie! Teraz pozostało jedynie opakować nasz kod w blok try-catch oraz dodać zapisywanie pobranych danych do wewnętrznego stanu naszej aplikacji. Dodaj dwa stany – todos (typu ITodo[] – plik models.ts) oraz loading (typ boolean), a następnie dodaj odpowiednio te funkcjonalności w następujący sposób:

try {
    setLoading(true);
    const userId = supabase.auth.user()!.id;
    const { data: Todos, error, status } = await supabase
        .from('Todo')
        .select('*')
        .eq('user_id', userId);

    if (error && status !== 406) {
        throw error;
    }
    
    if (Todos) {
        setTodos(Todos);
    }

 }
 catch (error:any) {
    alert(error.message);
 }
 finally {
    setLoading(false);
 }

Po umieszczeniu powyższego kodu w funkcji (asynchronicznej!) a następnie wywołaniu jej w hook’u useEffect, początek naszego komponentu powinien wyglądać następująco:

const [todos,setTodos] = useState<ITodo[]>([]);
 const [loading,setLoading] = useState(true);

 useEffect(() => {
    getTasks();
 },[]);

 const getTasks = async () => {
    try {
        setLoading(true);
        const userId = supabase.auth.user()!.id;
        const { data: Todos, error, status } = await supabase
            .from('Todo')
            .select('*')
            .eq('user_id', userId);

        if (error && status !== 406) {
            throw error;
        }

        if (Todos) {
            setTodos(Todos)
        }
    }
    catch (error:any) {
        alert(error.message)
    }
    finally {
        setLoading(false)
    }
 }

Teraz możemy wykorzystać pobrane dane i wyświetlić je w postaci komponentów Todo korzystając z funkcji map:

{loading ?
    <div>Loading...</div> :
    <ul className="todo-list">
        {todos.map((todo) => <Todo/>)}
    </ul>
 }

Póki co nie widzimy żadnych zadań, gdyż żadnego jeszcze nie dodaliśmy. Możemy dodać zadanie bezpośrednio z panelu Supabase. Wejdź w zakładkę Authentication i skopiuj swoje id użytkownika.

Ponieważ zadanie powiązane jest z daną kategorią, musimy dodać również naszą pierwszą kategorię. W zakładce Table Editor wybierz tabelę Category i kliknij Insert row. W nowym panelu wprowadź dane dla nowej kategorii i wklej swoje id użytkownika:

Table Editor -> Category -> Insert row

W ten sam sposób dodaj pierwsze zadanie. Pamiętaj, żeby wskazać odpowiednią kategorię, a także podać swoje id użytkownika:

Table Editor -> To do -> Insert row
Table Editor -> To do -> Insert row

Jeżeli wszystko przebiegło poprawnie, powinniśmy zobaczyć dokładnie jedno zadanie. Dalej nie jest ono jednak wypełnione naszymi danymi, dlatego musimy przekazać pobrane dane do komponentu zadania. Przygotujmy zatem ten komponent:

import { FunctionComponent } from "react";
 import { ITodo } from "../models";

 interface TodoProps {
    todo: ITodo
 }

 const Todo: FunctionComponent<TodoProps> = ({todo}) => {

    return (
        <li className="todo">
            <p className="todo-date">{todo?.created_at}</p>
            <p className="todo-name">{todo?.name}</p>
            <p className="todo-date">Category: <span>{todo?.category_id}</span></p>
            <p className="todo-desc">{todo?.description}</p>
            <div className="btn-container">
                <button className="btn">Delete</button>
                <button className="btn">Edit</button>
            </div>
        </li>
    );
 }

 export default Todo;

Przekażmy do niego obiekt todo w metodzie map wraz z podaniem id zadania jako klucz:

<ul className="todo-list">
    {todos.map((todo) => <Todo key={todo.id} todo={todo}/>)}
 </ul>

W naszej aplikacji już widać dodane wcześniej zadanie!

Widok zadania w aplikacji
Widok zadania w aplikacji

Ostatnim elementem wymagającym zmiany w komponencie Todo jest poprawienie sposobu wyświetlania kategorii. Aktualnie pokazujemy jedynie jej id, a chcielibyśmy, żeby użytkownik miał możliwość zobaczenia jej nazwy.

Krok 6

Wyświetlanie kategorii

Aby to zrobić musimy zmodyfikować naszą funkcję pobierającą zadania, a dokładniej jej metodę select(). Aby odwołać się w niej do danych z innej, powiązanej relacją tabeli oraz pobrać je, należy podać jej nazwę oraz wskazać w nawiasach, jaka dokładnie kolumna nas interesuje. Składnia tego polecenia po zmianach powinna zatem wyglądać następująco:

.select(
    `*,
    Category (
        name
    )
 `)

Jeśli wyświetlisz w konsoli pobraną listę zadań zobaczysz, że teraz nasz obiekt posiada pole Category zawierające obiekt z polem name.

Widok pobranej listy w narzędziach deweloperskich

Poprawmy zatem nasz komponent Todo oraz, jeżeli korzystasz z mojego szablonu lub piszesz w TypeScript’cie, dodajmy pole Category do interfejsu przygotowanego dla naszych zadań (w szablonie wszystkie interfejsy dla obiektów przygotowane są w pliku models.ts)

<p className="todo-date">Category: <span>{todo?.Category.name}</span></p>
export interface ITodo {
    id: number,
    name: string,
    created_at?: string,
    description?: string,
    category_id: number,
    user_id: number,
    Category: {
        name:string
    }
}

Teraz nasze zadanie wygląda tak jak powinno.

Widok zadania w aplikacji
Widok zadania w aplikacji
Krok 7

Dodawanie zadań

Kolejną funkcjonalnością naszej aplikacji jest dodawanie zadań. W komponencie TodoList mamy już przygotowany do tego formularz, ale musimy jeszcze zapewnić jego implementację i powiązanie z naszą bazą.

Aby dodawać rekordy do bazy Supabase zapewnia metodę insert. Możesz zobaczyć sposób jej użycia wchodząc w zakładkę API i wybierając tabelę Todo. Korzystając z tej wiedzy, a także poprzedniej funkcji pobierającej zadania napiszmy funkcję dodająca zadanie do bazy:

const saveTask = async () => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .insert([{
                name: taskName,
                description: taskDescription,
                category_id: taskCategoryId,
                user_id: userId
            }]);

        if (error) {
            throw error;
        }
        alert('Task successfully added')
    }
    catch (error:any) {
        alert(error.message)
    }
 }

Teraz musimy przygotować stany dla poszczególnych pól formularza oraz połączyć je z nimi. Mamy 3 pola, zatem potrzebne są nam 3 stany:

const [taskName,setTaskName] = useState('');

 const [taskCategoryId,setTaskCategoryId] = useState('');

 const [taskDescription,setTaskDescription] = useState('');

Po podłączeniu pól dodajmy jeszcze naszą metodę saveTask, do funkcji obsługującej wysłanie formularza handleSubmit.

const handleSubmit = (e:FormEvent) => {
    e.preventDefault()
    if(taskCategoryId === ''){
        alert('You have to choose some category')
        return;
    }
    saveTask()
 }

Wszystko byłoby idealnie, gdyby nie fakt, że dalej nie mamy możliwości wybrania kategorii dla naszego zadania, gdyż nie pobieramy jeszcze z bazy żadnej z nich. Aby się tym zająć, analogicznie do metody pobierającej zadania napiszmy metodę do pobierania kategorii i dodajmy jej wywołanie w hook’u useEffect.

const getCategories =async () => {
    try {
        const userId = supabase.auth.user()!.id;
        const { data: Categories, error, status } = await supabase
            .from('Category')
            .select(`*`)
            .eq('user_id', userId);

        if (error && status !== 406) {
            throw error;
        }

        if (Categories) {
            setCategories(Categories);
        }
    }
    catch (error:any) {
        alert(error.message)
    }      
 }

Mając już nasze dane, możemy zaktualizować pole select formularza tak, aby wyświetlało ono wszystkie pobrane kategorie.

<select
    name="task-category"
    id="task-category"
    value={taskCategoryId}
    onChange={(e) => setTaskCategoryId(e.target.value)}
 >
    <option value="">--Please choose an option--</option>
    {categories.map((category) =>{
        return (
            <option 
                key={category.id} 
                value={category.id}>
                    {category.name}
            </option>
    )})}
 </select>

Przetestujmy zatem dodanie nowego zadania. Po wysłaniu formularza, w panelu Supabase możemy sprawdzić, że pojawił się nowy rekord w tabeli Todo:

Widok nowego zadania w panelu Supabase

Na koniec, dla lepszego efektu, dodajmy jeszcze czyszczenie pól formularza po jego wysłaniu poprzez ustawienie ich wartości na puste stringi w metodzie saveTask.

Krok 8

Edycja i usuwanie zadań

/*...*/
 alert('Task successfully added');

 setTaskCategoryId('');
 setTaskDescription('');
 setTaskName('');

Sporo pracy za nami, ale została nam jeszcze jedna funkcjonalność. Musimy umożliwić użytkownikowi edycję oraz usuwanie zadań. Zajmijmy się najpierw tym drugim. Dodajmy następującą metodę do naszego komponentu TodoList:

const deleteTask = async (taskId:number) => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .delete()
            .eq('id', taskId)
            .eq('user_id',userId);

        if (error) {
            throw error;
        }

        alert('Task successfully deleted');
    }
    catch (error:any) {
        alert(error.message);
    }
 }

Metoda ta korzysta z udostępnionej przez Supabase metody delete, która pozwala na usuwanie rekordów z bazy danych. Zauważ, że filtr eq został wykorzystany dwa razy. Łączenie filtrów jest możliwe i pozwala na tworzenie znacznie bardziej skomplikowanej logiki podczas wykonywania operacji na bazie danych.

Musimy jeszcze przekazać tę metodę do naszego komponentu Todo, a także dodać obsługę kliknięcia przycisku „Delete”. Tym sposobem nasz komponent Todo wygląda następująco:

interface TodoProps {
    todo: ITodo,
    deleteTask: (taskId: number) => void
}

 const Todo: FunctionComponent<TodoProps> = ({todo,deleteTask}) => {

    return (
        <li className="todo">
            <p className="todo-date">{todo?.created_at}</p>
            <p className="todo-name">{todo?.name}</p>
            <p className="todo-date">Category: <span>{todo?.Category.name}</span></p>
            <p className="todo-desc">{todo?.description}</p>
            <div className="btn-container">
                <button className="btn" 
                    onClick={() => deleteTask(todo.id)}
                >
                    Delete
                </button>
                <button className="btn">Edit</button>
            </div>
        </li>
    );
 }

 export default Todo;

Ostatnią rzeczą jest obsługa edycji naszych zadań. Wykorzystamy do tego zadania posiadany formularz dodawania zadań, jednak aby to zrobić potrzebujemy stanu, który pozwoli określić czy dane przechowywane w jego polach dotyczą zadania już istniejącego, czy może raczej nowego zadania. Stan ten będzie jednocześnie przechowywał informację o id zadania, które edytujemy:

const [editedTaskId,setEditedTaskId] = useState<number | null>(null);

W metodzie handleSubmit wykorzystajmy go do warunkowego wybierania odpowiednio funkcji edycji lub funkcji dodania zadania:

const handleSubmit = (e:FormEvent) => {
    e.preventDefault();

    if(taskCategoryId === ''){
        alert('You have to choose some category');
        return;
    }

    if(editedTaskId)
        editTask(editedTaskId);
    else
        saveTask();
 }

Korzystając dalej z dokumentacji API na Supabase napiszmy odpowiednią metodę, która będzie analogiczna do metody saveTask, ale wykorzystywać będzie metodę update zapewnioną przez Supabase. Metoda ta pozwoli na zapisanie zmian w naszym zadaniu.

const editTask = async (taskId:number) => {
    try {
        const userId = supabase.auth.user()!.id;
        const { error } = await supabase
            .from('Todo')
            .update({
                name: taskName,
                description: taskDescription,
                category_id: taskCategoryId,
            })
            .eq('id', taskId)
            .eq('user_id',userId);

        if (error) {
            throw error;
        }

        alert('Task successfully edited');

        setTaskCategoryId('');
        setTaskDescription('');
        setTaskName('');

        setEditedTaskId(null);
        }
    catch (error:any) {
        alert(error.message);
    }
 }
Więcej o metodzie delete możesz przeczytać tutaj.
Teraz potrzebujemy jeszcze możliwości wskazania edytowanego zadania poprzez kliknięcie przycisku Edit danego zadania, a także możliwości umieszczenia danych w polach formularza. Napiszmy metodę handleEdit, którą umieścimy w komponencie TodoList.
const handleEdit = (taskId:number) => {
    setEditedTaskId(taskId);
    const editedTask = todos.find((todo) => todo.id == taskId)!
    setTaskCategoryId(editedTask.category_id.toString())
    setTaskDescription(editedTask.description ? editedTask.description : '')
    setTaskName(editedTask.name)
 }

Przekażmy tę metodę do komponentu Todo i wywołajmy ją w przypadku kliknięcia w przycisk Edit:

<button className="btn" onClick={() => handleEdit(todo.id)}>Edit</button>

Możesz teraz już przetestować edytowanie zadań.

Krok 9

Ostatnie szlify

Wszystko działa już jak należy, ale możliwe, że zwróciłeś uwagę na fakt, że po edycji/usunięciu zadania, lista zadań nie aktualizuje się. Musimy poprawić ten fakt tak, aby wszystko działo się dynamicznie. Możesz pomyśleć, że można po prostu wywołać zapytanie pobierające listę zadań za każdym razem, gdy następuje jakaś zmiana.

To rozwiązanie dałoby poprawny rezultat, jednak w przypadku, gdybyśmy mieli miliony rekordów, byłoby to bardzo nieoptymalne. Dlatego też lepiej jest pracować bezpośrednio na liście, którą już pobraliśmy przy pierwszym renderowaniu komponentu. Poprawmy zatem nasze metody editTask, deleteTask oraz saveTask tak, aby aktualizowały one zmienną stanu todos w przypadku poprawnego wykonania.

  • editTask
setTodos(todos.map(todo =>
    todo.id === taskId ?
    {...todo,
        name: taskName,
        category_id: parseInt(taskCategoryId),
        description: taskDescription,
        Category: {
            name: categories.find(category => 
                category.id === parseInt(taskCategoryId))?.name!
    }} :
    todo))
  • deleteTask
setTodos(todos.filter((todo) => todo.id !== taskId))
  • saveTask
const newTodo:ITodo = {...Todo[0], 
    Category: {
        name: categories.find(category => 
            category.id === parseInt(taskCategoryId))?.name!
    }
 }

 setTodos([...todos,newTodo])

Mamy już w pełni funkcjonalną, dynamiczną aplikację. Skoro zbliżamy się do końca tego artykułu, dobrze byłoby móc wylogować się z aplikacji po zakończonej pracy. Dodajmy zatem metodę, która to umożliwi. W tym przypadku również mamy praktycznie gotowe rozwiązanie dla tego problemu, dzięki metodzie supabase.auth.signOut() zapewnianej przez Supabase.

const logOut = async () => {
    try{
      const { error } = await supabase.auth.signOut();

      if(error)
        throw error;
    }
    catch(error){
      alert('This action is unavailable right now');
    }
 }

Dodajmy tę metodę do obsługi kliknięcia przycisku z nagłówka naszej aplikacji oraz zapewnijmy, że przycisk będzie widoczny tylko dla zalogowanych użytkowników:

{session && <button className='btn' onClick={logOut}>Log out</button>}
Krok 10

Trochę samodzielnej pracy

Możliwe, że zauważyłeś, że w projekcie nie ma komponentu umożliwiającego dodawanie nowych typów zadań z poziomu aplikacji. Jest to zabieg celowy (wcale nie chodzi o moje lenistwo, przysięgam), żeby dać Ci możliwość sprawdzenia swoich sił w samodzielnej pracy z Supabase.

Skorzystaj z wiedzy zawartej w tym poradniku, a także z automatycznej dokumentacji z panelu Supabase i spróbuj przygotować tę funkcjonalność samodzielnie :). Jeżeli chcesz, możesz dodać nowy formularz w komponencie z listą zadań, albo stworzyć zupełnie nowy komponent, specjalnie dla tej funkcjonalności — możliwości jest mnóstwo!

Podsumowanie

Mam nadzieję, że udało mi się przybliżyć Ci podstawy pracy z Supabase. Oczywiście wszystko, co udało mi się zawrzeć w tym artykule, to dopiero wierzchołek góry lodowej, a możliwości tego serwisu są przeogromne. Jeszcze raz zachęcam Cię do zgłębienia dokumentacji Supabase i dalszej pracy nad poznawaniem coraz bardziej zaawansowanych sposobów użycia tego narzędzia.
Adam Lipiński

Za dnia początkujący front-end developer, w nocy zaś student (czasami na odwrót). Fan testowania nowych technologii oraz CSSowych sztuczek.

Zostaw komentarz:

Witryna jest chroniona przez reCAPTCHA i Google Politykę Prywatności oraz obowiązują Warunki Korzystania z Usługi.