diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx index 9b0da9e47cf..632268ac572 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerColumn.tsx @@ -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) diff --git a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx index a55f093d2ef..e851762ac4f 100644 --- a/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx +++ b/apps/studio/components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx @@ -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 = [ diff --git a/apps/studio/components/interfaces/Storage/Storage.types.ts b/apps/studio/components/interfaces/Storage/Storage.types.ts index 3b55265bca9..3763158c309 100644 --- a/apps/studio/components/interfaces/Storage/Storage.types.ts +++ b/apps/studio/components/interfaces/Storage/Storage.types.ts @@ -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] diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsFolderPicker.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsFolderPicker.tsx index 55b89dfc2a7..00d6f302ff4 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsFolderPicker.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsFolderPicker.tsx @@ -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), diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.test.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.test.tsx index 551719be029..6dcf66c9229 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.test.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/MoveItemsModal.test.tsx @@ -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 = { '': [ @@ -45,7 +55,35 @@ const BUCKET_CONTENTS: Record = { 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 } + ) }, }) diff --git a/apps/studio/data/storage/bucket-objects-infinite-query.test.ts b/apps/studio/data/storage/bucket-objects-infinite-query.test.ts new file mode 100644 index 00000000000..d70cc19f70e --- /dev/null +++ b/apps/studio/data/storage/bucket-objects-infinite-query.test.ts @@ -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') + }) +}) diff --git a/apps/studio/data/storage/bucket-objects-infinite-query.ts b/apps/studio/data/storage/bucket-objects-infinite-query.ts index eadaf31a821..f73f492c694 100644 --- a/apps/studio/data/storage/bucket-objects-infinite-query.ts +++ b/apps/studio/data/storage/bucket-objects-infinite-query.ts @@ -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, '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 = ( +export const bucketObjectsInfiniteQueryOptions = ( { projectRef, bucketId, path, options }: StorageObjectsQueryParams, - { - enabled = true, - ...queryOptions - }: UseCustomInfiniteQueryOptions< - StorageObjectsData, - StorageObjectsError, - InfiniteData, - 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, + ), 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, }) } diff --git a/apps/studio/data/storage/bucket-objects-list-mutation.ts b/apps/studio/data/storage/bucket-objects-list-mutation.ts index 4dab2205362..13f775408e2 100644 --- a/apps/studio/data/storage/bucket-objects-list-mutation.ts +++ b/apps/studio/data/storage/bucket-objects-list-mutation.ts @@ -41,6 +41,40 @@ export const listBucketObjects = async ( type ListBucketObjectsData = Awaited> +type ListBucketObjectsV2Params = { + projectRef: string + bucketId?: string + prefix: string + cursor?: string + options?: Omit +} + +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, diff --git a/apps/studio/pages/api/platform/storage/[ref]/buckets/[id]/objects/list-v2.ts b/apps/studio/pages/api/platform/storage/[ref]/buckets/[id]/objects/list-v2.ts new file mode 100644 index 00000000000..0c0e2081d0e --- /dev/null +++ b/apps/studio/pages/api/platform/storage/[ref]/buckets/[id]/objects/list-v2.ts @@ -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) +} diff --git a/apps/studio/routeTree.gen.ts b/apps/studio/routeTree.gen.ts index 8ca051240cc..7b18ae86134 100644 --- a/apps/studio/routeTree.gen.ts +++ b/apps/studio/routeTree.gen.ts @@ -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: diff --git a/apps/studio/routes/api/platform/storage/$ref/buckets/$id/objects/list-v2.ts b/apps/studio/routes/api/platform/storage/$ref/buckets/$id/objects/list-v2.ts new file mode 100644 index 00000000000..83963a2c7d5 --- /dev/null +++ b/apps/studio/routes/api/platform/storage/$ref/buckets/$id/objects/list-v2.ts @@ -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 } }, +})