diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index a0a6fb52b4f..de0ddc7425b 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -39,6 +39,7 @@ netlify blobs:set my-store my-key --input ./some-file.txt netlify blobs:delete my-store my-key netlify blobs:list my-store netlify blobs:list my-store --json +netlify blobs:list my-store --region eu-central-1 ``` --- @@ -61,6 +62,7 @@ netlify blobs:delete - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in @@ -84,6 +86,7 @@ netlify blobs:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `output` (*string*) - Defines the filesystem path where the blob data should be persisted +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in @@ -110,6 +113,7 @@ netlify blobs:list - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used --- ## `blobs:set` @@ -133,6 +137,7 @@ netlify blobs:set - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. - `input` (*string*) - Defines the filesystem path where the blob data should be read from +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index 8b877670b7a..0a7fac02e7d 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -1,4 +1,4 @@ -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { chalk, logAndThrowError, log } from '../../utils/command-helpers.js' import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' @@ -13,6 +13,7 @@ export const blobsDelete = async (storeName: string, key: string, _options: Reco const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: _options.region as GetStoreOptions['region'], siteID: siteInfo.id ?? '', token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-get.ts b/src/commands/blobs/blobs-get.ts index 0b105e97d05..45d72dd2ded 100644 --- a/src/commands/blobs/blobs-get.ts +++ b/src/commands/blobs/blobs-get.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import { resolve } from 'path' -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { OptionValues } from 'commander' import { chalk, logAndThrowError } from '../../utils/command-helpers.js' @@ -9,6 +9,7 @@ import BaseCommand from '../base-command.js' interface Options extends OptionValues { output?: string + region?: GetStoreOptions['region'] } export const blobsGet = async (storeName: string, key: string, options: Options, command: BaseCommand) => { @@ -17,6 +18,7 @@ export const blobsGet = async (storeName: string, key: string, options: Options, const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo?.id ?? '', token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-list.ts b/src/commands/blobs/blobs-list.ts index 55844976261..f4fe717e15a 100644 --- a/src/commands/blobs/blobs-list.ts +++ b/src/commands/blobs/blobs-list.ts @@ -1,4 +1,4 @@ -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import AsciiTable from 'ascii-table' import { OptionValues } from 'commander' @@ -9,6 +9,7 @@ interface Options extends OptionValues { directories?: boolean json?: boolean prefix?: string + region?: GetStoreOptions['region'] } export const blobsList = async (storeName: string, options: Options, command: BaseCommand) => { @@ -16,6 +17,7 @@ export const blobsList = async (storeName: string, options: Options, command: Ba const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo.id, token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index 66b34d21d6c..be8119c4617 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import { resolve } from 'path' -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { OptionValues } from 'commander' import { chalk, logAndThrowError, isNodeError, log } from '../../utils/command-helpers.js' @@ -11,6 +11,7 @@ import BaseCommand from '../base-command.js' interface Options extends OptionValues { input?: string force?: string | boolean + region?: GetStoreOptions['region'] } export const blobsSet = async ( @@ -25,6 +26,7 @@ export const blobsSet = async ( const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo.id, token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs.ts b/src/commands/blobs/blobs.ts index 62b77fd203c..39b510cee97 100644 --- a/src/commands/blobs/blobs.ts +++ b/src/commands/blobs/blobs.ts @@ -20,6 +20,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .description(`Deletes an object with a given key, if it exists, from a Netlify Blobs store`) .argument('', 'Name of the store') .argument('', 'Object key') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:delete') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, _options: OptionValues, command: BaseCommand) => { @@ -35,6 +39,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Name of the store') .argument('', 'Object key') .option('-O, --output ', 'Defines the filesystem path where the blob data should be persisted') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:get') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, options: OptionValues, command: BaseCommand) => { @@ -55,6 +63,10 @@ export const createBlobsCommand = (program: BaseCommand) => { `A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned`, ) .option('--json', 'Output list contents as JSON') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:list') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, options: OptionValues, command: BaseCommand) => { @@ -71,6 +83,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Object key') .argument('[value...]', 'Object value') .option('-i, --input ', 'Defines the filesystem path where the blob data should be read from') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:set') .hook('preAction', requiresSiteInfo) @@ -98,6 +114,7 @@ For more information about Netlify Blobs, see ${terminalLink(docsUrl, docsUrl, { 'netlify blobs:delete my-store my-key', 'netlify blobs:list my-store', 'netlify blobs:list my-store --json', + 'netlify blobs:list my-store --region eu-central-1', ]) .action(blobs) } diff --git a/tests/integration/commands/blobs/blobs.test.ts b/tests/integration/commands/blobs/blobs.test.ts index 6fa7187a07f..c80ac7c26aa 100644 --- a/tests/integration/commands/blobs/blobs.test.ts +++ b/tests/integration/commands/blobs/blobs.test.ts @@ -27,6 +27,7 @@ describe('blobs:* commands', () => { const directory = temporaryDirectory() let server: BlobsServer + const requestedURLs: string[] = [] const routes: Route[] = [ { path: 'sites/site_id', response: siteInfo }, @@ -50,6 +51,7 @@ describe('blobs:* commands', () => { method: 'all', path: 'blobs/{*splat}', response: (req, res) => { + requestedURLs.push(req.url) blobsProxy.web(req, res, { target: `http://localhost:${address.port}` }) }, }) @@ -118,5 +120,36 @@ describe('blobs:* commands', () => { }), ).rejects.toThrowError('Error: Blob my-key does not exist in store my-store') }) + + test('should send the region to the API when one is given', async ({ fixture }) => { + const region = 'eu-central-1' + + requestedURLs.length = 0 + await fixture.callCli(['blobs:list', 'my-store', '--region', region, '--json'], { + offline: false, + parseJson: true, + }) + + expect(requestedURLs.some((url) => url.includes(`region=${region}`))).toBe(true) + }) + + test('should not send a region when none is given', async ({ fixture }) => { + requestedURLs.length = 0 + await fixture.callCli(['blobs:list', 'my-store', '--json'], { + offline: false, + parseJson: true, + }) + + expect(requestedURLs.length).toBeGreaterThan(0) + expect(requestedURLs.some((url) => url.includes('region'))).toBe(false) + }) + + test('should reject a region the Blobs client does not support', async ({ fixture }) => { + await expect( + fixture.callCli(['blobs:list', 'my-store', '--region', 'mars-north-1', '--json'], { + offline: false, + }), + ).rejects.toThrowError('not a supported Netlify Blobs region') + }) }) })