Skip to content

Commit e668b1b

Browse files
authored
feat(storage): add support for sorting to list v2 (#1606)
1 parent a9e258b commit e668b1b

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed

packages/core/storage-js/infra/postgres/dummy-data.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ INSERT INTO "storage"."objects" ("id", "bucket_id", "name", "owner", "created_at
4040
-- allows user to CRUD all buckets
4141
CREATE POLICY crud_buckets ON storage.buckets for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a');
4242
CREATE POLICY crud_objects ON storage.objects for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a');
43+
CREATE POLICY crud_prefixes ON storage.prefixes for all USING (auth.uid() = '317eadce-631a-4429-a0bb-f19a7a517b4a');
4344

4445
-- allow public CRUD acccess to the public folder in bucket2
4546
CREATE POLICY crud_public_folder ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'public');
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM supabase/storage-api:v1.19.1
1+
FROM supabase/storage-api:v1.27.4
22

33
RUN apk add curl --no-cache

packages/core/storage-js/src/lib/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,51 @@ export interface SearchOptions {
103103
search?: string
104104
}
105105

106+
export interface SortByV2 {
107+
column: 'name' | 'updated_at' | 'created_at'
108+
order?: 'asc' | 'desc'
109+
}
110+
106111
export interface SearchV2Options {
112+
/**
113+
* The number of files you want to be returned.
114+
* @default 1000
115+
*/
107116
limit?: number
117+
118+
/**
119+
* The prefix search string to filter files by.
120+
*/
108121
prefix?: string
122+
123+
/**
124+
* The cursor used for pagination. Pass the value received from nextCursor of the previous request.
125+
*/
109126
cursor?: string
127+
128+
/**
129+
* Whether to emulate a hierarchical listing of objects using delimiters.
130+
*
131+
* - When `false` (default), all objects are listed as flat key/value pairs.
132+
* - When `true`, the response groups objects by delimiter, making it appear
133+
* like a file/folder hierarchy.
134+
*
135+
* @default false
136+
*/
110137
with_delimiter?: boolean
138+
139+
/**
140+
* The column and order to sort by
141+
* @default 'name asc'
142+
*/
143+
sortBy?: SortByV2
111144
}
112145

113146
export interface SearchV2Result {
114147
hasNext: boolean
115148
folders: { name: string }[]
116149
objects: FileObject[]
150+
nextCursor?: string
117151
}
118152

119153
export interface FetchParameters {

packages/core/storage-js/test/__snapshots__/storageApi.test.ts.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ exports[`bucket api delete bucket 1`] = `
2323

2424
exports[`bucket api empty bucket 1`] = `
2525
{
26-
"message": "Successfully emptied",
26+
"message": "Empty bucket has been queued. Completion may take up to an hour.",
2727
}
2828
`;
2929

packages/core/storage-js/test/storageFileApi.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StorageClient } from '../src/index'
1+
import { SortByV2, StorageClient } from '../src/index'
22
import * as fsp from 'fs/promises'
33
import * as fs from 'fs'
44
import * as path from 'path'
@@ -357,6 +357,76 @@ describe('Object API', () => {
357357
)
358358
})
359359

360+
test('list objects V2 - folders', async () => {
361+
await storage.from(bucketName).upload(uploadPath, file)
362+
const res = await storage.from(bucketName).listV2({ with_delimiter: true })
363+
364+
expect(res.error).toBeNull()
365+
expect(res.data).toEqual(
366+
expect.objectContaining({
367+
hasNext: false,
368+
folders: expect.arrayContaining([
369+
expect.objectContaining({ key: 'testpath', name: 'testpath/' }),
370+
]),
371+
objects: [],
372+
})
373+
)
374+
})
375+
376+
test('list objects V2 - paginated', async () => {
377+
const fileSuffixes = ['zz', 'bb', 'xx', 'ww', 'cc', 'aa', 'yy', 'oo']
378+
for (const suffix of fileSuffixes) {
379+
await storage.from(bucketName).upload(uploadPath + suffix, file)
380+
}
381+
382+
const testCases: { expectedSuffixes: string[]; sortBy?: SortByV2 }[] = [
383+
{
384+
expectedSuffixes: fileSuffixes.slice().sort(),
385+
// default sortBy = name asc
386+
},
387+
{
388+
expectedSuffixes: fileSuffixes.slice().sort().reverse(),
389+
sortBy: { column: 'name', order: 'desc' },
390+
},
391+
{
392+
expectedSuffixes: fileSuffixes.slice(),
393+
sortBy: { column: 'created_at', order: 'asc' },
394+
},
395+
{
396+
expectedSuffixes: fileSuffixes.slice().reverse(),
397+
sortBy: { column: 'created_at', order: 'desc' },
398+
},
399+
]
400+
401+
for (const { expectedSuffixes, sortBy } of testCases) {
402+
let cursor: string | undefined
403+
let hasNext = true
404+
let pages = 0
405+
while (hasNext) {
406+
const res = await storage.from(bucketName).listV2({
407+
prefix: 'testpath/',
408+
with_delimiter: true,
409+
limit: 2,
410+
cursor,
411+
sortBy,
412+
})
413+
414+
expect(res.error).toBeNull()
415+
expect(res.data?.objects).toHaveLength(2)
416+
expect(res.data?.objects).toEqual(
417+
expectedSuffixes
418+
.splice(0, 2)
419+
.map((v) => expect.objectContaining({ name: uploadPath + v }))
420+
)
421+
422+
hasNext = res.data?.hasNext || false
423+
cursor = res.data?.nextCursor
424+
pages++
425+
}
426+
expect(pages).toBe(4)
427+
}
428+
})
429+
360430
test('move object to different path', async () => {
361431
const newPath = `testpath/file-moved-${Date.now()}.txt`
362432
await storage.from(bucketName).upload(uploadPath, file)

0 commit comments

Comments
 (0)