@message-queue-toolkit/sqs
Version:
SQS adapter for message-queue-toolkit
253 lines • 10.2 kB
JavaScript
import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueuesCommand, SetQueueAttributesCommand, } from '@aws-sdk/client-sqs';
import { globalLogger } from '@lokalise/node-core';
import { isShallowSubset, waitAndRetry } from '@message-queue-toolkit/core';
import { generateQueuePolicyFromPolicyConfig, generateQueuePublishForTopicPolicy, } from "./sqsAttributeUtils.js";
import { updateQueueAttributes, updateQueueTags } from "./sqsInitter.js";
const AWS_QUEUE_DOES_NOT_EXIST_ERROR_NAME = 'QueueDoesNotExist';
export async function getQueueUrl(sqsClient, queueName) {
try {
const result = await sqsClient.send(new GetQueueUrlCommand({
QueueName: queueName,
}));
if (result.QueueUrl) {
return {
result: result.QueueUrl,
};
}
return {
error: 'not_found',
};
}
catch (err) {
// @ts-expect-error
if (err.Code === 'AWS.SimpleQueueService.NonExistentQueue') {
return {
error: 'not_found',
};
}
throw err;
}
}
export async function getQueueAttributes(sqsClient, queueUrl, attributeNames = ['All']) {
const command = new GetQueueAttributesCommand({
QueueUrl: queueUrl,
AttributeNames: attributeNames,
});
try {
const response = await sqsClient.send(command);
return {
result: {
attributes: response.Attributes,
},
};
}
catch (err) {
// @ts-expect-error
if (err.Code === 'AWS.SimpleQueueService.NonExistentQueue') {
return {
error: 'not_found',
};
}
throw err;
}
}
export async function resolveQueueUrlFromLocatorConfig(sqsClient, locatorConfig) {
if (locatorConfig.queueUrl) {
return locatorConfig.queueUrl;
}
if (!locatorConfig.queueName) {
throw new Error('Invalid locatorConfig setup - queueUrl or queueName must be provided');
}
const queueUrlResult = await getQueueUrl(sqsClient, locatorConfig.queueName);
if (queueUrlResult.error === 'not_found') {
throw new Error(`Queue with queueName ${locatorConfig.queueName} does not exist.`);
}
return queueUrlResult.result;
}
async function updateExistingQueue(sqsClient, queueUrl, queueConfig, extraParams) {
const existingAttributes = await getQueueAttributes(sqsClient, queueUrl);
if (!existingAttributes.result?.attributes) {
throw new Error('Attributes are not set');
}
const queueArn = existingAttributes.result?.attributes.QueueArn;
if (!queueArn) {
throw new Error('Queue ARN was not set');
}
// we will try to update existing queue if exists
if (extraParams?.updateAttributesIfExists) {
const updatedAttributes = {
...queueConfig.Attributes,
};
if (extraParams.policyConfig) {
updatedAttributes.Policy = generateQueuePolicyFromPolicyConfig(queueArn, extraParams.policyConfig);
}
else if (extraParams?.topicArnsWithPublishPermissionsPrefix) {
updatedAttributes.Policy = generateQueuePublishForTopicPolicy(queueArn, extraParams.topicArnsWithPublishPermissionsPrefix);
}
// Only perform update if there are new or changed values in the queue config
if (!isShallowSubset(updatedAttributes, existingAttributes.result.attributes)) {
await updateQueueAttributes(sqsClient, queueUrl, updatedAttributes);
}
}
if (extraParams?.forceTagUpdate) {
await updateQueueTags(sqsClient, queueUrl, queueConfig.tags);
}
return {
queueUrl,
queueArn,
queueName: queueConfig.QueueName,
};
}
export async function assertQueue(sqsClient, queueConfig, extraParams, isFifoQueue) {
// biome-ignore lint/style/noNonNullAssertion: It's ok
const queueName = queueConfig.QueueName;
// Validate FIFO queue configuration before creating/updating
validateFifoQueueConfiguration(queueName, queueConfig.Attributes, isFifoQueue);
const queueUrlResult = await getQueueUrl(sqsClient, queueName);
const queueExists = !!queueUrlResult.result;
if (queueExists) {
return updateExistingQueue(sqsClient, queueUrlResult.result, queueConfig, extraParams);
}
// create new queue
const command = new CreateQueueCommand(queueConfig);
await sqsClient.send(command);
// biome-ignore lint/style/noNonNullAssertion: It's ok
const newQueueUrlResult = await getQueueUrl(sqsClient, queueConfig.QueueName);
const newQueueExists = !!newQueueUrlResult.result;
if (!newQueueExists) {
throw new Error(`Queue ${queueConfig.QueueName ?? ''} was not created`);
}
const getQueueAttributesCommand = new GetQueueAttributesCommand({
QueueUrl: newQueueUrlResult.result,
AttributeNames: ['QueueArn'],
});
const queueAttributesResponse = await sqsClient.send(getQueueAttributesCommand);
const queueArn = queueAttributesResponse.Attributes?.QueueArn;
if (!queueArn) {
throw new Error('Queue ARN was not set');
}
if (extraParams?.policyConfig) {
await sqsClient.send(new SetQueueAttributesCommand({
QueueUrl: newQueueUrlResult.result,
Attributes: {
Policy: generateQueuePolicyFromPolicyConfig(queueArn, extraParams.policyConfig),
},
}));
}
else if (extraParams?.topicArnsWithPublishPermissionsPrefix) {
await sqsClient.send(new SetQueueAttributesCommand({
QueueUrl: newQueueUrlResult.result,
Attributes: {
Policy: generateQueuePublishForTopicPolicy(queueArn, extraParams.topicArnsWithPublishPermissionsPrefix),
},
}));
}
return {
queueArn,
queueUrl: newQueueUrlResult.result,
queueName: queueConfig.QueueName,
};
}
export async function deleteQueue(client, queueName, waitForConfirmation = true) {
try {
const queueUrlCommand = new GetQueueUrlCommand({
QueueName: queueName,
});
const response = await client.send(queueUrlCommand);
const command = new DeleteQueueCommand({
QueueUrl: response.QueueUrl,
});
await client.send(command);
if (waitForConfirmation) {
await waitAndRetry(async () => {
const queueList = await client.send(new ListQueuesCommand({
QueueNamePrefix: queueName,
}));
return !queueList.QueueUrls || queueList.QueueUrls.length === 0;
});
}
}
catch (err) {
// This is fine
// @ts-expect-error
if (err.name === AWS_QUEUE_DOES_NOT_EXIST_ERROR_NAME) {
return;
}
// @ts-expect-error
globalLogger.error(`Failed to delete: ${err.message}`);
}
}
/**
* Calculates the size of an outgoing SQS message.
*
* SQS imposes a 256 KB limit on the total size of a message, which includes both the message body and any metadata (attributes).
* This function currently computes the size based solely on the message body, as no attributes are included at this time.
* For future updates, if message attributes are added, their sizes should also be considered.
*
* Reference: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes
*/
export function calculateOutgoingMessageSize(message) {
const messageBody = JSON.stringify(message);
return calculateSqsMessageBodySize(messageBody);
}
export function calculateSqsMessageBodySize(messageBody) {
return messageBody ? Buffer.byteLength(messageBody, 'utf8') : 0;
}
/**
* Checks if a queue name indicates a FIFO queue (ends with .fifo)
*/
export function isFifoQueueName(queueName) {
return queueName.endsWith('.fifo');
}
/**
* Validates that queue name matches the FIFO configuration flag
*/
export function validateFifoQueueName(queueName, isFifoQueue) {
const hasFifoName = isFifoQueueName(queueName);
if (isFifoQueue && !hasFifoName) {
throw new Error(`FIFO queue names must end with .fifo suffix. Queue name: ${queueName}`);
}
if (!isFifoQueue && hasFifoName) {
throw new Error(`Queue name ends with .fifo but fifoQueue option is not set to true. Queue name: ${queueName}`);
}
}
/**
* Validates FIFO queue configuration for creation
* - FIFO queues must have names ending with .fifo
* - If FifoQueue attribute is 'true', name must end with .fifo
*/
export function validateFifoQueueConfiguration(queueName, attributes, isFifoQueue) {
const hasFifoAttribute = attributes?.FifoQueue === 'true';
const hasFifoName = isFifoQueueName(queueName);
// If explicit FIFO flag is provided, validate against it
if (isFifoQueue !== undefined) {
validateFifoQueueName(queueName, isFifoQueue);
// Also validate that attributes match if provided
if (attributes && hasFifoAttribute !== isFifoQueue) {
throw new Error(`FifoQueue attribute (${hasFifoAttribute}) does not match fifoQueue option (${isFifoQueue})`);
}
}
// Validate consistency between name and attributes
if (hasFifoAttribute && !hasFifoName) {
throw new Error(`FIFO queue names must end with .fifo suffix. Queue name: ${queueName}`);
}
if (hasFifoName && attributes && !hasFifoAttribute) {
throw new Error(`Queue name ends with .fifo but FifoQueue attribute is not set to 'true'. Queue name: ${queueName}`);
}
}
/**
* Detects if a queue is FIFO based on its attributes
*/
export async function detectFifoQueue(sqsClient, queueUrl, queueName) {
// First check the queue name if provided
if (queueName && isFifoQueueName(queueName)) {
return true;
}
// Fallback to checking queue attributes
const attributesResult = await getQueueAttributes(sqsClient, queueUrl, ['FifoQueue']);
if (attributesResult.error) {
return false;
}
return attributesResult.result?.attributes?.FifoQueue === 'true';
}
//# sourceMappingURL=sqsUtils.js.map