Skip to content

Commit fc02863

Browse files
authored
Merge pull request #1087 from supabase-community/bucket-pagination
Add support for bucket pagination and sorting
2 parents e4cdbee + 4a37c6a commit fc02863

File tree

8 files changed

+446
-12
lines changed

8 files changed

+446
-12
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.github.jan.supabase.storage
2+
3+
import io.ktor.http.parameters
4+
5+
/**
6+
* A filter builder for [Storage.listBuckets]
7+
*/
8+
class BucketFilter {
9+
10+
/**
11+
* The maximum number of buckets to return. If null, no limit is applied.
12+
*/
13+
var limit: Int? = null
14+
15+
/**
16+
* The number of buckets to skip before returning results. Useful for pagination.
17+
*/
18+
var offset: Int? = null
19+
20+
/**
21+
* A search query to filter buckets by name. If null, no search filter is applied.
22+
*/
23+
var search: String? = null
24+
25+
/**
26+
* The sort order for the results. Can be [SortOrder.ASC] (ascending) or [SortOrder.DESC] (descending).
27+
* If null, the default sort order from the API is used.
28+
*/
29+
private var sortOrder: SortOrder? = null
30+
31+
/**
32+
* The column to sort the results by. If null, the default sort column from the API is used.
33+
*/
34+
private var sortColumn: SortColumn? = null
35+
36+
/**
37+
* Sets the sorting criteria for the bucket list results
38+
* @param column The column to sort by
39+
* @param order The sort order (ascending or descending)
40+
*/
41+
fun sortBy(column: SortColumn, order: SortOrder) {
42+
sortColumn = column
43+
sortOrder = order
44+
}
45+
46+
internal fun build() = parameters {
47+
limit?.let { set("limit", it.toString()) }
48+
offset?.let { set("offset", it.toString()) }
49+
search?.let { set("search", it) }
50+
sortOrder?.let { set("sortOrder", it.name.lowercase()) }
51+
sortColumn?.let { set("sortColumn", it.name.lowercase()) }
52+
}
53+
54+
/**
55+
* Represents the available columns for sorting bucket results.
56+
*/
57+
enum class SortColumn {
58+
/** Sort by bucket ID */
59+
ID,
60+
61+
/** Sort by bucket name */
62+
NAME,
63+
64+
/** Sort by bucket creation timestamp */
65+
CREATED_AT,
66+
67+
/** Sort by bucket last updated timestamp */
68+
UPDATED_AT
69+
}
70+
71+
}

Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketListFilter.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ class BucketListFilter {
2929

3030
/**
3131
* Sorts the result by the given [column] in the given [order]
32+
* @param column The column to sort by
33+
* @param order The sort order (ascending or descending)
3234
*/
33-
fun sortBy(column: String, order: String) {
35+
fun sortBy(column: String, order: SortOrder) {
3436
this.column = column
35-
this.order = order
37+
this.order = order.name.lowercase()
3638
}
3739

3840
@SupabaseInternal
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.jan.supabase.storage
2+
3+
/**
4+
* Represents the sort order for query results.
5+
*/
6+
enum class SortOrder {
7+
/**
8+
* Ascending order
9+
*/
10+
ASC,
11+
12+
/**
13+
* Descending order
14+
*/
15+
DESC
16+
}

Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,33 @@ interface Storage : MainPlugin<Storage.Config>, CustomSerializationPlugin {
7575
* @throws HttpRequestTimeoutException if the request timed out
7676
* @throws HttpRequestException on network related issues
7777
*/
78-
suspend fun retrieveBuckets(): List<Bucket>
78+
suspend fun listBuckets(filter: BucketFilter.() -> Unit = {}): List<Bucket>
79+
80+
/**
81+
* Returns all buckets in the storage
82+
* @throws RestException or one of its subclasses if receiving an error response
83+
* @throws HttpRequestTimeoutException if the request timed out
84+
* @throws HttpRequestException on network related issues
85+
*/
86+
@Deprecated("Use listBuckets instead", ReplaceWith("listBuckets()"))
87+
suspend fun retrieveBuckets(): List<Bucket> = listBuckets()
88+
89+
/**
90+
* Retrieves a bucket by its [bucketId]
91+
* @throws RestException or one of its subclasses if receiving an error response
92+
* @throws HttpRequestTimeoutException if the request timed out
93+
* @throws HttpRequestException on network related issues
94+
*/
95+
suspend fun getBucket(bucketId: String): Bucket?
7996

8097
/**
8198
* Retrieves a bucket by its [bucketId]
8299
* @throws RestException or one of its subclasses if receiving an error response
83100
* @throws HttpRequestTimeoutException if the request timed out
84101
* @throws HttpRequestException on network related issues
85102
*/
86-
suspend fun retrieveBucketById(bucketId: String): Bucket?
103+
@Deprecated("Use getBucket instead", ReplaceWith("getBucket(bucketId)"))
104+
suspend fun retrieveBucketById(bucketId: String): Bucket? = getBucket(bucketId)
87105

88106
/**
89107
* Empties a bucket by its [bucketId]
@@ -200,9 +218,14 @@ internal class StorageImpl(override val supabaseClient: SupabaseClient, override
200218

201219
private val resumableClients = AtomicMutableMap<String, BucketApi>()
202220

203-
override suspend fun retrieveBuckets(): List<Bucket> = api.get("bucket").safeBody()
221+
override suspend fun listBuckets(filter: BucketFilter.() -> Unit): List<Bucket> {
222+
val response = api.get("bucket") {
223+
url.parameters.appendAll(BucketFilter().apply(filter).build())
224+
}
225+
return response.safeBody()
226+
}
204227

205-
override suspend fun retrieveBucketById(bucketId: String): Bucket? = api.get("bucket/$bucketId").safeBody()
228+
override suspend fun getBucket(bucketId: String): Bucket? = api.get("bucket/$bucketId").safeBody()
206229

207230
override suspend fun deleteBucket(bucketId: String) {
208231
api.delete("bucket/$bucketId")

Storage/src/commonTest/kotlin/BucketApiTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import io.github.jan.supabase.storage.BucketApi
44
import io.github.jan.supabase.storage.FileObjectV2
55
import io.github.jan.supabase.storage.FileUploadResponse
66
import io.github.jan.supabase.storage.ImageTransformation
7+
import io.github.jan.supabase.storage.SortOrder
78
import io.github.jan.supabase.storage.Storage
89
import io.github.jan.supabase.storage.resumable.MemoryResumableCache
910
import io.github.jan.supabase.storage.storage
@@ -451,7 +452,7 @@ class BucketApiTest {
451452
limit = expectedLimit
452453
offset = expectedOffset
453454
search = expectedSearch
454-
sortBy(expectedColumn, expectedOrder)
455+
sortBy(expectedColumn, SortOrder.ASC)
455456
}
456457
// assertContentEquals(expectedData, data, "Data should be $expectedData")
457458
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import io.github.jan.supabase.storage.BucketFilter
2+
import io.github.jan.supabase.storage.SortOrder
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertNull
6+
7+
class BucketFilterTest {
8+
9+
@Test
10+
fun testBucketFilterWithAllParameters() {
11+
val filter = BucketFilter().apply {
12+
limit = 10
13+
offset = 5
14+
search = "test"
15+
sortBy(BucketFilter.SortColumn.NAME, SortOrder.ASC)
16+
}
17+
val params = filter.build()
18+
assertEquals("10", params["limit"])
19+
assertEquals("5", params["offset"])
20+
assertEquals("test", params["search"])
21+
assertEquals("asc", params["sortOrder"])
22+
assertEquals("name", params["sortColumn"])
23+
}
24+
25+
@Test
26+
fun testBucketFilterEmpty() {
27+
val filter = BucketFilter()
28+
val params = filter.build()
29+
assertNull(params["limit"])
30+
assertNull(params["offset"])
31+
assertNull(params["search"])
32+
assertNull(params["sortOrder"])
33+
assertNull(params["sortColumn"])
34+
}
35+
36+
@Test
37+
fun testBucketFilterIndividualParameters() {
38+
// Test limit only
39+
var filter = BucketFilter().apply { limit = 20 }
40+
var params = filter.build()
41+
assertEquals("20", params["limit"])
42+
assertNull(params["offset"])
43+
44+
// Test offset only
45+
filter = BucketFilter().apply { offset = 15 }
46+
params = filter.build()
47+
assertEquals("15", params["offset"])
48+
assertNull(params["limit"])
49+
50+
// Test search only
51+
filter = BucketFilter().apply { search = "my-bucket" }
52+
params = filter.build()
53+
assertEquals("my-bucket", params["search"])
54+
assertNull(params["limit"])
55+
}
56+
57+
@Test
58+
fun testBucketFilterSortColumns() {
59+
// Test all sort columns with both orders
60+
val columns = listOf(
61+
BucketFilter.SortColumn.ID to "id",
62+
BucketFilter.SortColumn.NAME to "name",
63+
BucketFilter.SortColumn.CREATED_AT to "created_at",
64+
BucketFilter.SortColumn.UPDATED_AT to "updated_at"
65+
)
66+
67+
for ((column, expectedName) in columns) {
68+
// Test ascending
69+
var filter = BucketFilter().apply { sortBy(column, SortOrder.ASC) }
70+
var params = filter.build()
71+
assertEquals(expectedName, params["sortColumn"])
72+
assertEquals("asc", params["sortOrder"])
73+
74+
// Test descending
75+
filter = BucketFilter().apply { sortBy(column, SortOrder.DESC) }
76+
params = filter.build()
77+
assertEquals(expectedName, params["sortColumn"])
78+
assertEquals("desc", params["sortOrder"])
79+
}
80+
}
81+
82+
@Test
83+
fun testBucketFilterEdgeCases() {
84+
// Zero values
85+
var filter = BucketFilter().apply {
86+
limit = 0
87+
offset = 0
88+
}
89+
var params = filter.build()
90+
assertEquals("0", params["limit"])
91+
assertEquals("0", params["offset"])
92+
93+
// Empty search string
94+
filter = BucketFilter().apply { search = "" }
95+
params = filter.build()
96+
assertEquals("", params["search"])
97+
98+
// Special characters in search
99+
filter = BucketFilter().apply { search = "test-bucket_123" }
100+
params = filter.build()
101+
assertEquals("test-bucket_123", params["search"])
102+
103+
// Large numbers
104+
filter = BucketFilter().apply {
105+
limit = 1000
106+
offset = 5000
107+
}
108+
params = filter.build()
109+
assertEquals("1000", params["limit"])
110+
assertEquals("5000", params["offset"])
111+
}
112+
113+
@Test
114+
fun testBucketFilterCombinations() {
115+
// Limit and offset
116+
var filter = BucketFilter().apply {
117+
limit = 25
118+
offset = 50
119+
}
120+
var params = filter.build()
121+
assertEquals("25", params["limit"])
122+
assertEquals("50", params["offset"])
123+
assertNull(params["search"])
124+
125+
// Search and sort
126+
filter = BucketFilter().apply {
127+
search = "images"
128+
sortBy(BucketFilter.SortColumn.UPDATED_AT, SortOrder.ASC)
129+
}
130+
params = filter.build()
131+
assertEquals("images", params["search"])
132+
assertEquals("updated_at", params["sortColumn"])
133+
assertEquals("asc", params["sortOrder"])
134+
135+
// Pagination with sort
136+
filter = BucketFilter().apply {
137+
limit = 10
138+
offset = 30
139+
sortBy(BucketFilter.SortColumn.NAME, SortOrder.ASC)
140+
}
141+
params = filter.build()
142+
assertEquals("10", params["limit"])
143+
assertEquals("30", params["offset"])
144+
assertEquals("name", params["sortColumn"])
145+
assertEquals("asc", params["sortOrder"])
146+
}
147+
148+
}

0 commit comments

Comments
 (0)