diff --git a/.changeset/wet-grapes-vanish.md b/.changeset/wet-grapes-vanish.md new file mode 100644 index 000000000000..a66caf251234 --- /dev/null +++ b/.changeset/wet-grapes-vanish.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +feat: Add a new update command for Queues and a message-retention-period-secs option + +A new queues update command has been added to support selectively updating queues settings. message-retention-period-secs has been added as an option in both the queues create and update commands diff --git a/packages/wrangler/src/__tests__/queues.test.ts b/packages/wrangler/src/__tests__/queues.test.ts index 484b81c38154..2917e3a168ce 100644 --- a/packages/wrangler/src/__tests__/queues.test.ts +++ b/packages/wrangler/src/__tests__/queues.test.ts @@ -28,6 +28,7 @@ describe("wrangler", () => { COMMANDS wrangler queues list List Queues wrangler queues create Create a Queue + wrangler queues update Update a Queue wrangler queues delete Delete a Queue wrangler queues consumer Configure Queue consumers @@ -196,7 +197,9 @@ describe("wrangler", () => { describe("create", () => { function mockCreateRequest( queueName: string, - queueSettings: { delivery_delay?: number } | undefined = undefined + queueSettings: + | { delivery_delay?: number; message_retention_period?: number } + | undefined = undefined ) { const requests = { count: 0 }; @@ -210,6 +213,7 @@ describe("wrangler", () => { queue_name: string; settings: { delivery_delay: number; + message_retention_period: number; }; }; expect(body.queue_name).toEqual(queueName); @@ -250,7 +254,8 @@ describe("wrangler", () => { -v, --version Show version number [boolean] OPTIONS - --delivery-delay-secs How long a published message should be delayed for, in seconds. Must be a positive integer [number]" + --delivery-delay-secs How long a published message should be delayed for, in seconds. Must be between 0 and 42300 [number] + --message-retention-period-secs How long to retain a message in the queue, in seconds. Must be between 60 and 1209600 [number]" `); }); @@ -327,6 +332,275 @@ describe("wrangler", () => { expect(requests.count).toEqual(0); }); + + it("should show an error when invalid delivery delay is set", async () => { + const requests = mockCreateRequest("testQueue", { delivery_delay: 10 }); + await expect( + runWrangler("queues create testQueue --delivery-delay-secs=99999") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid --delivery-delay-secs value: 99999. Must be between 0 and 42300]` + ); + + expect(requests.count).toEqual(0); + }); + + it("should send queue settings with message retention period", async () => { + const requests = mockCreateRequest("testQueue", { + message_retention_period: 100, + }); + await runWrangler( + "queues create testQueue --message-retention-period-secs=100" + ); + expect(std.out).toMatchInlineSnapshot(` + "Creating queue testQueue. + Created queue testQueue." + `); + expect(requests.count).toEqual(1); + }); + + it("should show an error when two message retention periods are set", async () => { + const requests = mockCreateRequest("testQueue", { + message_retention_period: 60, + }); + + await expect( + runWrangler( + "queues create testQueue --message-retention-period-secs=70 --message-retention-period-secs=80" + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot specify --message-retention-period-secs multiple times]` + ); + + expect(requests.count).toEqual(0); + }); + + it("should show an error when invalid message retention period is set", async () => { + const requests = mockCreateRequest("testQueue", { + message_retention_period: 100, + }); + await expect( + runWrangler( + "queues create testQueue --message-retention-period-secs=0" + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid --message-retention-period-secs value: 0. Must be between 60 and 1209600]` + ); + + expect(requests.count).toEqual(0); + }); + }); + + describe("update", () => { + function mockUpdateRequest( + queueName: string, + queueSettings: + | { delivery_delay?: number; message_retention_period?: number } + | undefined = undefined + ) { + const requests = { count: 0 }; + + msw.use( + http.put( + "*/accounts/:accountId/queues/:queueId", + async ({ request }) => { + requests.count += 1; + + const body = (await request.json()) as { + queue_name: string; + settings: { + delivery_delay: number; + message_retention_period: number; + }; + }; + expect(body.queue_name).toEqual(queueName); + expect(body.settings).toEqual(queueSettings); + return HttpResponse.json({ + success: true, + errors: [], + messages: [], + result: { + queue_name: queueName, + created_on: "01-01-2001", + modified_on: "01-01-2001", + }, + }); + }, + { once: true } + ) + ); + return requests; + } + function mockGetQueueRequest( + queueName: string, + queueSettings: { + delivery_delay: number; + message_retention_period: number; + } + ) { + const requests = { count: 0 }; + msw.use( + http.get( + "*/accounts/:accountId/queues?*", + async () => { + requests.count += 1; + return HttpResponse.json({ + success: true, + errors: [], + messages: [], + result: [ + { + queue_name: queueName, + created_on: "", + producers: [], + consumers: [], + producers_total_count: 1, + consumers_total_count: 0, + modified_on: "", + queue_id: "queueId", + settings: { + delivery_delay: queueSettings.delivery_delay, + message_retention_period: + queueSettings.message_retention_period, + }, + }, + ], + }); + }, + { once: true } + ) + ); + return requests; + } + + it("should show the correct help text", async () => { + await runWrangler("queues update --help"); + expect(std.err).toMatchInlineSnapshot(`""`); + expect(std.out).toMatchInlineSnapshot(` + "wrangler queues update + + Update a Queue + + POSITIONALS + name The name of the queue [string] [required] + + GLOBAL FLAGS + -j, --experimental-json-config Experimental: support wrangler.json [boolean] + -c, --config Path to .toml configuration file [string] + -e, --env Environment to use for operations and .env files [string] + -h, --help Show help [boolean] + -v, --version Show version number [boolean] + + OPTIONS + --delivery-delay-secs How long a published message should be delayed for, in seconds. Must be between 0 and 42300 [number] + --message-retention-period-secs How long to retain a message in the queue, in seconds. Must be between 60 and 1209600 [number]" + `); + }); + + it("should update a queue with new message retention period and preserve old delivery delay", async () => { + const getrequests = mockGetQueueRequest("testQueue", { + delivery_delay: 10, + message_retention_period: 100, + }); + + //update queue with new message retention period + const requests = mockUpdateRequest("testQueue", { + delivery_delay: 10, + message_retention_period: 400, + }); + await runWrangler( + "queues update testQueue --message-retention-period-secs=400" + ); + + expect(requests.count).toEqual(1); + expect(getrequests.count).toEqual(1); + + expect(std.out).toMatchInlineSnapshot(` + "Updating queue testQueue. + Updated queue testQueue." + `); + }); + + it("should show an error when two message retention periods are set", async () => { + const requests = mockUpdateRequest("testQueue", { + message_retention_period: 60, + }); + + mockGetQueueRequest("testQueue", { + delivery_delay: 0, + message_retention_period: 100, + }); + + await expect( + runWrangler( + "queues update testQueue --message-retention-period-secs=70 --message-retention-period-secs=80" + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot specify --message-retention-period-secs multiple times]` + ); + + expect(requests.count).toEqual(0); + }); + + it("should show an error when two delivery delays are set", async () => { + const requests = mockUpdateRequest("testQueue", { + delivery_delay: 10, + }); + + mockGetQueueRequest("testQueue", { + delivery_delay: 0, + message_retention_period: 100, + }); + + await expect( + runWrangler( + "queues update testQueue --delivery-delay-secs=5 --delivery-delay-secs=10" + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Cannot specify --delivery-delay-secs multiple times]` + ); + + expect(requests.count).toEqual(0); + }); + + it("should show an error when invalid delivery delay is set", async () => { + const requests = mockUpdateRequest("testQueue", { + delivery_delay: 10, + }); + + mockGetQueueRequest("testQueue", { + delivery_delay: 0, + message_retention_period: 100, + }); + + await expect( + runWrangler("queues update testQueue --delivery-delay-secs=99999") + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid --delivery-delay-secs value: 99999. Must be between 0 and 42300]` + ); + + expect(requests.count).toEqual(0); + }); + + it("should show an error when invalid message retention period is set", async () => { + const requests = mockUpdateRequest("testQueue", { + message_retention_period: 100, + }); + + mockGetQueueRequest("testQueue", { + delivery_delay: 0, + message_retention_period: 100, + }); + + await expect( + runWrangler( + "queues update testQueue --message-retention-period-secs=0" + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid --message-retention-period-secs value: 0. Must be between 60 and 1209600]` + ); + + expect(requests.count).toEqual(0); + }); }); describe("delete", () => { diff --git a/packages/wrangler/src/queues/cli/commands/create.ts b/packages/wrangler/src/queues/cli/commands/create.ts index 5d39f8dd1315..4741ed3a774e 100644 --- a/packages/wrangler/src/queues/cli/commands/create.ts +++ b/packages/wrangler/src/queues/cli/commands/create.ts @@ -2,6 +2,12 @@ import { readConfig } from "../../../config"; import { CommandLineArgsError } from "../../../index"; import { logger } from "../../../logger"; import { createQueue } from "../../client"; +import { + MAX_DELIVERY_DELAY_SECS, + MAX_MESSAGE_RETENTION_PERIOD_SECS, + MIN_DELIVERY_DELAY_SECS, + MIN_MESSAGE_RETENTION_PERIOD_SECS, +} from "../../constants"; import { handleFetchError } from "../../utils"; import type { CommonYargsArgv, @@ -20,7 +26,12 @@ export function options(yargs: CommonYargsArgv) { "delivery-delay-secs": { type: "number", describe: - "How long a published message should be delayed for, in seconds. Must be a positive integer", + "How long a published message should be delayed for, in seconds. Must be between 0 and 42300", + }, + "message-retention-period-secs": { + type: "number", + describe: + "How long to retain a message in the queue, in seconds. Must be between 60 and 1209600", }, }); } @@ -38,10 +49,40 @@ function createBody( ); } + if (Array.isArray(args.messageRetentionPeriodSecs)) { + throw new CommandLineArgsError( + "Cannot specify --message-retention-period-secs multiple times" + ); + } + + body.settings = {}; + if (args.deliveryDelaySecs != undefined) { - body.settings = { - delivery_delay: args.deliveryDelaySecs, - }; + if ( + args.deliveryDelaySecs < MIN_DELIVERY_DELAY_SECS || + args.deliveryDelaySecs > MAX_DELIVERY_DELAY_SECS + ) { + throw new CommandLineArgsError( + `Invalid --delivery-delay-secs value: ${args.deliveryDelaySecs}. Must be between ${MIN_DELIVERY_DELAY_SECS} and ${MAX_DELIVERY_DELAY_SECS}` + ); + } + body.settings.delivery_delay = args.deliveryDelaySecs; + } + + if (args.messageRetentionPeriodSecs != undefined) { + if ( + args.messageRetentionPeriodSecs < MIN_MESSAGE_RETENTION_PERIOD_SECS || + args.messageRetentionPeriodSecs > MAX_MESSAGE_RETENTION_PERIOD_SECS + ) { + throw new CommandLineArgsError( + `Invalid --message-retention-period-secs value: ${args.messageRetentionPeriodSecs}. Must be between ${MIN_MESSAGE_RETENTION_PERIOD_SECS} and ${MAX_MESSAGE_RETENTION_PERIOD_SECS}` + ); + } + body.settings.message_retention_period = args.messageRetentionPeriodSecs; + } + + if (Object.keys(body.settings).length === 0) { + body.settings = undefined; } return body; diff --git a/packages/wrangler/src/queues/cli/commands/index.ts b/packages/wrangler/src/queues/cli/commands/index.ts index 2da413fe30ca..a9dbc3b2e764 100644 --- a/packages/wrangler/src/queues/cli/commands/index.ts +++ b/packages/wrangler/src/queues/cli/commands/index.ts @@ -3,6 +3,7 @@ import { consumers } from "./consumer/index"; import { handler as createHandler, options as createOptions } from "./create"; import { handler as deleteHandler, options as deleteOptions } from "./delete"; import { handler as listHandler, options as listOptions } from "./list"; +import { handler as updateHandler, options as updateOptions } from "./update"; import type { CommonYargsArgv } from "../../../yargs-types"; export function queues(yargs: CommonYargsArgv) { @@ -15,6 +16,13 @@ export function queues(yargs: CommonYargsArgv) { createHandler ); + yargs.command( + "update ", + "Update a Queue", + updateOptions, + updateHandler + ); + yargs.command( "delete ", "Delete a Queue", diff --git a/packages/wrangler/src/queues/cli/commands/update.ts b/packages/wrangler/src/queues/cli/commands/update.ts new file mode 100644 index 000000000000..893a0d96b5b3 --- /dev/null +++ b/packages/wrangler/src/queues/cli/commands/update.ts @@ -0,0 +1,110 @@ +import { readConfig } from "../../../config"; +import { CommandLineArgsError } from "../../../index"; +import { logger } from "../../../logger"; +import { getQueue, updateQueue } from "../../client"; +import { + MAX_DELIVERY_DELAY_SECS, + MAX_MESSAGE_RETENTION_PERIOD_SECS, + MIN_DELIVERY_DELAY_SECS, + MIN_MESSAGE_RETENTION_PERIOD_SECS, +} from "../../constants"; +import { handleFetchError } from "../../utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../../../yargs-types"; +import type { PostQueueBody, QueueSettings } from "../../client"; + +export function options(yargs: CommonYargsArgv) { + return yargs + .positional("name", { + type: "string", + demandOption: true, + description: "The name of the queue", + }) + .options({ + "delivery-delay-secs": { + type: "number", + describe: + "How long a published message should be delayed for, in seconds. Must be between 0 and 42300", + }, + "message-retention-period-secs": { + type: "number", + describe: + "How long to retain a message in the queue, in seconds. Must be between 60 and 1209600", + }, + }); +} + +function updateBody( + args: StrictYargsOptionsToInterface, + currentSettings?: QueueSettings +): PostQueueBody { + const body: PostQueueBody = { + queue_name: args.name, + }; + + if (Array.isArray(args.deliveryDelaySecs)) { + throw new CommandLineArgsError( + "Cannot specify --delivery-delay-secs multiple times" + ); + } + + if (Array.isArray(args.messageRetentionPeriodSecs)) { + throw new CommandLineArgsError( + "Cannot specify --message-retention-period-secs multiple times" + ); + } + + body.settings = {}; + + if (args.deliveryDelaySecs != undefined) { + if ( + args.deliveryDelaySecs < MIN_DELIVERY_DELAY_SECS || + args.deliveryDelaySecs > MAX_DELIVERY_DELAY_SECS + ) { + throw new CommandLineArgsError( + `Invalid --delivery-delay-secs value: ${args.deliveryDelaySecs}. Must be between ${MIN_DELIVERY_DELAY_SECS} and ${MAX_DELIVERY_DELAY_SECS}` + ); + } + body.settings.delivery_delay = args.deliveryDelaySecs; + } else if (currentSettings?.delivery_delay != undefined) { + body.settings.delivery_delay = currentSettings.delivery_delay; + } + + if (args.messageRetentionPeriodSecs != undefined) { + if ( + args.messageRetentionPeriodSecs < MIN_MESSAGE_RETENTION_PERIOD_SECS || + args.messageRetentionPeriodSecs > MAX_MESSAGE_RETENTION_PERIOD_SECS + ) { + throw new CommandLineArgsError( + `Invalid --message-retention-period-secs value: ${args.messageRetentionPeriodSecs}. Must be between ${MIN_MESSAGE_RETENTION_PERIOD_SECS} and ${MAX_MESSAGE_RETENTION_PERIOD_SECS}` + ); + } + body.settings.message_retention_period = args.messageRetentionPeriodSecs; + } else if (currentSettings?.message_retention_period != undefined) { + body.settings.message_retention_period = + currentSettings.message_retention_period; + } + + if (Object.keys(body.settings).length === 0) { + body.settings = undefined; + } + + return body; +} + +export async function handler( + args: StrictYargsOptionsToInterface +) { + const config = readConfig(args.config, args); + try { + const currentQueue = await getQueue(config, args.name); + const body = updateBody(args, currentQueue.settings); + logger.log(`Updating queue ${args.name}.`); + await updateQueue(config, body, currentQueue.queue_id); + logger.log(`Updated queue ${args.name}.`); + } catch (e) { + handleFetchError(e as { code?: number }); + } +} diff --git a/packages/wrangler/src/queues/client.ts b/packages/wrangler/src/queues/client.ts index d4a624719e94..37175a144df1 100644 --- a/packages/wrangler/src/queues/client.ts +++ b/packages/wrangler/src/queues/client.ts @@ -19,6 +19,7 @@ export interface WorkerService { export interface QueueSettings { delivery_delay?: number; + message_retention_period?: number; } export interface PostQueueResponse { @@ -109,6 +110,18 @@ export async function createQueue( }); } +export async function updateQueue( + config: Config, + body: PostQueueBody, + queue_id: string +): Promise { + const accountId = await requireAuth(config); + return fetchResult(queuesUrl(accountId, queue_id), { + method: "PUT", // TODO: PATCH + body: JSON.stringify(body), + }); +} + export async function deleteQueue( config: Config, queueName: string diff --git a/packages/wrangler/src/queues/constants.ts b/packages/wrangler/src/queues/constants.ts index dd47d23e2e42..cb61938e127d 100644 --- a/packages/wrangler/src/queues/constants.ts +++ b/packages/wrangler/src/queues/constants.ts @@ -1,2 +1,8 @@ export const INVALID_CONSUMER_SETTINGS_ERROR = 100127; export const INVALID_QUEUE_SETTINGS_ERROR = 100128; + +export const MIN_DELIVERY_DELAY_SECS = 0; +export const MAX_DELIVERY_DELAY_SECS = 42300; + +export const MIN_MESSAGE_RETENTION_PERIOD_SECS = 60; +export const MAX_MESSAGE_RETENTION_PERIOD_SECS = 1209600;