From 2a4de0188af28423821a8be5f034edf41ea0eabc Mon Sep 17 00:00:00 2001 From: geoguide Date: Thu, 13 Feb 2025 14:59:29 -0800 Subject: [PATCH 1/2] Improve storage documentation --- docs/pages/storage/mutations.mdx | 214 ++++++++++++++++------- docs/pages/storage/queries.mdx | 289 +++++++++++++++---------------- 2 files changed, 290 insertions(+), 213 deletions(-) diff --git a/docs/pages/storage/mutations.mdx b/docs/pages/storage/mutations.mdx index ac9638d3b..0bcab0fd7 100644 --- a/docs/pages/storage/mutations.mdx +++ b/docs/pages/storage/mutations.mdx @@ -1,22 +1,70 @@ import { Callout, Tabs } from 'nextra/components'; import { LinkedTabs } from '@/components/linked-tabs'; -# Mutations +# Storage Mutations -The cache helpers query hooks wrap the mutation hooks of the cache libraries and automatically revalidate the relevant queries across your app. For example, if you list all files in `dirname/` with `useDirectory`, and upload a new file into `dirname/file.jpg`, the query is revalidated after the upload succeeded. The same goes for file removals. +The **Supabase Cache Helpers** provide optimized mutation hooks for interacting with Supabase Storage using **React Query** or **SWR**. These hooks automatically revalidate related queries upon execution, ensuring a seamless cache update when uploading or removing files. -## `useUpload` +--- -Upload a list of files. Accepts `File[]`, `FileList` and `{ data: ArrayBuffer; type: string; name: string }` objects. The latter is primarily useful for uploading from React Native. By default, the path to which the file is uploaded to is computed with +### **Mutation Configuration Options** ```ts -const defaultBuildFileName: BuildFileNameFn = ({ path, fileName }) => - [path, fileName].filter(Boolean).join("/"); +import { UseMutationOptions } from '@tanstack/react-query'; + +type MutationConfig = Omit< + UseMutationOptions, + 'mutationFn' +>; +``` + +These options are available for all mutations: + +| Option | Type | Description | +|---------------|---------------------------------------------------------------------|-------------| +| `onMutate` | `(variables: TVariables) => Promise \| TData` | Called before the mutation function executes. Can return a context value used in `onError` or `onSettled`. | +| `onError` | `(error: TError, variables: TVariables, context?: unknown) => void` | Called if the mutation fails. | +| `onSuccess` | `(data: TData, variables: TVariables, context?: unknown) => void` | Called if the mutation succeeds. | +| `onSettled` | `(data?: TData, error?: TError, variables?: TVariables, context?: unknown) => void` | Called when mutation finishes, regardless of success or failure. | +| `retry` | `boolean \| number \| (failureCount: number, error: TError) => boolean` | Number of retry attempts before failing. | +| `retryDelay` | `(failureCount: number, error: TError) => number \| number` | Delay between retries. | +| `mutationKey` | `unknown` | A unique key to track the mutation state. | +| `meta` | `Record` | Additional metadata to associate with the mutation. | + +These options apply to: + +- `useUpload` +- `useRemoveDirectory` +- `useRemoveFiles` + +For more details, refer to the [React Query `useMutation` documentation](https://tanstack.com/query/latest/docs/react/reference/useMutation). + +--- + +## **Uploading Files** + +### `useUpload` + +Uploads a **list of files** to a specified directory in Supabase Storage. + +#### **Parameters:** +```ts +useUpload( + fileApi: StorageFileApi, // Supabase storage API instance + config?: UploadFetcherConfig & UseMutationOptions +) ``` -A custom `BuildFileNameFn` can be passed to `config.buildFileName`. +#### **`UseUploadInput` Options:** +```ts +type UseUploadInput = { + files: FileList | (File | FileInput)[]; // Files to upload + path?: string; // Optional storage path +}; +``` - +#### **Example Usage:** + ```tsx import { useUpload } from '@supabase-cache-helpers/storage-swr'; @@ -27,17 +75,18 @@ A custom `BuildFileNameFn` can be passed to `config.buildFileName`. process.env.SUPABASE_ANON_KEY ); - const dirName = 'my-directory'; - function Page() { const { trigger: upload } = useUpload( client.storage.from('private_contact_files'), { buildFileName: ({ fileName, path }) => `${dirName}/${path}/${fileName}` } ); - return
...
; + + function handleUpload(files) { + upload({ files, path: 'user-123' }); + } + } ``` -
```tsx @@ -45,110 +94,143 @@ A custom `BuildFileNameFn` can be passed to `config.buildFileName`. import { createClient } from '@supabase/supabase-js'; const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY + process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY ); - const dirName = 'my-directory'; - function Page() { const { mutateAsync: upload } = useUpload( - client.storage.from('private_contact_files'), + client.storage.from('uploads'), { buildFileName: ({ fileName, path }) => `${dirName}/${path}/${fileName}` } ); - return
...
; + + async function handleUpload(files) { + await upload({ files, path: 'user-123' }); + } } ``` -
-## `useRemoveDirectory` +--- -Remove all files in a directory. Does not delete files recursively. +## **Removing a Directory** + +### `useRemoveDirectory` + +Removes all files **inside a directory** but does **not** delete subdirectories recursively. + +#### **Parameters:** +```ts +useRemoveDirectory( + fileApi: StorageFileApi, // Supabase storage API instance + config?: UseMutationOptions +) +``` - +#### **Example Usage:** + ```tsx import { useRemoveDirectory } from '@supabase-cache-helpers/storage-swr'; - import { createClient } from '@supabase/supabase-js'; - - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); - function Page() { + function Page() {} const { trigger: remove } = useRemoveDirectory( - client.storage.from('private_contact_files') + client.storage.from('uploads') ); - return
...
; + + function handleRemove() { + remove('user-123'); + } } ``` -
```tsx import { useRemoveDirectory } from '@supabase-cache-helpers/storage-react-query'; - import { createClient } from '@supabase/supabase-js'; - - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); function Page() { const { mutateAsync: remove } = useRemoveDirectory( - client.storage.from('private_contact_files') + client.storage.from('uploads') ); - return
...
; + + async function handleRemove() { + await remove('user-123'); + } } ``` -
-## `useRemoveFiles` +--- + +## **Removing Specific Files** + +### `useRemoveFiles` + +Removes **specific files** in Supabase Storage by their paths. -Remove a list of files by paths. +#### **Parameters:** +```ts +useRemoveFiles( + fileApi: StorageFileApi, // Supabase storage API instance + config?: UseMutationOptions +) +``` - +#### **Example Usage:** + ```tsx import { useRemoveFiles } from '@supabase-cache-helpers/storage-swr'; - import { createClient } from '@supabase/supabase-js'; - - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); function Page() { - const { trigger: remove } = useRemoveFiles( - client.storage.from('private_contact_files') - ); - return
...
; + const { trigger: remove } = useRemoveFiles(client.storage.from('uploads')); + + function handleRemove() { + remove(['user-123/file1.png', 'user-123/file2.jpg']); + } } ``` -
```tsx import { useRemoveFiles } from '@supabase-cache-helpers/storage-react-query'; - import { createClient } from '@supabase/supabase-js'; - - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); function Page() { - const { mutateAsync: remove } = useRemoveFiles( - client.storage.from('private_contact_files') - ); - return
...
; + const { mutateAsync: remove } = useRemoveFiles(client.storage.from('uploads')); + + async function handleRemove() { + await remove(['user-123/file1.png', 'user-123/file2.jpg']); + } } - ``` + ```
+ +--- + +### **Return Types for Mutations** + +| Hook | Return Type | Description | +|--------------------|------------|-------------| +| `useUpload` | `UploadFileResponse[]` | List of uploaded files with paths and potential errors | +| `useRemoveDirectory` | `FileObject[]` | List of removed files in the directory | +| `useRemoveFiles` | `FileObject[]` | List of removed files by path | + +#### **`UploadFileResponse` Definition:** +```ts +type UploadFileResponse = { + path: string; // The uploaded file path + error?: StorageError; // Error if applicable +}; +``` + +#### **`StorageError` Definition:** +```ts +export interface StorageError { + message: string; // Error message + statusCode?: number; // HTTP status code (if applicable) + error?: string; // Additional error info (if available) +} +``` diff --git a/docs/pages/storage/queries.mdx b/docs/pages/storage/queries.mdx index 47de04f5c..8a56c058c 100644 --- a/docs/pages/storage/queries.mdx +++ b/docs/pages/storage/queries.mdx @@ -1,207 +1,202 @@ import { Callout, Tabs } from 'nextra/components'; import { LinkedTabs } from '@/components/linked-tabs'; -# Queries +# Storage Queries -The cache helpers query hooks wrap the data fetching hooks of the cache libraries and pass both the cache key and the fetcher function from the Storage query. For example, +The **Supabase Cache Helpers** provide optimized query hooks for interacting with Supabase Storage using **React Query** or **SWR**. These hooks streamline caching, signed URL generation, and storage operations. + +--- + +## **Query Configuration Options** + +Most query hooks accept a configuration object based on React Query's `UseQueryOptions` or SWR's equivalent. ```ts -useFileUrl( - client.storage.from("public_contact_files"), - "postgrest-storage-file-url-94/1.jpg", - "public", -); +// Base type for query configuration +type QueryConfig = Omit, 'queryKey' | 'queryFn'>; ``` -is parsed into +### **Common Options** - - - ` storage$public_contact_files$postgrest-storage-file-url-94/1.jpg ` - - - ` [ "storage", "public_contact_files", "postgrest-storage-file-url-94/1.jpg" - ] ` - - +These options are commonly used across all query hooks: + +| Option | Type | Description | +|-------------------------|------------------------------------------|-------------| +| `refetchOnWindowFocus` | `boolean` | Refetches data when the window regains focus. Defaults to `true`. | +| `staleTime` | `number` | Time in milliseconds before data is considered stale. | +| `cacheTime` | `number` | Time in milliseconds to cache data before garbage collection. | +| `enabled` | `boolean` | If `false`, disables the query from automatically running. | +| `onSuccess` | `(data: TData) => void` | Callback when the query successfully fetches data. | +| `onError` | `(error: StorageError) => void` | Callback when the query encounters an error. | + +For the full list of React Query options, see the [React Query `useQuery` documentation](https://tanstack.com/query/latest/docs/react/reference/useQuery). + +### **Additional Options for `useDirectoryFileUrls`** + +In addition to the common options, `useDirectoryFileUrls` includes: + +```ts +// Extended configuration for useDirectoryFileUrls +type DirectoryFileUrlsConfig = Omit, 'queryKey' | 'queryFn'> & +Pick; +``` -## `useFileUrl` +| Option | Type | Description | +|------------|---------|-------------| +| `expiresIn` | `number` | Number of seconds before a signed URL expires (only for private storage). | -Supports `private`, and `public` buckets. You can pass `URLFetcherConfig` to configure signed urls, and ensure that a file in a public bucket exists. +Each hook's documentation specifies which options apply. + +--- + +## **Fetching File URLs** + +### `useFileUrl` + +Fetches the **URL of a file** in Supabase Storage, handling both **private** and **public** files. + +#### **Parameters:** +```ts +useFileUrl( + fileApi: StorageFileApi, // Supabase storage API instance + path: string, // Path to the file (e.g., 'dirname/myfile.jpg') + mode: 'public' | 'private', // Storage privacy mode + config?: URLFetcherConfig // Optional fetcher config (see below) +) +``` + +#### **`URLFetcherConfig` Options:** ```ts type URLFetcherConfig = { - // For private buckets only, set how long the signed url should be valid - expiresIn?: number; - // For public buckets only, if true queries the file using .list() - // and returns null if file does not exist - ensureExistence?: boolean; - // Triggers the file as a download if set to true. Set this parameter as the name of the file of you want to trigger the download with a different filename. - download?: string | boolean | undefined; - // Transform the asset before serving it to the client. - transform?: TransformOptions | undefined; + expiresIn?: number; // Expiration time for signed URLs (private buckets) + ensureExistence?: boolean; // Check if the file exists (public buckets only) + download?: string | boolean | undefined; // Trigger file download + transform?: TransformOptions | undefined; // Transform image before serving }; ``` - +#### **Return Type:** +```ts +UseQueryResult +``` + +| Property | Type | Description | +|--------------|-------------------------|-------------| +| `data` | `string \| undefined` | The file URL if found, otherwise `undefined`. | +| `error` | `StorageError \| null` | Error details if the request fails. | + +#### **Usage Example:** + ```tsx import { useFileUrl } from '@supabase-cache-helpers/storage-swr'; import { createClient } from '@supabase/supabase-js'; - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); + const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); function Page() { const { data: url } = useFileUrl( client.storage.from('public_contact_files'), 'dirname/myfile.jpg', 'public', - { - ensureExistence: true, - revalidateOnFocus: false, - } + { ensureExistence: true, revalidateOnFocus: false } ); return
{url}
; } ``` -
```tsx import { useFileUrl } from '@supabase-cache-helpers/storage-react-query'; import { createClient } from '@supabase/supabase-js'; - const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY - ); + const client = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY); function Page() { const { data: url } = useFileUrl( client.storage.from('public_contact_files'), 'dirname/myfile.jpg', 'public', - { - ensureExistence: true, - refetchOnWindowFocus: false, - } + { ensureExistence: true, refetchOnWindowFocus: false } ); return
{url}
; } ``` -
-## `useDirectory` +--- -Returns all files in a directory. +## **Fetching Directory Listings** - - -```tsx -import { useDirectory } from "@supabase-cache-helpers/storage-swr"; -import { createClient } from "@supabase/supabase-js"; - -const client = createClient( -process.env.SUPABASE_URL, -process.env.SUPABASE_ANON_KEY -); - -function Page() { -const { data: files } = useDirectory( -client.storage.from("private_contact_files"), -dirName, -{ -revalidateOnFocus: false, -} -); -return
...
; -} - -```` +### `useDirectory` -
- -```tsx -import { useDirectory } from "@supabase-cache-helpers/storage-react-query"; -import { createClient } from "@supabase/supabase-js"; - -const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY -); - -function Page() { - const { data: files } = useDirectory( - client.storage.from("private_contact_files"), - dirName, - { - refetchOnWindowFocus: false, - } - ); - return
...
; -} -```` +Fetches **all files** in a given **directory** from Supabase Storage. -
-
+#### **Parameters:** +```ts +useDirectory( + fileApi: StorageFileApi, // Supabase storage API instance + path: string, // Directory path (e.g., 'dirname/') + config?: UseQueryOptions // Optional query config +) +``` -## `useDirectoryFileUrls` +#### **Return Type:** +```ts +UseQueryResult +``` -Convenience hook that returns the files in a directory similar to `useDirectory` but adds the `url` for each. +| Property | Type | Description | +|--------------|-------------------------|-------------| +| `data` | `FileObject[] \| undefined` | List of files in the directory (or `undefined` if none exist). | +| `error` | `StorageError \| null` | Error details if the request fails. | - - -```tsx -import { useDirectoryFileUrls } from "@supabase-cache-helpers/storage-swr"; -import { createClient } from "@supabase/supabase-js"; - -const client = createClient( -process.env.SUPABASE_URL, -process.env.SUPABASE_ANON_KEY -); - -function Page() { -const { data: files } = useDirectoryFileUrls( -client.storage.from("private_contact_files"), -dirName, -"private", -{ -revalidateOnFocus: false, -} -); -return
...
; -} - -```` +#### **FileObject Structure:** +```ts +type FileObject = { + name: string; // File name (e.g., 'image.png') + id: string; // Unique identifier + updated_at: string; // Last modified timestamp + created_at: string; // Creation timestamp + metadata: Record; // File metadata (e.g., size, type) + bucket_id: string; // The storage bucket name +}; +``` -
- -```tsx -import { useDirectoryFileUrls } from "@supabase-cache-helpers/storage-react-query"; -import { createClient } from "@supabase/supabase-js"; - -const client = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY -); - -function Page() { - const { data: files } = useDirectoryFileUrls( - client.storage.from("private_contact_files"), - dirName, - "private", - { - refetchOnWindowFocus: false, - } - ); - return
...
; -} -```` +--- + +## **Fetching Directory Files with URLs** + +### `useDirectoryFileUrls` + +Fetches **all files in a directory and their URLs**, combining `useDirectory` with `useFileUrl`. + +#### **Parameters:** +```ts +useDirectoryFileUrls( + fileApi: StorageFileApi, // Supabase storage API instance + path: string, // Directory path (e.g., 'dirname/') + mode: 'public' | 'private', // Storage privacy mode + config?: UseQueryOptions<(FileObject & { url: string })[] | undefined, StorageError> // Query config +) +``` + +#### **Return Type:** +```ts +UseQueryResult<(FileObject & { url: string })[] | undefined, StorageError> +``` + +| Property | Type | Description | +|--------------|-----------------------------------------|-------------| +| `data` | `(FileObject & { url: string })[] \| undefined` | List of files with their URLs (or `undefined` if no files exist). | +| `error` | `StorageError \| null` | Error details if the request fails. | + +#### **FileObjectWithUrl Structure:** +```ts +type FileObjectWithUrl = FileObject & { + url: string; // The signed or public URL for the file +}; +``` -
-
From 7957809e9f05f45beed6997e80d5b1592e1fa0df Mon Sep 17 00:00:00 2001 From: geoguide Date: Thu, 13 Feb 2025 17:50:43 -0800 Subject: [PATCH 2/2] FIx example bucket name --- docs/pages/storage/mutations.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/pages/storage/mutations.mdx b/docs/pages/storage/mutations.mdx index 0bcab0fd7..e792510d1 100644 --- a/docs/pages/storage/mutations.mdx +++ b/docs/pages/storage/mutations.mdx @@ -99,7 +99,7 @@ type UseUploadInput = { function Page() { const { mutateAsync: upload } = useUpload( - client.storage.from('uploads'), + client.storage.from('private_contact_files'), { buildFileName: ({ fileName, path }) => `${dirName}/${path}/${fileName}` } ); @@ -135,7 +135,7 @@ useRemoveDirectory( function Page() {} const { trigger: remove } = useRemoveDirectory( - client.storage.from('uploads') + client.storage.from('private_contact_files') ); function handleRemove() { @@ -150,7 +150,7 @@ useRemoveDirectory( function Page() { const { mutateAsync: remove } = useRemoveDirectory( - client.storage.from('uploads') + client.storage.from('private_contact_files') ); async function handleRemove() { @@ -184,7 +184,7 @@ useRemoveFiles( import { useRemoveFiles } from '@supabase-cache-helpers/storage-swr'; function Page() { - const { trigger: remove } = useRemoveFiles(client.storage.from('uploads')); + const { trigger: remove } = useRemoveFiles(client.storage.from('private_contact_files')); function handleRemove() { remove(['user-123/file1.png', 'user-123/file2.jpg']); @@ -197,7 +197,7 @@ useRemoveFiles( import { useRemoveFiles } from '@supabase-cache-helpers/storage-react-query'; function Page() { - const { mutateAsync: remove } = useRemoveFiles(client.storage.from('uploads')); + const { mutateAsync: remove } = useRemoveFiles(client.storage.from('private_contact_files')); async function handleRemove() { await remove(['user-123/file1.png', 'user-123/file2.jpg']);