marmelab / ra-supabase

Supabase adapter for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services.
MIT License
156 stars 27 forks source link

RPC Functions #64

Closed griiettner closed 4 months ago

griiettner commented 4 months ago

I'm trying to find if is possible to call Supabase RPC functions. For example, when creating a data in one of tables, it need to update data on another table at the same time, so I created a RPC function that perform all the tasks that need to be done and I'm not looking to rewrite the logic in JavaScript, since it is very complicated. My hope is that I can call the this RPC function every time I create a new data row on my database.

In reality, I have several RPC functions, I use it heavily, for filtering data, generating reports, applying changes to existing data and so on.

I the documentation I could not find anything that pointed to RPC on the dataProvider, only on the filter part.

Thanks for the amazing framework.

djhi commented 4 months ago

Thanks for the kind words! You have multiple options here:

import { supabaseDataProvider } from 'ra-supabase';
import { supabaseClient } from './supabase';

const baseDataProvider = supabaseDataProvider({
    instanceUrl: 'YOUR_SUPABASE_URL',
    apiKey: 'YOUR_SUPABASE_ANON_KEY',
    supabaseClient
});

export const dataProvider = {
  ...baseDataProvider,
  myCustomMethod: async () => {
  }
}
griiettner commented 4 months ago

Ok, thanks for the response, by looking to the documentation, I came up with another solution, so can still use the Guesser layouts.

Here is a sample implementation

import { supabaseDataProvider } from "ra-supabase";
import { supabase } from "./supabase";
import {
  GetListParams,
  GetListResult,
  PaginationPayload,
  SortPayload,
  DataProvider,
} from "react-admin";
import { PostgrestSingleResponse } from "@supabase/supabase-js";

const baseDataProvider = supabaseDataProvider({
  instanceUrl: import.meta.env.VITE_SUPABASE_URL as string,
  apiKey: import.meta.env.VITE_SUPABASE_ANON_KEY as string,
  supabaseClient: supabase,
});

export const dataProvider: DataProvider = {
  ...baseDataProvider,
  getList: async (
    resource: string,
    params: GetListParams
  ): Promise<GetListResult> => {
    if (!resource.startsWith("@")) {
      return baseDataProvider.getList(resource, params);
    } else {
      const cleanedResource = resource.slice(1);
      const { page, perPage }: PaginationPayload = params.pagination || {
        page: 1,
        perPage: 10,
      };
      const { field, order }: SortPayload = params.sort || {
        field: "id",
        order: "ASC",
      };
      const rangeFrom = (page - 1) * perPage;
      const funcName = `getlist${cleanedResource.toLowerCase()}`;

      try {
        // Call the RPC function
        const response: PostgrestSingleResponse<any[]> = await supabase
          .rpc(funcName, params.filter)
          .order(field, { ascending: order === "ASC" })
          .range(rangeFrom, rangeFrom + perPage - 1);

        if (response.error) {
          throw new Error(response.error.message);
        }

        if (!response.data) {
          throw new Error("Received null data from Supabase");
        }

        const data: any[] = response.data;
        return {
          data,
          total: response.count ?? data.length, // Adjust based on actual total count logic
        };
      } catch (error) {
        console.error("Error calling Supabase RPC function:", error);
        throw error; // Re-throw the error to propagate it
      }
    }
  },
};

In the case above, when calling the Resource component, I just add the @ symbol at the beginning of the name field. You notice that I remove the @ symbol on my dataProvider implementation.

Everything should be named accordingly, for example, if the resource name is products, I have to do this

<Resource
      name="@products"
      list={ListGuesser}
      edit={EditGuesser}
      show={ShowGuesser}
    />

The name of my RPC function need to follow the convention as well and it should be getlistproducts.

In my case, I created RPC functions to getList, getOne, getMany, create, update, updateMany, delete, deleteMany.

I made sure my RPC function takes care of all the scenarios listed on the documentation as well.

interface GetListParams {
    pagination: { page: number, perPage: number };
    sort: { field: string, order: 'ASC' | 'DESC' };
    filter: any;
    meta?: any;
    signal?: AbortSignal;
}
djhi commented 4 months ago

Ah I see, I thought you wanted RPC functions for custom calls not following the usual CRUD conventions. :+1: