feat(studio): migrate storage infinite-query hook to list-v2 (#50730)

## Problem

Storage's `list` (v1) endpoint is being deprecated in favor of
`list-v2`, which uses cursor pagination instead of numeric offset (which
degrades on large buckets) and fixes folders that differ only by case
not both being listable. This is PR 1 of the migration (parent:
FE-4423); it covers the shared infinite-query hook and its two
consumers.

## Solution

Added `listBucketObjectsV2` alongside the existing v1
`listBucketObjects` (still used elsewhere, migrated in a later PR), and
replaced the `useBucketObjectsInfiniteQuery` hook with
`bucketObjectsInfiniteQueryOptions` built on `infiniteQueryOptions`,
following the repo's preferred data-fetching pattern. Pagination now
uses `hasNext`/`nextCursor` instead of an offset multiplier, and
`queryFn` rejects a response that claims `hasNext` without advancing the
cursor so a misbehaving backend can't send `fetchNextPage` into an
infinite loop. v2 splits results into separate `folders`/`objects`
arrays and has no `search` field, so the two consumers
(`BucketFilePickerColumn`, `MoveItemsFolderPicker`) merge/sort those
arrays themselves, and search is folded into a `prefix` match instead.
Also removed "Time last accessed" from the picker's sort dropdown since
v2's `sortBy.column` doesn't support it, with a defensive fallback to
`name` in case the shared sort preference (still used by the v1 main
file explorer) carries that value over. Added the missing self-hosted
`list-v2` API proxy route (`pages/api` + the TanStack `routes/api`
wrapper) using storage-js's `listV2()`, since self-hosted Studio only
had a v1 route and every v2 request was 404ing there.

## Review instructions

1. Open the bucket file picker (e.g. via an OAuth app logo upload),
confirm folders and files both render and paginate correctly, and that
searching still filters as expected.
2. Open the "Move items" modal's folder picker, confirm you can navigate
into and back out of subfolders, and that folder search still works.
3. Run `pnpm test:studio -- MoveItemsModal`.

## Checklist

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which references
[WORD_LIST](https://github.com/supabase/supabase/blob/master/apps/docs/WORD_LIST.md)
and the docs
[CONTRIBUTING](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md)
guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Storage browsing loads large bucket listings in pages, helping keep
navigation responsive.
* Folder pickers display folders and files together across paginated
results.
* Moving items between folders uses the same paginated browsing
experience.
* Search remains available in the final folder level, and folder
navigation shows the correct contents.

* **Updates**
* “Time last accessed” is no longer available as a sorting option in
storage pickers. Sorting is available by name, creation time, or update
time.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ivan VasilovandClaude Sonnet 5 authored and GitHub committed 2026-09-25 16:11:24 +02:00
1 parent 51a167d910
commit 12894ddd2a
11 files changed
+323 -59

No files matched your search

@@ -1,5 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
import { useDebounce } from '@uidotdev/usehooks'
import { useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
@@ -10,7 +10,13 @@ import { toast } from 'sonner'
import { Checkbox, cn } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
import {
STORAGE_ROW_STATUS,
STORAGE_ROW_TYPES,
STORAGE_SORT_BY,
STORAGE_SORT_BY_ORDER,
STORAGE_VIEWS,
} from '../Storage.constants'
import type { StorageItem } from '../Storage.types'
import { formatFolderItems } from '../StorageExplorer/StorageExplorer.utils'
import { useStoragePreference } from '../StorageExplorer/useStoragePreference'
@@ -19,7 +25,7 @@ import { BucketFilePickerRow } from './BucketFilePickerRow'
import { useBucketFilePickerStateSnapshot } from './BucketFilePickerState'
import { InfiniteListDefault, LoaderForIconMenuItems } from '@/components/ui/InfiniteList'
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
import { useBucketObjectsInfiniteQuery } from '@/data/storage/bucket-objects-infinite-query'
import { bucketObjectsInfiniteQueryOptions } from '@/data/storage/bucket-objects-infinite-query'
import { storageKeys } from '@/data/storage/keys'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { formatBytes } from '@/lib/helpers'
@@ -136,28 +142,57 @@ export const BucketFilePickerColumn = ({
const isLastFolder = index === columns.length
const { view, sortBy, sortByOrder } = useStoragePreference(projectRef!)
// v2's sortBy.column doesn't support 'last_accessed_at'; the picker's own sort dropdown no
// longer offers it, but the preference is shared with the (still v1) main file explorer.
const sortColumn = sortBy === STORAGE_SORT_BY.LAST_ACCESSED_AT ? STORAGE_SORT_BY.NAME : sortBy
const debouncedSearchString = useDebounce(itemSearchString, 500)
const { data, isLoading, isFetching, fetchNextPage, hasNextPage } = useBucketObjectsInfiniteQuery(
{
const { data, isLoading, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
...bucketObjectsInfiniteQueryOptions({
projectRef,
bucketId: bucket.id,
path,
options: {
sortBy: {
column: sortBy,
column: sortColumn,
order: sortByOrder,
},
// When a user tries to search, only search in the last opened folder (rightmost column)
...(isLastFolder && debouncedSearchString ? { search: debouncedSearchString } : {}),
},
}
)
}),
})
const items = useMemo(() => {
const objs = data?.pages.flatMap((page) => page) || []
return formatFolderItems(objs)
}, [data])
const pages = data?.pages ?? []
// v2's `name` is the full key/prefix from the bucket root (folders end with a trailing
// slash), not the bare file/folder name — take the last path segment for display.
const objs = pages
.flatMap((page) => page.objects)
.map((object) => ({ ...object, name: object.name.split('/').pop() ?? object.name }))
// v2 returns folders and objects separately; re-combine them into the single sorted list
// formatFolderItems expects, tagging folders with `id: null` per its existing convention.
const folders = pages
.flatMap((page) => page.folders)
.map((folder) => {
const folderPath = folder.name.replace(/\/$/, '')
return {
id: null,
name: folderPath.split('/').pop() ?? folderPath,
created_at: null,
updated_at: null,
last_accessed_at: null,
metadata: null,
}
})
const direction = sortByOrder === STORAGE_SORT_BY_ORDER.DESC ? -1 : 1
const merged = [...folders, ...objs].sort((a, b) => {
const aValue = (sortColumn === STORAGE_SORT_BY.NAME ? a.name : a[sortColumn]) ?? ''
const bValue = (sortColumn === STORAGE_SORT_BY.NAME ? b.name : b[sortColumn]) ?? ''
return direction * String(aValue).localeCompare(String(bValue))
})
return formatFolderItems(merged)
}, [data, sortColumn, sortByOrder])
const haveSelectedItems = selectedItems.length > 0
const columnItemsId = items.map((item) => item.id)
@@ -40,11 +40,11 @@ const VIEW_OPTIONS = [
{ key: STORAGE_VIEWS.LIST, name: 'As list' },
]
// list-v2 only supports sorting by name, created_at, or updated_at
const SORT_BY_OPTIONS = [
{ key: STORAGE_SORT_BY.NAME, name: 'Name' },
{ key: STORAGE_SORT_BY.CREATED_AT, name: 'Time created' },
{ key: STORAGE_SORT_BY.UPDATED_AT, name: 'Time modified' },
{ key: STORAGE_SORT_BY.LAST_ACCESSED_AT, name: 'Time last accessed' },
]
const SORT_ORDER_OPTIONS = [
@@ -1,3 +1,5 @@
import type { components } from 'api-types'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES } from './Storage.constants'
import { PolicyFormField } from './StoragePolicies/StoragePolicies.types'
@@ -51,3 +53,7 @@ export interface StorageItemMetadata {
lastModified: string
mimetype: string
}
/** A folder entry as returned by the storage list-v2 endpoint's `folders` array */
export type StorageObjectV2Folder =
components['schemas']['StorageListResponseV2_Output']['folders'][number]
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { useDebounce } from '@uidotdev/usehooks'
import { ArrowLeft, Search, X } from 'lucide-react'
import { useMemo, useState } from 'react'
@@ -9,11 +9,11 @@ import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { STORAGE_SORT_BY, STORAGE_SORT_BY_ORDER } from '../Storage.constants'
import { MoveItemsFolderPickerBreadcrumb } from './MoveItemsFolderPickerBreadcrumb'
import { FolderPickerRow } from './MoveItemsFolderPickerRow'
import { filterFoldersBySearch, getDestinationLabel, toFolders } from './MoveItemsModal.utils'
import { filterFoldersBySearch, getDestinationLabel } from './MoveItemsModal.utils'
import { AlertError } from '@/components/ui/AlertError'
import { InfiniteListDefault, LoaderForIconMenuItems } from '@/components/ui/InfiniteList'
import { bucketFoldersQueryOptions } from '@/data/storage/bucket-folders-query'
import { useBucketObjectsInfiniteQuery } from '@/data/storage/bucket-objects-infinite-query'
import { bucketObjectsInfiniteQueryOptions } from '@/data/storage/bucket-objects-infinite-query'
import { onSearchInputEscape } from '@/lib/keyboard'
const ROW_HEIGHT = 37
@@ -51,13 +51,15 @@ export const MoveItemsFolderPicker = ({
hasNextPage,
fetchNextPage,
refetch: refetchObjects,
} = useBucketObjectsInfiniteQuery({
projectRef,
bucketId,
path,
options: {
sortBy: { column: STORAGE_SORT_BY.NAME, order: STORAGE_SORT_BY_ORDER.ASC },
},
} = useInfiniteQuery({
...bucketObjectsInfiniteQueryOptions({
projectRef,
bucketId,
path,
options: {
sortBy: { column: STORAGE_SORT_BY.NAME, order: STORAGE_SORT_BY_ORDER.ASC },
},
}),
})
const {
@@ -71,10 +73,19 @@ export const MoveItemsFolderPicker = ({
enabled: isSearching,
})
const folders = useMemo(
() => toFolders(objectsData?.pages.flat() ?? [], path),
[objectsData, path]
)
const folders = useMemo(() => {
// v2's `name` is the folder's full path from the bucket root, with a trailing slash
// (e.g. "outer/inner/") — not the bare folder name, and not relative to `path`.
return (objectsData?.pages ?? [])
.flatMap((page) => page.folders)
.map((folder) => {
const folderPath = folder.name.replace(/\/$/, '')
return {
name: folderPath.split('/').pop() ?? folderPath,
path: folderPath,
}
})
}, [objectsData])
const searchResults = useMemo(
() => filterFoldersBySearch(foldersData?.folders ?? [], debouncedSearchString),
@@ -31,6 +31,16 @@ const createFile = (name: string): StorageObject => ({
metadata: { size: 1024, mimetype: 'image/png' },
})
/** list-v2's `objects` entries require the timestamp fields non-null, unlike the v1 fixture type */
const toV2Object = (file: StorageObject) => ({
id: file.id!,
name: file.name,
created_at: file.created_at!,
updated_at: file.updated_at!,
last_accessed_at: file.last_accessed_at!,
metadata: file.metadata,
})
/** Contents of the fake bucket the picker browses, keyed by folder path */
const BUCKET_CONTENTS: Record<string, StorageObject[]> = {
'': [
@@ -45,7 +55,35 @@ const BUCKET_CONTENTS: Record<string, StorageObject[]> = {
invoices: [],
}
const mockObjectsList = () =>
const mockObjectsList = () => {
// Used by the migrated infinite-query hook (folder contents in the picker's main listing)
addAPIMock({
method: 'post',
path: '/platform/storage/:ref/buckets/:id/objects/list-v2',
response: async ({ request }) => {
const body = (await request.json()) as { prefix: string }
// The real API expects a trailing slash to browse a folder's contents; strip it to match
// the BUCKET_CONTENTS keys, which mirror path segments without one.
const path = body.prefix.replace(/\/$/, '')
const contents = BUCKET_CONTENTS[path] ?? []
// The real API returns `name` as the full path from the bucket root, with a trailing
// slash for folders — not the bare name relative to the requested prefix.
const fullPath = (name: string) => (path ? `${path}/${name}` : name)
return HttpResponse.json({
folders: contents
.filter((object) => object.id === null)
.map((object) => ({
name: `${fullPath(object.name)}/`,
})),
objects: contents
.filter((object) => object.id !== null)
.map((object) => toV2Object({ ...object, name: fullPath(object.name) })),
hasNext: false,
})
},
})
// Still v1: used by bucket-folders-query.ts's crawl, which powers the search results list
addAPIMock({
method: 'post',
path: '/platform/storage/:ref/buckets/:id/objects/list',
@@ -54,6 +92,7 @@ const mockObjectsList = () =>
return HttpResponse.json(BUCKET_CONTENTS[body.path] ?? [])
},
})
}
const selectedFile: StorageItemWithColumn = {
id: 'id-avatar.png',
@@ -146,11 +185,15 @@ describe('MoveItemsModal', () => {
const files = Array.from({ length: 200 }, (_, index) => createFile(`file-${index}.png`))
addAPIMock({
method: 'post',
path: '/platform/storage/:ref/buckets/:id/objects/list',
path: '/platform/storage/:ref/buckets/:id/objects/list-v2',
response: async ({ request }) => {
const body = (await request.json()) as { options?: { offset?: number } }
const isFirstPage = (body.options?.offset ?? 0) === 0
return HttpResponse.json(isFirstPage ? files : [createFolder('buried')])
const body = (await request.json()) as { cursor?: string }
const isFirstPage = !body.cursor
return HttpResponse.json(
isFirstPage
? { folders: [], objects: files.map(toV2Object), hasNext: true, nextCursor: 'page-2' }
: { folders: [{ name: 'buried/' }], objects: [], hasNext: false }
)
},
})
@@ -0,0 +1,63 @@
import { HttpResponse } from 'msw'
import { describe, expect, it } from 'vitest'
import { bucketObjectsInfiniteQueryOptions } from './bucket-objects-infinite-query'
import { addAPIMock } from '@/tests/lib/msw'
/** Mocks list-v2 and returns a getter for the last request body it received. */
const captureRequestBody = () => {
let body: { prefix: string; cursor?: string } | undefined
addAPIMock({
method: 'post',
path: '/platform/storage/:ref/buckets/:id/objects/list-v2',
response: async ({ request }) => {
body = (await request.json()) as { prefix: string; cursor?: string }
return HttpResponse.json({ folders: [], objects: [], hasNext: false })
},
})
return () => body
}
const runQueryFn = (path: string, search?: string, pageParam?: string) => {
const options = bucketObjectsInfiniteQueryOptions({
projectRef: 'default',
bucketId: 'avatars',
path,
options: search ? { search } : undefined,
})
return options.queryFn!({ signal: new AbortController().signal, pageParam } as any)
}
describe('bucketObjectsInfiniteQueryOptions', () => {
it('browses the bucket root with an empty prefix', async () => {
const getBody = captureRequestBody()
await runQueryFn('')
expect(getBody()?.prefix).toBe('')
})
it('appends a trailing slash to browse into a subfolder', async () => {
// list-v2 with with_delimiter:true treats a bare "photos" as a partial name match
// against siblings like "photos-old" rather than descending into the folder.
const getBody = captureRequestBody()
await runQueryFn('photos')
expect(getBody()?.prefix).toBe('photos/')
})
it('folds a search string into the prefix without a trailing slash after it', async () => {
const getBody = captureRequestBody()
await runQueryFn('photos', 'inv')
expect(getBody()?.prefix).toBe('photos/inv')
})
it('searches at the bucket root without a leading slash', async () => {
const getBody = captureRequestBody()
await runQueryFn('', 'inv')
expect(getBody()?.prefix).toBe('inv')
})
it('forwards the page param as the cursor', async () => {
const getBody = captureRequestBody()
await runQueryFn('photos', undefined, 'cursor-123')
expect(getBody()?.cursor).toBe('cursor-123')
})
})
@@ -1,9 +1,8 @@
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
import { infiniteQueryOptions } from '@tanstack/react-query'
import { listBucketObjects, type StorageObject } from './bucket-objects-list-mutation'
import { listBucketObjectsV2 } from './bucket-objects-list-mutation'
import { storageKeys } from './keys'
import type { components } from '@/data/api'
import type { ResponseError, UseCustomInfiniteQueryOptions } from '@/types'
const DEFAULT_LIMIT = 200
@@ -11,45 +10,53 @@ type StorageObjectsQueryParams = {
projectRef?: string
bucketId?: string
path: string
options?: Omit<NonNullable<components['schemas']['GetObjectsBody']['options']>, 'offset'>
options?: Omit<
components['schemas']['GetObjectsV2Body'],
'prefix' | 'cursor' | 'with_delimiter'
> & {
/** Not a real v2 API field: folded into `prefix` so v2 returns entries starting with it */
search?: string
}
}
export type StorageObjectsData = StorageObject[]
export type StorageObjectsError = ResponseError
export type StorageObjectsPage = components['schemas']['StorageListResponseV2_Output']
export const useBucketObjectsInfiniteQuery = <TData = StorageObjectsData>(
export const bucketObjectsInfiniteQueryOptions = (
{ projectRef, bucketId, path, options }: StorageObjectsQueryParams,
{
enabled = true,
...queryOptions
}: UseCustomInfiniteQueryOptions<
StorageObjectsData,
StorageObjectsError,
InfiniteData<TData>,
readonly unknown[],
number
> = {}
{ enabled = true }: { enabled?: boolean } = {}
) => {
const limit = options?.limit ?? DEFAULT_LIMIT
const { search, ...v2Options } = options ?? {}
// A trailing slash is required to browse a folder's contents with with_delimiter: true —
// without it, v2 treats e.g. "docs" as a partial name match against siblings like "docs-old"
// rather than descending into the folder.
const browsePrefix = path ? `${path}/` : ''
const prefix = search ? `${browsePrefix}${search}` : browsePrefix
const limit = v2Options.limit ?? DEFAULT_LIMIT
return useInfiniteQuery({
return infiniteQueryOptions({
// prefix and search are incorporated into the path.
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: storageKeys.objects(projectRef, bucketId, path, options),
queryFn: ({ signal, pageParam }) =>
listBucketObjects(
listBucketObjectsV2(
{
projectRef: projectRef!,
bucketId,
path,
options: { ...options, limit, offset: pageParam * limit },
prefix,
cursor: pageParam,
options: { ...v2Options, limit },
},
signal
) as Promise<StorageObjectsData>,
),
enabled: enabled && !!projectRef && !!bucketId,
initialPageParam: 0,
getNextPageParam(lastPage, pages) {
if (lastPage.length < limit) return undefined
return pages.length
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage, _pages, lastPageParam) => {
if (!lastPage.hasNext) return undefined
// A backend that says there's more but doesn't advance the cursor would otherwise send
// fetchNextPage into an infinite loop re-fetching the same page forever; treat it as the
// end of the list instead.
if (!lastPage.nextCursor || lastPage.nextCursor === lastPageParam) return undefined
return lastPage.nextCursor
},
...queryOptions,
})
}
@@ -41,6 +41,40 @@ export const listBucketObjects = async (
type ListBucketObjectsData = Awaited<ReturnType<typeof listBucketObjects>>
type ListBucketObjectsV2Params = {
projectRef: string
bucketId?: string
prefix: string
cursor?: string
options?: Omit<components['schemas']['GetObjectsV2Body'], 'prefix' | 'cursor' | 'with_delimiter'>
}
export const listBucketObjectsV2 = async (
{ projectRef, bucketId, prefix, cursor, options }: ListBucketObjectsV2Params,
signal?: AbortSignal
) => {
if (!bucketId) throw new Error('bucketId is required')
const { data, error } = await post('/platform/storage/{ref}/buckets/{id}/objects/list-v2', {
params: {
path: {
ref: projectRef,
id: bucketId,
},
},
body: {
prefix,
cursor,
with_delimiter: true,
...options,
},
signal,
})
if (error) handleError(error)
return data
}
export const useGetSignBucketObjectMutation = ({
onSuccess,
onError,
@@ -0,0 +1,32 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { apiWrapper } from '@/lib/api/apiWrapper'
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
export default function listV2Route(req: NextApiRequest, res: NextApiResponse) {
return apiWrapper(req, res, handler)
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
res.setHeader('Allow', ['POST'])
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
}
}
const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { id } = req.query
const { data, error } = await supabase.storage.from(id as string).listV2(req.body)
if (error) {
return res.status(500).json({ error: error.message })
}
return res.status(200).json(data)
}
+23
View File
@@ -329,6 +329,7 @@ import { Route as ApiPlatformStorageRefBucketsIdObjectsSignMultiRouteImport } fr
import { Route as ApiPlatformStorageRefBucketsIdObjectsSignRouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/sign'
import { Route as ApiPlatformStorageRefBucketsIdObjectsPublicUrlRouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/public-url'
import { Route as ApiPlatformStorageRefBucketsIdObjectsMoveRouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/move'
import { Route as ApiPlatformStorageRefBucketsIdObjectsListV2RouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/list-v2'
import { Route as ApiPlatformStorageRefBucketsIdObjectsListRouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/list'
import { Route as ApiPlatformStorageRefBucketsIdObjectsDownloadRouteImport } from './routes/api/platform/storage/$ref/buckets/$id/objects/download'
@@ -2119,6 +2120,12 @@ const ApiPlatformStorageRefBucketsIdObjectsMoveRoute =
path: '/api/platform/storage/$ref/buckets/$id/objects/move',
getParentRoute: () => rootRouteImport,
} as any)
const ApiPlatformStorageRefBucketsIdObjectsListV2Route =
ApiPlatformStorageRefBucketsIdObjectsListV2RouteImport.update({
id: '/api/platform/storage/$ref/buckets/$id/objects/list-v2',
path: '/api/platform/storage/$ref/buckets/$id/objects/list-v2',
getParentRoute: () => rootRouteImport,
} as any)
const ApiPlatformStorageRefBucketsIdObjectsListRoute =
ApiPlatformStorageRefBucketsIdObjectsListRouteImport.update({
id: '/api/platform/storage/$ref/buckets/$id/objects/list',
@@ -2444,6 +2451,7 @@ export interface FileRoutesByFullPath {
'/project/$ref/integrations/$id/$pageId/$childId/': typeof ProjectRefIntegrationsIdPageIdChildIdIndexRoute
'/api/platform/storage/$ref/buckets/$id/objects/download': typeof ApiPlatformStorageRefBucketsIdObjectsDownloadRoute
'/api/platform/storage/$ref/buckets/$id/objects/list': typeof ApiPlatformStorageRefBucketsIdObjectsListRoute
'/api/platform/storage/$ref/buckets/$id/objects/list-v2': typeof ApiPlatformStorageRefBucketsIdObjectsListV2Route
'/api/platform/storage/$ref/buckets/$id/objects/move': typeof ApiPlatformStorageRefBucketsIdObjectsMoveRoute
'/api/platform/storage/$ref/buckets/$id/objects/public-url': typeof ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute
'/api/platform/storage/$ref/buckets/$id/objects/sign': typeof ApiPlatformStorageRefBucketsIdObjectsSignRoute
@@ -2751,6 +2759,7 @@ export interface FileRoutesByTo {
'/project/$ref/integrations/$id/$pageId/$childId': typeof ProjectRefIntegrationsIdPageIdChildIdIndexRoute
'/api/platform/storage/$ref/buckets/$id/objects/download': typeof ApiPlatformStorageRefBucketsIdObjectsDownloadRoute
'/api/platform/storage/$ref/buckets/$id/objects/list': typeof ApiPlatformStorageRefBucketsIdObjectsListRoute
'/api/platform/storage/$ref/buckets/$id/objects/list-v2': typeof ApiPlatformStorageRefBucketsIdObjectsListV2Route
'/api/platform/storage/$ref/buckets/$id/objects/move': typeof ApiPlatformStorageRefBucketsIdObjectsMoveRoute
'/api/platform/storage/$ref/buckets/$id/objects/public-url': typeof ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute
'/api/platform/storage/$ref/buckets/$id/objects/sign': typeof ApiPlatformStorageRefBucketsIdObjectsSignRoute
@@ -3076,6 +3085,7 @@ export interface FileRoutesById {
'/project/$ref/integrations/$id/$pageId/$childId/': typeof ProjectRefIntegrationsIdPageIdChildIdIndexRoute
'/api/platform/storage/$ref/buckets/$id/objects/download': typeof ApiPlatformStorageRefBucketsIdObjectsDownloadRoute
'/api/platform/storage/$ref/buckets/$id/objects/list': typeof ApiPlatformStorageRefBucketsIdObjectsListRoute
'/api/platform/storage/$ref/buckets/$id/objects/list-v2': typeof ApiPlatformStorageRefBucketsIdObjectsListV2Route
'/api/platform/storage/$ref/buckets/$id/objects/move': typeof ApiPlatformStorageRefBucketsIdObjectsMoveRoute
'/api/platform/storage/$ref/buckets/$id/objects/public-url': typeof ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute
'/api/platform/storage/$ref/buckets/$id/objects/sign': typeof ApiPlatformStorageRefBucketsIdObjectsSignRoute
@@ -3400,6 +3410,7 @@ export interface FileRouteTypes {
| '/project/$ref/integrations/$id/$pageId/$childId/'
| '/api/platform/storage/$ref/buckets/$id/objects/download'
| '/api/platform/storage/$ref/buckets/$id/objects/list'
| '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
| '/api/platform/storage/$ref/buckets/$id/objects/move'
| '/api/platform/storage/$ref/buckets/$id/objects/public-url'
| '/api/platform/storage/$ref/buckets/$id/objects/sign'
@@ -3707,6 +3718,7 @@ export interface FileRouteTypes {
| '/project/$ref/integrations/$id/$pageId/$childId'
| '/api/platform/storage/$ref/buckets/$id/objects/download'
| '/api/platform/storage/$ref/buckets/$id/objects/list'
| '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
| '/api/platform/storage/$ref/buckets/$id/objects/move'
| '/api/platform/storage/$ref/buckets/$id/objects/public-url'
| '/api/platform/storage/$ref/buckets/$id/objects/sign'
@@ -4031,6 +4043,7 @@ export interface FileRouteTypes {
| '/project/$ref/integrations/$id/$pageId/$childId/'
| '/api/platform/storage/$ref/buckets/$id/objects/download'
| '/api/platform/storage/$ref/buckets/$id/objects/list'
| '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
| '/api/platform/storage/$ref/buckets/$id/objects/move'
| '/api/platform/storage/$ref/buckets/$id/objects/public-url'
| '/api/platform/storage/$ref/buckets/$id/objects/sign'
@@ -4156,6 +4169,7 @@ export interface RootRouteChildren {
ApiV1ProjectsRefFunctionsSlugIndexRoute: typeof ApiV1ProjectsRefFunctionsSlugIndexRoute
ApiPlatformStorageRefBucketsIdObjectsDownloadRoute: typeof ApiPlatformStorageRefBucketsIdObjectsDownloadRoute
ApiPlatformStorageRefBucketsIdObjectsListRoute: typeof ApiPlatformStorageRefBucketsIdObjectsListRoute
ApiPlatformStorageRefBucketsIdObjectsListV2Route: typeof ApiPlatformStorageRefBucketsIdObjectsListV2Route
ApiPlatformStorageRefBucketsIdObjectsMoveRoute: typeof ApiPlatformStorageRefBucketsIdObjectsMoveRoute
ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute: typeof ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute
ApiPlatformStorageRefBucketsIdObjectsSignRoute: typeof ApiPlatformStorageRefBucketsIdObjectsSignRoute
@@ -6409,6 +6423,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiPlatformStorageRefBucketsIdObjectsMoveRouteImport
parentRoute: typeof rootRouteImport
}
'/api/platform/storage/$ref/buckets/$id/objects/list-v2': {
id: '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
path: '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
fullPath: '/api/platform/storage/$ref/buckets/$id/objects/list-v2'
preLoaderRoute: typeof ApiPlatformStorageRefBucketsIdObjectsListV2RouteImport
parentRoute: typeof rootRouteImport
}
'/api/platform/storage/$ref/buckets/$id/objects/list': {
id: '/api/platform/storage/$ref/buckets/$id/objects/list'
path: '/api/platform/storage/$ref/buckets/$id/objects/list'
@@ -7304,6 +7325,8 @@ const rootRouteChildren: RootRouteChildren = {
ApiPlatformStorageRefBucketsIdObjectsDownloadRoute,
ApiPlatformStorageRefBucketsIdObjectsListRoute:
ApiPlatformStorageRefBucketsIdObjectsListRoute,
ApiPlatformStorageRefBucketsIdObjectsListV2Route:
ApiPlatformStorageRefBucketsIdObjectsListV2Route,
ApiPlatformStorageRefBucketsIdObjectsMoveRoute:
ApiPlatformStorageRefBucketsIdObjectsMoveRoute,
ApiPlatformStorageRefBucketsIdObjectsPublicUrlRoute:
@@ -0,0 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { toWebHandler } from '@/compat/next/api'
import nextHandler from '@/pages/api/platform/storage/[ref]/buckets/[id]/objects/list-v2'
const handler = toWebHandler(nextHandler)
export const Route = createFileRoute('/api/platform/storage/$ref/buckets/$id/objects/list-v2')({
server: { handlers: { POST: handler } },
})