From 03ea7cd7e21f7affbe9489f595b01d858b4a584b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 12:46:20 +0000 Subject: [PATCH 1/6] feat!: add `listStores` method BREAKING CHANGE: Site-scoped created with previous versions of the client will not be accessible. --- .eslintrc.cjs | 1 + src/backend/list_stores.ts | 4 + src/client.ts | 28 ++-- src/main.ts | 1 + src/store.ts | 13 +- src/store_list.test.ts | 276 +++++++++++++++++++++++++++++++++++++ src/store_list.ts | 92 +++++++++++++ 7 files changed, 397 insertions(+), 18 deletions(-) create mode 100644 src/backend/list_stores.ts create mode 100644 src/store_list.test.ts create mode 100644 src/store_list.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 66cc2ba..9819c91 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { 'unicorn/prefer-ternary': 'off', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], + 'func-style': 'off', }, overrides: [ ...overrides, diff --git a/src/backend/list_stores.ts b/src/backend/list_stores.ts new file mode 100644 index 0000000..721c0a3 --- /dev/null +++ b/src/backend/list_stores.ts @@ -0,0 +1,4 @@ +export interface ListStoresResponse { + stores: string[] + next_cursor?: string +} diff --git a/src/client.ts b/src/client.ts index 9bd980a..259db44 100644 --- a/src/client.ts +++ b/src/client.ts @@ -12,7 +12,7 @@ interface MakeStoreRequestOptions { metadata?: Metadata method: HTTPMethod parameters?: Record - storeName: string + storeName?: string } export interface ClientOptions { @@ -31,7 +31,7 @@ interface GetFinalRequestOptions { metadata?: Metadata method: string parameters?: Record - storeName: string + storeName?: string } export class Client { @@ -70,6 +70,16 @@ export class Client { const encodedMetadata = encodeMetadata(metadata) const consistency = opConsistency ?? this.consistency + let urlPath = `/${this.siteID}` + + if (storeName) { + urlPath += `/${storeName}` + } + + if (key) { + urlPath += `/${key}` + } + if (this.edgeURL) { if (consistency === 'strong' && !this.uncachedEdgeURL) { throw new BlobsConsistencyError() @@ -83,8 +93,7 @@ export class Client { headers[METADATA_HEADER_INTERNAL] = encodedMetadata } - const path = key ? `/${this.siteID}/${storeName}/${key}` : `/${this.siteID}/${storeName}` - const url = new URL(path, consistency === 'strong' ? this.uncachedEdgeURL : this.edgeURL) + const url = new URL(urlPath, consistency === 'strong' ? this.uncachedEdgeURL : this.edgeURL) for (const key in parameters) { url.searchParams.set(key, parameters[key]) @@ -97,23 +106,22 @@ export class Client { } const apiHeaders: Record = { authorization: `Bearer ${this.token}` } - const url = new URL(`/api/v1/blobs/${this.siteID}/${storeName}`, this.apiURL ?? 'https://api.netlify.com') + const url = new URL(`/api/v1/blobs${urlPath}`, this.apiURL ?? 'https://api.netlify.com') for (const key in parameters) { url.searchParams.set(key, parameters[key]) } - // If there is no key, we're dealing with the list endpoint, which is - // implemented directly in the Netlify API. - if (key === undefined) { + // If there is no store name, we're listing stores. If there's no key, + // we're listing blobs. Both operations are implemented directly in the + // Netlify API. + if (storeName === undefined || key === undefined) { return { headers: apiHeaders, url: url.toString(), } } - url.pathname += `/${key}` - if (encodedMetadata) { apiHeaders[METADATA_HEADER_EXTERNAL] = encodedMetadata } diff --git a/src/main.ts b/src/main.ts index fb71d5a..2efa7fe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ export { getDeployStore, getStore } from './store_factory.ts' +export { listStores } from './store_list.ts' export { BlobsServer } from './server.ts' export type { Store, diff --git a/src/store.ts b/src/store.ts index d003b82..ae82a2e 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,6 +7,9 @@ import { getMetadataFromResponse, Metadata } from './metadata.ts' import { BlobInput, HTTPMethod } from './types.ts' import { BlobsInternalError, collectIterator } from './util.ts' +export const DEPLOY_STORE_PREFIX = 'deploy:' +export const SITE_STORE_PREFIX = 'site:' + interface BaseStoreOptions { client: Client consistency?: ConsistencyMode @@ -64,21 +67,19 @@ export type BlobResponseType = 'arrayBuffer' | 'blob' | 'json' | 'stream' | 'tex export class Store { private client: Client - private consistency: ConsistencyMode private name: string constructor(options: StoreOptions) { this.client = options.client - this.consistency = options.consistency ?? 'eventual' if ('deployID' in options) { Store.validateDeployID(options.deployID) - this.name = `deploy:${options.deployID}` + this.name = DEPLOY_STORE_PREFIX + options.deployID } else { Store.validateStoreName(options.name) - this.name = options.name + this.name = SITE_STORE_PREFIX + options.name } } @@ -349,10 +350,6 @@ export class Store { } private static validateStoreName(name: string) { - if (name.startsWith('deploy:') || name.startsWith('deploy%3A1')) { - throw new Error('Store name must not start with the `deploy:` reserved keyword.') - } - if (name.includes('/') || name.includes('%2F')) { throw new Error('Store name must not contain forward slashes (/).') } diff --git a/src/store_list.test.ts b/src/store_list.test.ts new file mode 100644 index 0000000..b80e49e --- /dev/null +++ b/src/store_list.test.ts @@ -0,0 +1,276 @@ +import { Buffer } from 'node:buffer' +import { env, version as nodeVersion } from 'node:process' + +import semver from 'semver' +import { describe, test, expect, beforeAll, afterEach } from 'vitest' + +import { MockFetch } from '../test/mock_fetch.js' + +import type { ListStoresResponse } from './backend/list_stores.js' +import { listStores } from './main.js' + +beforeAll(async () => { + if (semver.lt(nodeVersion, '18.0.0')) { + const nodeFetch = await import('node-fetch') + + // @ts-expect-error Expected type mismatch between native implementation and node-fetch + globalThis.fetch = nodeFetch.default + // @ts-expect-error Expected type mismatch between native implementation and node-fetch + globalThis.Request = nodeFetch.Request + // @ts-expect-error Expected type mismatch between native implementation and node-fetch + globalThis.Response = nodeFetch.Response + // @ts-expect-error Expected type mismatch between native implementation and node-fetch + globalThis.Headers = nodeFetch.Headers + } +}) + +afterEach(() => { + delete env.NETLIFY_BLOBS_CONTEXT + delete globalThis.netlifyBlobsContext +}) + +const siteID = '9a003659-aaaa-0000-aaaa-63d3720d8621' +const apiToken = 'some token' +const edgeToken = 'some other token' +const edgeURL = 'https://edge.netlify' + +describe('listStores', () => { + describe('With API credentials', () => { + test('Lists site stores', async () => { + const mockStore = new MockFetch().get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store1', 'site:store2', 'deploy:deploy1'] })), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A`, + }) + + globalThis.fetch = mockStore.fetch + + const { stores } = await listStores({ + token: apiToken, + siteID, + }) + + expect(stores).toStrictEqual(['store1', 'store2']) + expect(mockStore.fulfilled).toBeTruthy() + }) + + test('Paginates automatically', async () => { + const mockStore = new MockFetch() + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store1', 'site:store2', 'deploy:6527dfab35be400008332a1a'], + next_cursor: 'cursor_1', + }), + ), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A`, + }) + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + next_cursor: 'cursor_2', + }), + ), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A&cursor=cursor_1`, + }) + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store5', 'deploy:6527dfab35be400008332a1c'] })), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A&cursor=cursor_2`, + }) + + globalThis.fetch = mockStore.fetch + + const { stores } = await listStores({ + token: apiToken, + siteID, + }) + + expect(stores).toStrictEqual(['store1', 'store2', 'store3', 'store4', 'store5']) + expect(mockStore.fulfilled).toBeTruthy() + }) + + test('Supports manual pagination', async () => { + const mockStore = new MockFetch() + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store1', 'site:store2', 'deploy:6527dfab35be400008332a1a'], + next_cursor: 'cursor_1', + }), + ), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A`, + }) + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + next_cursor: 'cursor_2', + }), + ), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A&cursor=cursor_1`, + }) + .get({ + headers: { authorization: `Bearer ${apiToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store5', 'deploy:6527dfab35be400008332a1c'] })), + url: `https://api.netlify.com/api/v1/blobs/${siteID}?prefix=site%3A&cursor=cursor_2`, + }) + + globalThis.fetch = mockStore.fetch + + const result: ListStoresResponse = { + stores: [], + } + + for await (const entry of listStores({ token: apiToken, siteID, paginate: true })) { + result.stores.push(...entry.stores) + } + + expect(result.stores).toStrictEqual(['store1', 'store2', 'store3', 'store4', 'store5']) + expect(mockStore.fulfilled).toBeTruthy() + }) + }) + + describe('With edge credentials', () => { + test('Lists site stores', async () => { + const mockStore = new MockFetch().get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store1', 'site:store2', 'deploy:deploy1'] })), + url: `https://edge.netlify/${siteID}?prefix=site%3A`, + }) + + globalThis.fetch = mockStore.fetch + + const { stores } = await listStores({ + edgeURL, + token: edgeToken, + siteID, + }) + + expect(stores).toStrictEqual(['store1', 'store2']) + expect(mockStore.fulfilled).toBeTruthy() + }) + + test('Loads credentials from the environment', async () => { + const mockStore = new MockFetch().get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store1', 'site:store2', 'deploy:deploy1'] })), + url: `https://edge.netlify/${siteID}?prefix=site%3A`, + }) + + globalThis.fetch = mockStore.fetch + + const context = { + edgeURL, + siteID, + token: edgeToken, + } + + env.NETLIFY_BLOBS_CONTEXT = Buffer.from(JSON.stringify(context)).toString('base64') + + const { stores } = await listStores() + + expect(stores).toStrictEqual(['store1', 'store2']) + expect(mockStore.fulfilled).toBeTruthy() + }) + + test('Paginates automatically', async () => { + const mockStore = new MockFetch() + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store1', 'site:store2', 'deploy:6527dfab35be400008332a1a'], + next_cursor: 'cursor_1', + }), + ), + url: `https://edge.netlify/${siteID}?prefix=site%3A`, + }) + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + next_cursor: 'cursor_2', + }), + ), + url: `https://edge.netlify/${siteID}?prefix=site%3A&cursor=cursor_1`, + }) + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store5', 'deploy:6527dfab35be400008332a1c'] })), + url: `https://edge.netlify/${siteID}?prefix=site%3A&cursor=cursor_2`, + }) + + globalThis.fetch = mockStore.fetch + + const context = { + edgeURL, + siteID, + token: edgeToken, + } + + env.NETLIFY_BLOBS_CONTEXT = Buffer.from(JSON.stringify(context)).toString('base64') + + const { stores } = await listStores() + + expect(stores).toStrictEqual(['store1', 'store2', 'store3', 'store4', 'store5']) + expect(mockStore.fulfilled).toBeTruthy() + }) + + test('Supports manual pagination', async () => { + const mockStore = new MockFetch() + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store1', 'site:store2', 'deploy:6527dfab35be400008332a1a'], + next_cursor: 'cursor_1', + }), + ), + url: `https://edge.netlify/${siteID}?prefix=site%3A`, + }) + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response( + JSON.stringify({ + stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + next_cursor: 'cursor_2', + }), + ), + url: `https://edge.netlify/${siteID}?prefix=site%3A&cursor=cursor_1`, + }) + .get({ + headers: { authorization: `Bearer ${edgeToken}` }, + response: new Response(JSON.stringify({ stores: ['site:store5', 'deploy:6527dfab35be400008332a1c'] })), + url: `https://edge.netlify/${siteID}?prefix=site%3A&cursor=cursor_2`, + }) + + globalThis.fetch = mockStore.fetch + + const context = { + edgeURL, + siteID, + token: edgeToken, + } + + env.NETLIFY_BLOBS_CONTEXT = Buffer.from(JSON.stringify(context)).toString('base64') + + const result: ListStoresResponse = { + stores: [], + } + + for await (const entry of listStores({ paginate: true })) { + result.stores.push(...entry.stores) + } + + expect(result.stores).toStrictEqual(['store1', 'store2', 'store3', 'store4', 'store5']) + expect(mockStore.fulfilled).toBeTruthy() + }) + }) +}) diff --git a/src/store_list.ts b/src/store_list.ts new file mode 100644 index 0000000..4200194 --- /dev/null +++ b/src/store_list.ts @@ -0,0 +1,92 @@ +import { ListStoresResponse } from './backend/list_stores.ts' +import { Client, ClientOptions, getClientOptions } from './client.ts' +import { getEnvironmentContext } from './environment.ts' +import { DEPLOY_STORE_PREFIX, SITE_STORE_PREFIX } from './store.ts' +import { HTTPMethod } from './types.ts' +import { collectIterator } from './util.ts' + +export function listStores(options: Partial & { paginate: true }): AsyncIterable +export function listStores(options?: Partial & { paginate?: false }): Promise +export function listStores( + options: Partial & { paginate?: boolean } = {}, +): AsyncIterable | Promise { + const context = getEnvironmentContext() + const clientOptions = getClientOptions(options, context) + const client = new Client(clientOptions) + const iterator = getListIterator(client, SITE_STORE_PREFIX) + + if (options.paginate) { + return iterator + } + + // eslint-disable-next-line promise/prefer-await-to-then + return collectIterator(iterator).then((results) => + results.reduce( + (acc, item) => ({ + ...acc, + stores: [...acc.stores, ...item.stores], + }), + { stores: [] }, + ), + ) +} + +const formatListStoreResponse = (rawStores: string[]) => + rawStores.reduce((acc, rawStore) => { + if (rawStore.startsWith(DEPLOY_STORE_PREFIX)) { + return acc + } + + if (rawStore.startsWith(SITE_STORE_PREFIX)) { + return [...acc, rawStore.slice(SITE_STORE_PREFIX.length)] + } + + return [...acc, rawStore] + }, [] as string[]) + +const getListIterator = (client: Client, prefix: string): AsyncIterable => { + const parameters: Record = { + prefix, + } + + return { + [Symbol.asyncIterator]() { + let currentCursor: string | null = null + let done = false + + return { + async next() { + if (done) { + return { done: true, value: undefined } + } + + const nextParameters = { ...parameters } + + if (currentCursor !== null) { + nextParameters.cursor = currentCursor + } + + const res = await client.makeRequest({ + method: HTTPMethod.GET, + parameters: nextParameters, + }) + const page = (await res.json()) as ListStoresResponse + + if (page.next_cursor) { + currentCursor = page.next_cursor + } else { + done = true + } + + return { + done: false, + value: { + ...page, + stores: formatListStoreResponse(page.stores), + }, + } + }, + } + }, + } +} From 5da639a7db385415f56adfe48da41bb452e090e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 12:47:29 +0000 Subject: [PATCH 2/6] chore: fix test --- src/store_list.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/store_list.test.ts b/src/store_list.test.ts index b80e49e..256eedc 100644 --- a/src/store_list.test.ts +++ b/src/store_list.test.ts @@ -70,7 +70,7 @@ describe('listStores', () => { headers: { authorization: `Bearer ${apiToken}` }, response: new Response( JSON.stringify({ - stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + stores: ['site:store3', 'site:store4', 'deploy:6527dfab35be400008332a1b'], next_cursor: 'cursor_2', }), ), @@ -109,7 +109,7 @@ describe('listStores', () => { headers: { authorization: `Bearer ${apiToken}` }, response: new Response( JSON.stringify({ - stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + stores: ['site:store3', 'site:store4', 'deploy:6527dfab35be400008332a1b'], next_cursor: 'cursor_2', }), ), @@ -195,7 +195,7 @@ describe('listStores', () => { headers: { authorization: `Bearer ${edgeToken}` }, response: new Response( JSON.stringify({ - stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + stores: ['site:store3', 'site:store4', 'deploy:6527dfab35be400008332a1b'], next_cursor: 'cursor_2', }), ), @@ -239,7 +239,7 @@ describe('listStores', () => { headers: { authorization: `Bearer ${edgeToken}` }, response: new Response( JSON.stringify({ - stores: ['site:store3', 'site:store4', 'deploy: 6527dfab35be400008332a1b'], + stores: ['site:store3', 'site:store4', 'deploy:6527dfab35be400008332a1b'], next_cursor: 'cursor_2', }), ), From a685c59d985305d3f38e055f66cc11b1d33a7b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 12:50:53 +0000 Subject: [PATCH 3/6] chore: add comment --- src/store.ts | 2 ++ src/store_list.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/store.ts b/src/store.ts index ae82a2e..bccced4 100644 --- a/src/store.ts +++ b/src/store.ts @@ -262,6 +262,8 @@ export class Store { return iterator } + // We can't use `async/await` here because that would make the signature + // incompatible with one of the overloads. // eslint-disable-next-line promise/prefer-await-to-then return collectIterator(iterator).then((items) => items.reduce( diff --git a/src/store_list.ts b/src/store_list.ts index 4200194..e3e884e 100644 --- a/src/store_list.ts +++ b/src/store_list.ts @@ -19,6 +19,8 @@ export function listStores( return iterator } + // We can't use `async/await` here because that would make the signature + // incompatible with one of the overloads. // eslint-disable-next-line promise/prefer-await-to-then return collectIterator(iterator).then((results) => results.reduce( From cea28d5139e0b49a3a5cd70f3e60cf8b24d947ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 12:52:36 +0000 Subject: [PATCH 4/6] chore: fix tests --- src/consistency.test.ts | 18 ++++---- src/lambda_compat.test.ts | 4 +- src/main.test.ts | 88 +++++++++++++++++++-------------------- 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/consistency.test.ts b/src/consistency.test.ts index 11ee0e5..2ccc51f 100644 --- a/src/consistency.test.ts +++ b/src/consistency.test.ts @@ -52,17 +52,17 @@ describe('Consistency configuration', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) .head({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { headers }), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value, { headers }), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -107,17 +107,17 @@ describe('Consistency configuration', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) .head({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { headers }), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value, { headers }), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -213,17 +213,17 @@ describe('Consistency configuration', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${uncachedEdgeURL}/${siteID}/production/${key}`, + url: `${uncachedEdgeURL}/${siteID}/site:production/${key}`, }) .head({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { headers }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value, { headers }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch diff --git a/src/lambda_compat.test.ts b/src/lambda_compat.test.ts index 8bd3a8f..10b2e71 100644 --- a/src/lambda_compat.test.ts +++ b/src/lambda_compat.test.ts @@ -48,12 +48,12 @@ describe('With edge credentials', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch diff --git a/src/main.test.ts b/src/main.test.ts index 464c3c3..69ee9ce 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -47,7 +47,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response(value), @@ -56,7 +56,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response(value), @@ -65,7 +65,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${complexKey}`, }) .get({ response: new Response(value), @@ -97,7 +97,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 404 }), @@ -120,7 +120,7 @@ describe('get', () => { const mockStore = new MockFetch().get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -142,7 +142,7 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 401 }), @@ -171,12 +171,12 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -201,7 +201,7 @@ describe('get', () => { const mockStore = new MockFetch().get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { status: 404 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -221,7 +221,7 @@ describe('get', () => { const mockStore = new MockFetch().get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { status: 401 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -361,7 +361,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { headers }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -383,7 +383,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 404 }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -406,7 +406,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { headers }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -439,7 +439,7 @@ describe('getMetadata', () => { const mockStore = new MockFetch().head({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { headers }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -476,7 +476,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -485,7 +485,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -518,7 +518,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response('Something went wrong', { status: 404 }), @@ -546,7 +546,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ response: new Response(value, { headers: responseHeaders }), @@ -585,7 +585,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: `${signedURL}b` })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ headers: { 'if-none-match': etags.wrong }, @@ -595,7 +595,7 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: `${signedURL}a` })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .get({ headers: { 'if-none-match': etags.right }, @@ -640,12 +640,12 @@ describe('getWithMetadata', () => { .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value, { headers: responseHeaders }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(value, { headers: responseHeaders }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -679,7 +679,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .put({ body: value, @@ -690,7 +690,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${complexKey}`, }) .put({ body: value, @@ -724,7 +724,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}`, 'netlify-blobs-metadata': encodedMetadata }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .put({ body: value, @@ -753,7 +753,7 @@ describe('set', () => { const mockStore = new MockFetch().put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -795,7 +795,7 @@ describe('set', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .put({ body: value, @@ -851,13 +851,13 @@ describe('set', () => { body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .put({ body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null), - url: `${edgeURL}/${siteID}/production/${complexKey}`, + url: `${edgeURL}/${siteID}/site:production/${complexKey}`, }) globalThis.fetch = mockStore.fetch @@ -880,7 +880,7 @@ describe('set', () => { body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null, { status: 401 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -905,25 +905,25 @@ describe('set', () => { body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null, { status: 500 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .put({ body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Error('Some network problem'), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .put({ body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null, { headers: { 'X-RateLimit-Reset': '10' }, status: 429 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) .put({ body: value, headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -949,7 +949,7 @@ describe('setJSON', () => { .put({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .put({ body: JSON.stringify({ value }), @@ -980,7 +980,7 @@ describe('setJSON', () => { body: JSON.stringify({ value }), headers: { authorization: `Bearer ${edgeToken}`, 'cache-control': 'max-age=0, stale-while-revalidate=60' }, response: new Response(null), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1012,7 +1012,7 @@ describe('setJSON', () => { 'x-amz-meta-user': encodedMetadata, }, response: new Response(null), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1058,7 +1058,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .delete({ response: new Response(null), @@ -1067,7 +1067,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${complexKey}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${complexKey}`, }) .delete({ response: new Response(null), @@ -1093,7 +1093,7 @@ describe('delete', () => { .delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(JSON.stringify({ url: signedURL })), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) .delete({ response: new Response(null, { status: 404 }), @@ -1117,7 +1117,7 @@ describe('delete', () => { const mockStore = new MockFetch().delete({ headers: { authorization: `Bearer ${apiToken}` }, response: new Response(null, { status: 401 }), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/production/${key}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1140,7 +1140,7 @@ describe('delete', () => { const mockStore = new MockFetch().delete({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { status: 204 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1161,7 +1161,7 @@ describe('delete', () => { const mockStore = new MockFetch().delete({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { status: 404 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1182,7 +1182,7 @@ describe('delete', () => { const mockStore = new MockFetch().delete({ headers: { authorization: `Bearer ${edgeToken}` }, response: new Response(null, { status: 401 }), - url: `${edgeURL}/${siteID}/production/${key}`, + url: `${edgeURL}/${siteID}/site:production/${key}`, }) globalThis.fetch = mockStore.fetch From e0ba75730727be8653706f433f6cdb287b883d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 13:28:37 +0000 Subject: [PATCH 5/6] chore: fix tests --- src/list.test.ts | 44 ++++++++++++++++++++++---------------------- src/main.test.ts | 28 ++++++++++------------------ 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/list.test.ts b/src/list.test.ts index 4144e2f..adac6b6 100644 --- a/src/list.test.ts +++ b/src/list.test.ts @@ -59,7 +59,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -83,7 +83,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_1`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -100,7 +100,7 @@ describe('list', () => { directories: [], }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_2`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?cursor=cursor_2`, }) globalThis.fetch = mockStore.fetch @@ -148,7 +148,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?directories=true`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -172,7 +172,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true&cursor=cursor_1`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?directories=true&cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -189,7 +189,7 @@ describe('list', () => { directories: ['dir3'], }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?directories=true&cursor=cursor_2`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?directories=true&cursor=cursor_2`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -206,7 +206,7 @@ describe('list', () => { directories: [], }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?prefix=dir2%2F&directories=true`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?prefix=dir2%2F&directories=true`, }) globalThis.fetch = mockStore.fetch @@ -258,7 +258,7 @@ describe('list', () => { ], }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?prefix=group%2F`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?prefix=group%2F`, }) globalThis.fetch = mockStore.fetch @@ -303,7 +303,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}`, }) .get({ headers: { authorization: `Bearer ${apiToken}` }, @@ -319,7 +319,7 @@ describe('list', () => { ], }), ), - url: `https://api.netlify.com/api/v1/blobs/${siteID}/${storeName}?cursor=cursor_2`, + url: `https://api.netlify.com/api/v1/blobs/${siteID}/site:${storeName}?cursor=cursor_2`, }) globalThis.fetch = mockStore.fetch @@ -373,7 +373,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `${edgeURL}/${siteID}/${storeName}`, + url: `${edgeURL}/${siteID}/site:${storeName}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -397,7 +397,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `${edgeURL}/${siteID}/${storeName}?cursor=cursor_1`, + url: `${edgeURL}/${siteID}/site:${storeName}?cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -414,7 +414,7 @@ describe('list', () => { directories: [], }), ), - url: `${edgeURL}/${siteID}/${storeName}?cursor=cursor_2`, + url: `${edgeURL}/${siteID}/site:${storeName}?cursor=cursor_2`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -431,7 +431,7 @@ describe('list', () => { directories: [], }), ), - url: `${edgeURL}/${siteID}/${storeName}?prefix=dir2%2F`, + url: `${edgeURL}/${siteID}/site:${storeName}?prefix=dir2%2F`, }) globalThis.fetch = mockStore.fetch @@ -487,7 +487,7 @@ describe('list', () => { next_cursor: 'cursor_1', }), ), - url: `${edgeURL}/${siteID}/${storeName}?directories=true`, + url: `${edgeURL}/${siteID}/site:${storeName}?directories=true`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -511,7 +511,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `${edgeURL}/${siteID}/${storeName}?directories=true&cursor=cursor_1`, + url: `${edgeURL}/${siteID}/site:${storeName}?directories=true&cursor=cursor_1`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -528,7 +528,7 @@ describe('list', () => { directories: ['dir3'], }), ), - url: `${edgeURL}/${siteID}/${storeName}?directories=true&cursor=cursor_2`, + url: `${edgeURL}/${siteID}/site:${storeName}?directories=true&cursor=cursor_2`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -545,7 +545,7 @@ describe('list', () => { directories: [], }), ), - url: `${edgeURL}/${siteID}/${storeName}?prefix=dir2%2F&directories=true`, + url: `${edgeURL}/${siteID}/site:${storeName}?prefix=dir2%2F&directories=true`, }) globalThis.fetch = mockStore.fetch @@ -598,7 +598,7 @@ describe('list', () => { ], }), ), - url: `${edgeURL}/${siteID}/${storeName}?prefix=group%2F`, + url: `${edgeURL}/${siteID}/site:${storeName}?prefix=group%2F`, }) globalThis.fetch = mockStore.fetch @@ -644,7 +644,7 @@ describe('list', () => { next_cursor: 'cursor_2', }), ), - url: `${edgeURL}/${siteID}/${storeName}`, + url: `${edgeURL}/${siteID}/site:${storeName}`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -667,7 +667,7 @@ describe('list', () => { next_cursor: 'cursor_3', }), ), - url: `${edgeURL}/${siteID}/${storeName}?cursor=cursor_2`, + url: `${edgeURL}/${siteID}/site:${storeName}?cursor=cursor_2`, }) .get({ headers: { authorization: `Bearer ${edgeToken}` }, @@ -683,7 +683,7 @@ describe('list', () => { ], }), ), - url: `${edgeURL}/${siteID}/${storeName}?cursor=cursor_3`, + url: `${edgeURL}/${siteID}/site:${storeName}?cursor=cursor_3`, }) globalThis.fetch = mockStore.fetch diff --git a/src/main.test.ts b/src/main.test.ts index 69ee9ce..0f66745 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -247,22 +247,22 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${tokens[0]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[0]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[1]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[1]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) globalThis.fetch = mockStore.fetch @@ -294,22 +294,22 @@ describe('get', () => { .get({ headers: { authorization: `Bearer ${tokens[0]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[0]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[1]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) .get({ headers: { authorization: `Bearer ${tokens[1]}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) globalThis.fetch = mockStore.fetch @@ -1383,7 +1383,7 @@ describe('Custom `fetch`', () => { const mockStore = new MockFetch().get({ headers: { authorization: `Bearer ${mockToken}` }, response: new Response(value), - url: `${edgeURL}/${siteID}/images/${key}`, + url: `${edgeURL}/${siteID}/site:images/${key}`, }) const context = { edgeURL, @@ -1459,14 +1459,6 @@ describe(`getStore`, () => { }), ).toThrowError(`Store name must be a sequence of Unicode characters whose UTF-8 encoding is at most 64 bytes long.`) - expect(() => - getStore({ - name: 'deploy:foo', - token: apiToken, - siteID, - }), - ).toThrowError('Store name must not start with the `deploy:` reserved keyword.') - const context = { siteID, token: apiToken, @@ -1474,7 +1466,7 @@ describe(`getStore`, () => { env.NETLIFY_BLOBS_CONTEXT = Buffer.from(JSON.stringify(context)).toString('base64') - expect(() => getStore('deploy:foo')).toThrowError('Store name must not start with the `deploy:` reserved keyword.') + expect(() => getStore('some/store')).toThrowError('Store name must not contain forward slashes (/).') }) test('Throws when there is no `fetch` implementation available', async () => { From b5c5bd4e6ef16fa62145a851f8503b11519764f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 4 Mar 2024 13:39:23 +0000 Subject: [PATCH 6/6] refactor: rewrite `reduce` --- src/store_list.ts | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/store_list.ts b/src/store_list.ts index e3e884e..2bbd70f 100644 --- a/src/store_list.ts +++ b/src/store_list.ts @@ -22,29 +22,13 @@ export function listStores( // We can't use `async/await` here because that would make the signature // incompatible with one of the overloads. // eslint-disable-next-line promise/prefer-await-to-then - return collectIterator(iterator).then((results) => - results.reduce( - (acc, item) => ({ - ...acc, - stores: [...acc.stores, ...item.stores], - }), - { stores: [] }, - ), - ) + return collectIterator(iterator).then((results) => ({ stores: results.flatMap((page) => page.stores) })) } -const formatListStoreResponse = (rawStores: string[]) => - rawStores.reduce((acc, rawStore) => { - if (rawStore.startsWith(DEPLOY_STORE_PREFIX)) { - return acc - } - - if (rawStore.startsWith(SITE_STORE_PREFIX)) { - return [...acc, rawStore.slice(SITE_STORE_PREFIX.length)] - } - - return [...acc, rawStore] - }, [] as string[]) +const formatListStoreResponse = (stores: string[]) => + stores + .filter((store) => !store.startsWith(DEPLOY_STORE_PREFIX)) + .map((store) => (store.startsWith(SITE_STORE_PREFIX) ? store.slice(SITE_STORE_PREFIX.length) : store)) const getListIterator = (client: Client, prefix: string): AsyncIterable => { const parameters: Record = {