UNPKG

@ably/cli

Version:

Ably CLI for Pub/Sub, Chat and Spaces

304 lines (303 loc) 14.6 kB
import { Args, Flags } from "@oclif/core"; import { AblyBaseCommand } from "../../base-command.js"; export default class ChannelsBatchPublish extends AblyBaseCommand { static args = { message: Args.string({ description: "The message to publish (JSON format or plain text, not needed if using --spec)", required: false, }), }; static description = "Publish messages to multiple Ably channels with a single request"; static examples = [ '$ ably channels batch-publish --channels channel1,channel2 \'{"data":"Message to multiple channels"}\'', '$ ably channels batch-publish --channels channel1,channel2 --name event \'{"text":"Hello World"}\'', '$ ably channels batch-publish --channels-json \'["channel1", "channel2"]\' \'{"data":"Using JSON array for channels"}\'', '$ ably channels batch-publish --spec \'{"channels": ["channel1", "channel2"], "messages": {"data": "Using complete batch spec"}}\'', '$ ably channels batch-publish --spec \'[{"channels": "channel1", "messages": {"data": "First spec"}}, {"channels": "channel2", "messages": {"data": "Second spec"}}]\'', '$ ably channels batch-publish --channels channel1,channel2 \'{"data":"Message"}\' --json', '$ ably channels batch-publish --channels channel1,channel2 \'{"data":"Message"}\' --pretty-json', ]; static flags = { ...AblyBaseCommand.globalFlags, channels: Flags.string({ description: "Comma-separated list of channel names to publish to", exclusive: ["channels-json", "spec"], }), "channels-json": Flags.string({ description: "JSON array of channel names to publish to", exclusive: ["channels", "spec"], }), encoding: Flags.string({ char: "e", description: "The encoding for the message", exclusive: ["spec"], }), name: Flags.string({ char: "n", description: "The event name (if not specified in the message JSON)", exclusive: ["spec"], }), spec: Flags.string({ description: "Complete batch spec JSON (either a single BatchSpec object or an array of BatchSpec objects)", exclusive: ["channels", "channels-json", "name", "encoding"], }), }; async run() { const { args, flags } = await this.parse(ChannelsBatchPublish); try { // Create REST client const rest = await this.createAblyRestClient(flags); if (!rest) { return; } // Prepare the batch request content let batchContent; if (flags.spec) { // Use the provided spec directly try { batchContent = JSON.parse(flags.spec); } catch (error) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: `Failed to parse spec JSON: ${error instanceof Error ? error.message : String(error)}`, success: false, }, flags)); return; } this.error(`Failed to parse spec JSON: ${error instanceof Error ? error.message : String(error)}`); } } else { // Build the batch content from flags and args let channels = []; if (flags.channels) { channels = flags.channels.split(",").map((c) => c.trim()); } else if (flags["channels-json"]) { try { const parsedChannels = JSON.parse(flags["channels-json"]); if (!Array.isArray(parsedChannels)) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: "channels-json must be a valid JSON array of channel names", success: false, }, flags)); return; } this.error("channels-json must be a valid JSON array of channel names"); } channels = parsedChannels; } catch (error) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: `Failed to parse channels-json: ${error instanceof Error ? error.message : String(error)}`, success: false, }, flags)); return; } this.error(`Failed to parse channels-json: ${error instanceof Error ? error.message : String(error)}`); } } else { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: "You must specify either --channels, --channels-json, or --spec", success: false, }, flags)); return; } this.error("You must specify either --channels, --channels-json, or --spec"); } if (!args.message) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: "Message is required when not using --spec", success: false, }, flags)); return; } this.error("Message is required when not using --spec"); } // Parse the message let messageData; try { messageData = JSON.parse(args.message); } catch { // If parsing fails, use the raw message as data messageData = { data: args.message }; } // Prepare the message const message = {}; // If name is provided in flags, use it. Otherwise, check if it's in the message data if (flags.name) { message.name = flags.name; } else if (messageData.name) { message.name = messageData.name; // Remove the name from the data to avoid duplication delete messageData.name; } // If data is explicitly provided in the message, use it if ("data" in messageData) { message.data = messageData.data; } else { // Otherwise use the entire messageData as the data message.data = messageData; } // Add encoding if provided if (flags.encoding) { message.encoding = flags.encoding; } // Create the batch spec batchContent = { channels, messages: message, }; } if (!this.shouldSuppressOutput(flags)) { this.log("Sending batch publish request..."); } // Make the batch publish request using the REST client's request method const response = await rest.request("post", "/messages", 2, null, batchContent); // Convert batchContent to a known type for easier handling const batchContentObj = batchContent; if (response.statusCode >= 200 && response.statusCode < 300) { // Success response const responseItems = response.items || []; if (!this.shouldSuppressOutput(flags)) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ channels: Array.isArray(batchContentObj.channels) ? batchContentObj.channels : [batchContentObj.channels], message: batchContentObj.messages, response: responseItems, success: true, }, flags)); } else { this.log("Batch publish successful!"); this.log(`Response: ${this.formatJsonOutput({ responses: responseItems }, flags)}`); } } } else if (response.statusCode === 400) { // Partial success or error const responseData = response.items; // Handle the error response which could contain a batchResponse field if (responseData && typeof responseData === "object" && !Array.isArray(responseData)) { const errorInfo = responseData; if (errorInfo.error && errorInfo.error.code === 40_020 && errorInfo.batchResponse) { // This is a partial success with batchResponse field if (!this.shouldSuppressOutput(flags)) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ channels: Array.isArray(batchContentObj.channels) ? batchContentObj.channels : [batchContentObj.channels], error: errorInfo.error, message: batchContentObj.messages, partial: true, response: errorInfo.batchResponse, success: false, }, flags)); } else { this.log("Batch publish partially successful (some messages failed)."); // Format batch response in a friendly way const batchResponses = errorInfo.batchResponse; batchResponses.forEach((item) => { if (item.error) { this.log(`Failed to publish to channel '${item.channel}': ${item.error.message} (${item.error.code})`); } else { this.log(`Published to channel '${item.channel}' with messageId: ${item.messageId}`); } }); } } } else { // Complete failure const errorMessage = errorInfo.error ? errorInfo.error.message : "Unknown error"; const errorCode = errorInfo.error ? errorInfo.error.code : response.statusCode; if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: { code: errorCode, message: errorMessage, }, success: false, }, flags)); } else { this.error(`Batch publish failed: ${errorMessage} (${errorCode})`); } } } else if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: { code: response.statusCode, message: `Batch publish failed with status code ${response.statusCode}`, }, success: false, }, flags)); } else { this.error(`Batch publish failed with status code ${response.statusCode}`); } } else { // Other error response const responseData = response.items; let errorMessage = "Unknown error"; let errorCode = response.statusCode; if (responseData && typeof responseData === "object" && !Array.isArray(responseData)) { const errorInfo = responseData; if (errorInfo.error) { errorMessage = errorInfo.error.message || errorMessage; errorCode = errorInfo.error.code || errorCode; } } if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: { code: errorCode, message: errorMessage, }, success: false, }, flags)); } else { this.error(`Batch publish failed: ${errorMessage} (${errorCode})`); } } } catch (error) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: error instanceof Error ? error.message : String(error), success: false, }, flags)); } else { this.error(`Failed to execute batch publish: ${error instanceof Error ? error.message : String(error)}`); } } } }