marmelab / ra-supabase

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

Supabase Storage file saving support #51

Open kav opened 5 months ago

kav commented 5 months ago

I know this is already on the list. Adding an issue to discuss what to expose exactly. Something like the following could be pushed into the provider.

We'll likely want to offer a function to get a bucket name from a record/field/file. Further the below uses public urls which is probably not ideal but alternatives might require a SignedImageField. We also might want to offer the opportunity to remove previous files as new ones are uploaded.

Any thoughts on all this before I carry anything further?

export const dataProvider = withLifecycleCallbacks(
  supabaseDataProvider({
    instanceUrl: REACT_APP_SUPABASE_URL,
    apiKey: REACT_APP_SUPABASE_KEY,
    supabaseClient,
  }),
  [
    {
      resource: "*", // Note * support is unreleased so we'll want to wait on that
      beforeSave: async (data: any, dataProvider: any) => {
        const newFiles = (
          await Promise.all(
            Object.keys(data)
              .filter((key) => data[key]?.rawFile instanceof File)
              .map((key) => [key, data[key]])
              .map(async ([key, file]: any) => {
                const { data, error } = await supabaseClient.storage
                  .from("<bucket name>")
                  .upload(file.rawFile.name, file.rawFile)
                if (error) throw error
                const path = `${REACT_APP_SUPABASE_URL}/storage/v1/object/public/<bucket name>/${data?.path}`
                return { [key]: path }
              })
          )
        ).reduce((acc, val) => ({ ...acc, ...val }), {})
        return { ...data, ...newFiles }
      },
    },
  ]
)
slax57 commented 4 months ago

Hi,

Thank you so much for this suggestion, and for offering to work on this feature! :pray:

Further the below uses public urls which is probably not ideal but alternatives might require a SignedImageField.

True. Creating a SignedImageField component would certainly be a valid option. I guess it would take a supabaseClient prop? Alternatively, we could also call supabase.storage.from('bucket').createSignedUrl('private-document.pdf', 3600) and directly return a time-limited URL.

In any case, for a first version of the implementation, it's OK to support only public URL for now IMO.

We also might want to offer the opportunity to remove previous files as new ones are uploaded.

It would be very nice indeed, but it would go even further than what we documented in the OSS docs and, again, for a first version, this is not a requirement IMHO.

Note * support is unreleased so we'll want to wait on that

* support is scheduled for RA v5 if I'm not mistaken, so it will probably take some more weeks to be available. In the docs we can simply give examples with resource: "posts" and people will probably figure out how to generalize that when the * feature is available.

Now a question of my own: what API do you have in mind for that feature? Something like this?

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

export const dataProvider = withLifecycleCallbacks(
  supabaseDataProvider({
    instanceUrl: REACT_APP_SUPABASE_URL,
    apiKey: REACT_APP_SUPABASE_KEY,
    supabaseClient,
  }),
  [
    {
      resource: "posts",
      beforeSave: async (data: any, dataProvider: any) => {
        const newFiles = await storeInSupabase({
          supabaseClient,
          bucket: 'my-bucket',
          data,
        });
        return { ...data, ...newFiles }
      },
    },
  ]
)

(not very found of the name storeInSupabase but can't find a better one right now...)

In any case, a PR implementing file storage with supabase would be most welcome, and the implementation you suggest seems like a very promising start!