8000 feat: add `list()` method (#82) · netlify/blobs@00db5ff · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 00db5ff

Browse files
feat: add list() method (#82)
1 parent af867f8 commit 00db5ff

File tree

8 files changed

+979
-54
lines changed

8 files changed

+979
-54
lines changed

README.md

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ second parameter, with one of the following values:
186186
If an object with the given key is not found, `null` is returned.
187187

188188
```javascript
189-
const entry = await blobs.get('some-key', { type: 'json' })
189+
const entry = await store.get('some-key', { type: 'json' })
190190

191191
console.log(entry)
192192
```
@@ -209,7 +209,7 @@ second parameter, with one of the following values:
209209
If an object with the given key is not found, `null` is returned.
210210

211211
```javascript
212-
const blob = await blobs.getWithMetadata('some-key', { type: 'json' })
212+
const blob = await store.getWithMetadata('some-key', { type: 'json' })
213213

214214
console.log(blob.data, blob.etag, blob.metadata)
215215
```
@@ -223,7 +223,7 @@ const cachedETag = getFromMockCache('my-key')
223223

224224
// Get entry from the blob store only if its ETag is different from the one you
225225
// have locally, which means the entry has changed since you last obtained it
226-
const { data, etag, fresh } = await blobs.getWithMetadata('some-key', { etag: cachedETag })
226+
const { data, etag, fresh } = await store.getWithMetadata('some-key', { etag: cachedETag })
227227

228228
if (fresh) {
229229
// `data` is `null` because the local blob is fresh
@@ -240,7 +240,7 @@ Creates an object with the given key and value.
240240
If an entry with the given key already exists, its value is overwritten.
241241

242242
```javascript
243-
await blobs.set('some-key', 'This is a string value')
243+
await store.set('some-key', 'This is a string value')
244244
```
245245

246246
### `setJSON(key: string, value: any, { metadata?: object }): Promise<void>`
@@ -250,7 +250,7 @@ Convenience method for creating a JSON-serialized object with the given key.
250250
If an entry with the given key already exists, its value is overwritten.
251251

252252
```javascript
253-
await blobs.setJSON('some-key', {
253+
await store.setJSON('some-key', {
254254
foo: 'bar',
255255
})
256256
```
@@ -260,9 +260,86 @@ await blobs.setJSON('some-key', {
260260
Deletes an object with the given key, if one exists.
261261

262262
```javascript
263-
await blobs.delete('my-key')
263+
await store.delete('my-key')
264264
```
265265

266+
### `list(options?: { cursor?: string, directories?: boolean, paginate?: boolean. prefix?: string }): Promise<{ blobs: BlobResult[], directories: string[] }>`
267+
268+
Returns a list of blobs in a given store.
269+
270+
```javascript
271+
const { blobs } = await store.list()
272+
273+
// [ { etag: 'etag1', key: 'some-key' }, { etag: 'etag2', key: 'another-key' } ]
274+
console.log(blobs)
275+
```
276+
277+
To filter down the entries that should be returned, an optional `prefix` parameter can be supplied. When used, only the
278+
entries whose key starts with that prefix are returned.
279+
280+
```javascript
281+
const { blobs } = await store.list({ prefix: 'some' })
282+
283+
// [ { etag: 'etag1', key: 'some-key' } ]
284+
console.log(blobs)
285+
```
286+
287+
Optionally, you can choose to group blobs together under a common prefix and then browse them hierarchically when
288+
listing a store, just like grouping files in a directory. To do this, use the `/` character in your keys to group them
289+
into directories.
290+
291+
Take the following list of keys as an example:
292+
293+
```
294+
cats/garfield.jpg
295+
cats/tom.jpg
296+
mice/jerry.jpg
297+
mice/mickey.jpg
298+
pink-panther.jpg
299+
```
300+
301+
By default, calling `store.list()` will return all five keys.
302+
303+
```javascript
304+
const { blobs } = await store.list()
305+
306+
// [
307+
// { etag: "etag1", key: "cats/garfield.jpg" },
308+
// { etag: "etag2", key: "cats/tom.jpg" },
309+
// { etag: "etag3", key: "mice/jerry.jpg" },
310+
// { etag: "etag4", key: "mice/mickey.jpg" },
311+
// { etag: "etag5", key: "pink-panther.jpg" },
312+
// ]
313+
console.log(blobs)
314+
```
315+
316+
But if you want to list entries hierarchically, use the `directories` parameter.
317+
318+
```javascript
319+
const { blobs, directories } = await store.list({ directories: true })
320+
321+
// [ { etag: "etag1", key: "pink-panther.jpg" } ]
322+
console.log(blobs)
323+
324+
// [ "cats", "mice" ]
325+
console.log(directories)
326+
```
327+
328+
To drill down into a directory and get a list of its items, you can use the directory name as the `prefix` value.
329+
330+
```javascript
331+
const { blobs, directories } = await store.list({ directories: true, prefix: 'cats/' })
332+
333+
// [ { etag: "etag1", key: "cats/garfield.jpg" }, { etag: "etag2", key: "cats/tom.jpg" } ]
334+
console.log(blobs)
335+
336+
// [ ]
337+
console.log(directories)
338+
```
339+
340+
Note that we're only interested in entries under the `cats` directory, which is why we're using a trailing slash.
341+
Without it, other keys like `catsuit` would also match.
342+
266343
## Contributing
267344

268345
Contributions are welcome! If you encounter any issues or have suggestions for improvements, please open an issue or

src/backend/list.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface ListResponse {
2+
blobs?: ListResponseBlob[]
3+
directories?: string[]
4+
next_cursor?: string
5+
}
6+
7+
export interface ListResponseBlob {
8+
etag: string
9+
last_modified: string
10+
size: number
11+
key: string
12+
}

src/client.ts

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { BlobInput, Fetcher, HTTPMethod } from './types.ts'
66
interface MakeStoreRequestOptions {
77
body?: BlobInput | null
88
headers?: Record<string, string>
9-
key: string
9+
key?: string
1010
metadata?: Metadata
1111
method: HTTPMethod
12+
parameters?: Record<string, string>
1213
storeName: string
1314
}
1415

@@ -20,6 +21,14 @@ export interface ClientOptions {
2021
token: string
2122
}
2223

24+
interface GetFinalRequestOptions {
25+
key: string | undefined
26+
metadata?: Metadata
27+
method: string
28+
parameters?: Record<string, string>
29+
storeName: string
30+
}
31+
2332
export class Client {
2433
private apiURL?: string
2534
private edgeURL?: string
@@ -41,7 +50,7 @@ export class Client {
4150
}
4251
}
4352

44-
private async getFinalRequest(storeName: string, key: string, method: string, metadata?: Metadata) {
53+
private async getFinalRequest({ key, metadata, method, parameters = {}, storeName }: GetFinalRequestOptions) {
4554
const encodedMetadata = encodeMetadata(metadata)
4655

4756
if (this.edgeURL) {
@@ -53,38 +62,72 @@ export class Client {
5362
headers[METADATA_HEADER_EXTERNAL] = encodedMetadata
5463
}
5564

65+
const path = key ? `/${this.siteID}/${storeName}/${key}` : `/${this.siteID}/${storeName}`
66+
const url = new URL(path, this.edgeURL)
67+
68+
for (const key in parameters) {
69+
url.searchParams.set(key, parameters[key])
70+
}
71+
5672
return {
5773
headers,
58-
url: `${this.edgeURL}/${this.siteID}/${storeName}/${key}`,
74+
url: url.toString(),
5975
}
6076
}
6177

62-
const apiURL = `${this.apiURL ?? 'https://api.netlify.com'}/api/v1/sites/${
63-
this.siteID
64-
}/blobs/${key}?context=${storeName}`
6578
const apiHeaders: Record<string, string> = { authorization: `Bearer ${this.token}` }
79+
const url = new URL(`/api/v1/sites/${this.siteID}/blobs`, this.apiURL ?? 'https://api.netlify.com')
80+
81+
for (const key in parameters) {
82+
url.searchParams.set(key, parameters[key])
83+
}
84+
85+
url.searchParams.set('context', storeName)
86+
87+
if (key === undefined) {
88+
return {
89+
headers: apiHeaders,
90+
url: url.toString(),
91+
}
92+
}
93+
94+
url.pathname += `/${key}`
6695

6796
if (encodedMetadata) {
6897
apiHeaders[METADATA_HEADER 2851 _EXTERNAL] = encodedMetadata
6998
}
7099

71-
const res = await this.fetch(apiURL, { headers: apiHeaders, method })
100+
const res = await this.fetch(url.toString(), { headers: apiHeaders, method })
72101

73102
if (res.status !== 200) {
74-
throw new Error(`${method} operation has failed: API returned a ${res.status} response`)
103+
throw new Error(`Netlify Blobs has generated an internal error: ${res.status} response`)
75104
}
76105

77-
const { url } = await res.json()
106+
const { url: signedURL } = await res.json()
78107
const userHeaders = encodedMetadata ? { [METADATA_HEADER_INTERNAL]: encodedMetadata } : undefined
79108

80109
return {
81110
headers: userHeaders,
82-
url,
111+
url: signedURL,
83112
}
84113
}
85114

86-
async makeRequest({ body, headers: extraHeaders, key, metadata, method, storeName }: MakeStoreRequestOptions) {
87-
const { headers: baseHeaders = {}, url } = await this.getFinalRequest(storeName, key, method, metadata)
115+
async makeRequest({
116+
body,
117+
headers: extraHeaders,
118+
key,
119+
metadata,
120+
method,
121+
parameters,
122+
storeName,
123+
}: MakeStoreRequestOptions) {
124+
const { headers: baseHeaders = {}, url } = await this.getFinalRequest({
125+
key,
126+
metadata,
127+
method,
128+
parameters,
129+
storeName,
130+
})
88131
const headers: Record<string, string> = {
89132
...baseHeaders,
90133
...extraHeaders,
@@ -106,17 +149,7 @@ export class Client {
106149
options.duplex = 'half'
107150
}
108151

109-
const res = await fetchAndRetry(this.fetch, url, options)
110-
111-
if (res.status === 404 && method === HTTPMethod.GET) {
112-
return null
113-
}
114-
115-
if (res.status !== 200 && res.status !== 304) {
116-
throw new Error(`${method} operation has failed: store returned a ${res.status} response`)
117-
}
118-
119-
return res
152+
return fetchAndRetry(this.fetch, url, options)
120153
}
121154
}
122155

0 commit comments

Comments
 (0)
0