diff --git a/packages/core/storage-js/infra/postgres/dummy-data.sql b/packages/core/storage-js/infra/postgres/dummy-data.sql index 013fc8f7a..af5067898 100644 --- a/packages/core/storage-js/infra/postgres/dummy-data.sql +++ b/packages/core/storage-js/infra/postgres/dummy-data.sql @@ -40,6 +40,7 @@ INSERT INTO "storage"."objects" ("id", "bucket_id", "name", "owner", "created_at -- allows user to CRUD all buckets CREATE POLICY crud_buckets ON storage.buckets for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a'); CREATE POLICY crud_objects ON storage.objects for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a'); +CREATE POLICY crud_prefixes ON storage.prefixes for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a'); -- allow public CRUD acccess to the public folder in bucket2 CREATE POLICY crud_public_folder ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'public'); diff --git a/packages/core/storage-js/infra/storage/Dockerfile b/packages/core/storage-js/infra/storage/Dockerfile index 80a56b10e..5d8f27262 100644 --- a/packages/core/storage-js/infra/storage/Dockerfile +++ b/packages/core/storage-js/infra/storage/Dockerfile @@ -1,3 +1,3 @@ -FROM supabase/storage-api:v1.19.1 +FROM supabase/storage-api:v1.27.4 RUN apk add curl --no-cache \ No newline at end of file diff --git a/packages/core/storage-js/src/lib/types.ts b/packages/core/storage-js/src/lib/types.ts index 486186d90..a47d6520b 100644 --- a/packages/core/storage-js/src/lib/types.ts +++ b/packages/core/storage-js/src/lib/types.ts @@ -103,17 +103,51 @@ export interface SearchOptions { search?: string } +export interface SortByV2 { + column: 'name' | 'updated_at' | 'created_at' + order?: 'asc' | 'desc' +} + export interface SearchV2Options { + /** + * The number of files you want to be returned. + * @default 1000 + */ limit?: number + + /** + * The prefix search string to filter files by. + */ prefix?: string + + /** + * The cursor used for pagination. Pass the value received from nextCursor of the previous request. + */ cursor?: string + + /** + * Whether to emulate a hierarchical listing of objects using delimiters. + * + * - When `false` (default), all objects are listed as flat key/value pairs. + * - When `true`, the response groups objects by delimiter, making it appear + * like a file/folder hierarchy. + * + * @default false + */ with_delimiter?: boolean + + /** + * The column and order to sort by + * @default 'name asc' + */ + sortBy?: SortByV2 } export interface SearchV2Result { hasNext: boolean folders: { name: string }[] objects: FileObject[] + nextCursor?: string } export interface FetchParameters { diff --git a/packages/core/storage-js/test/__snapshots__/storageApi.test.ts.snap b/packages/core/storage-js/test/__snapshots__/storageApi.test.ts.snap index 25fc7e34d..1aebdc7fb 100644 --- a/packages/core/storage-js/test/__snapshots__/storageApi.test.ts.snap +++ b/packages/core/storage-js/test/__snapshots__/storageApi.test.ts.snap @@ -23,7 +23,7 @@ exports[`bucket api delete bucket 1`] = ` exports[`bucket api empty bucket 1`] = ` { - "message": "Successfully emptied", + "message": "Empty bucket has been queued. Completion may take up to an hour.", } `; diff --git a/packages/core/storage-js/test/storageFileApi.test.ts b/packages/core/storage-js/test/storageFileApi.test.ts index 028d34ed2..b2f1a873e 100644 --- a/packages/core/storage-js/test/storageFileApi.test.ts +++ b/packages/core/storage-js/test/storageFileApi.test.ts @@ -1,4 +1,4 @@ -import { StorageClient } from '../src/index' +import { SortByV2, StorageClient } from '../src/index' import * as fsp from 'fs/promises' import * as fs from 'fs' import * as path from 'path' @@ -357,6 +357,76 @@ describe('Object API', () => { ) }) + test('list objects V2 - folders', async () => { + await storage.from(bucketName).upload(uploadPath, file) + const res = await storage.from(bucketName).listV2({ with_delimiter: true }) + + expect(res.error).toBeNull() + expect(res.data).toEqual( + expect.objectContaining({ + hasNext: false, + folders: expect.arrayContaining([ + expect.objectContaining({ key: 'testpath', name: 'testpath/' }), + ]), + objects: [], + }) + ) + }) + + test('list objects V2 - paginated', async () => { + const fileSuffixes = ['zz', 'bb', 'xx', 'ww', 'cc', 'aa', 'yy', 'oo'] + for (const suffix of fileSuffixes) { + await storage.from(bucketName).upload(uploadPath + suffix, file) + } + + const testCases: { expectedSuffixes: string[]; sortBy?: SortByV2 }[] = [ + { + expectedSuffixes: fileSuffixes.slice().sort(), + // default sortBy = name asc + }, + { + expectedSuffixes: fileSuffixes.slice().sort().reverse(), + sortBy: { column: 'name', order: 'desc' }, + }, + { + expectedSuffixes: fileSuffixes.slice(), + sortBy: { column: 'created_at', order: 'asc' }, + }, + { + expectedSuffixes: fileSuffixes.slice().reverse(), + sortBy: { column: 'created_at', order: 'desc' }, + }, + ] + + for (const { expectedSuffixes, sortBy } of testCases) { + let cursor: string | undefined + let hasNext = true + let pages = 0 + while (hasNext) { + const res = await storage.from(bucketName).listV2({ + prefix: 'testpath/', + with_delimiter: true, + limit: 2, + cursor, + sortBy, + }) + + expect(res.error).toBeNull() + expect(res.data?.objects).toHaveLength(2) + expect(res.data?.objects).toEqual( + expectedSuffixes + .splice(0, 2) + .map((v) => expect.objectContaining({ name: uploadPath + v })) + ) + + hasNext = res.data?.hasNext || false + cursor = res.data?.nextCursor + pages++ + } + expect(pages).toBe(4) + } + }) + test('move object to different path', async () => { const newPath = `testpath/file-moved-${Date.now()}.txt` await storage.from(bucketName).upload(uploadPath, file)