nylas
Version:
A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.
176 lines (175 loc) • 6.54 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFileRequestBuilder = createFileRequestBuilder;
exports.encodeAttachmentStreams = encodeAttachmentStreams;
exports.objKeysToCamelCase = objKeysToCamelCase;
exports.objKeysToSnakeCase = objKeysToSnakeCase;
exports.safePath = safePath;
exports.makePathParams = makePathParams;
exports.calculateTotalPayloadSize = calculateTotalPayloadSize;
const change_case_1 = require("change-case");
const fs = require("fs");
const path = require("path");
const mime = require("mime-types");
const stream_1 = require("stream");
function createFileRequestBuilder(filePath) {
const stats = fs.statSync(filePath);
const filename = path.basename(filePath);
const contentType = mime.lookup(filePath) || 'application/octet-stream';
const content = fs.createReadStream(filePath);
return {
filename,
contentType,
content,
size: stats.size,
};
}
/**
* Converts a ReadableStream to a base64 encoded string.
* @param stream The ReadableStream containing the binary data.
* @returns The stream base64 encoded to a string.
*/
function streamToBase64(stream) {
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', () => {
const base64 = Buffer.concat(chunks).toString('base64');
resolve(base64);
});
stream.on('error', (err) => {
reject(err);
});
});
}
/**
* Encodes the content of each attachment stream to base64.
* @param attachments The attachments to encode.
* @returns The attachments with their content encoded to base64.
*/
async function encodeAttachmentStreams(attachments) {
return await Promise.all(attachments.map(async (attachment) => {
const base64EncodedContent = attachment.content instanceof stream_1.Readable
? await streamToBase64(attachment.content)
: attachment.content;
return { ...attachment, content: base64EncodedContent }; // Replace the stream with its base64 string
}));
}
/**
* Applies the casing function and ensures numeric parts are preceded by underscores in snake_case.
* @param casingFunction The original casing function.
* @param input The string to convert.
* @returns The converted string.
*/
function applyCasing(casingFunction, input) {
const transformed = casingFunction(input);
if (casingFunction === change_case_1.snakeCase) {
return transformed.replace(/(\d+)/g, '_$1');
}
else {
return transformed.replace(/_+(\d+)/g, (match, p1) => p1);
}
}
/**
* A utility function that recursively converts all keys in an object to a given case.
* @param obj The object to convert
* @param casingFunction The function to use to convert the keys
* @param excludeKeys An array of keys to exclude from conversion
* @returns The converted object
* @ignore Not for public use.
*/
function convertCase(obj, casingFunction, excludeKeys) {
const newObj = {};
for (const key in obj) {
if (excludeKeys?.includes(key)) {
newObj[key] = obj[key];
}
else if (Array.isArray(obj[key])) {
newObj[applyCasing(casingFunction, key)] = obj[key].map((item) => {
if (typeof item === 'object') {
return convertCase(item, casingFunction);
}
else {
return item;
}
});
}
else if (typeof obj[key] === 'object' && obj[key] !== null) {
newObj[applyCasing(casingFunction, key)] = convertCase(obj[key], casingFunction);
}
else {
newObj[applyCasing(casingFunction, key)] = obj[key];
}
}
return newObj;
}
/**
* A utility function that recursively converts all keys in an object to camelCase.
* @param obj The object to convert
* @param exclude An array of keys to exclude from conversion
* @returns The converted object
* @ignore Not for public use.
*/
function objKeysToCamelCase(obj, exclude) {
return convertCase(obj, change_case_1.camelCase, exclude);
}
/**
* A utility function that recursively converts all keys in an object to snake_case.
* @param obj The object to convert
* @param exclude An array of keys to exclude from conversion
* @returns The converted object
*/
function objKeysToSnakeCase(obj, exclude) {
return convertCase(obj, change_case_1.snakeCase, exclude);
}
/**
* Safely encodes a path template with replacements.
* @param pathTemplate The path template to encode.
* @param replacements The replacements to encode.
* @returns The encoded path.
*/
function safePath(pathTemplate, replacements) {
return pathTemplate.replace(/\{(\w+)\}/g, (_, key) => {
const val = replacements[key];
if (val == null)
throw new Error(`Missing replacement for ${key}`);
// Decode first (handles already encoded values), then encode
// This prevents double encoding while ensuring everything is properly encoded
try {
const decoded = decodeURIComponent(val);
return encodeURIComponent(decoded);
}
catch (error) {
// If decoding fails, the value wasn't properly encoded, so just encode it
return encodeURIComponent(val);
}
});
}
// Helper to create PathParams with type safety and runtime interpolation
function makePathParams(path, params) {
return safePath(path, params);
}
/**
* Calculates the total payload size for a message request, including body and attachments.
* This is used to determine if multipart/form-data should be used instead of JSON.
* @param requestBody The message request body
* @returns The total estimated payload size in bytes
*/
function calculateTotalPayloadSize(requestBody) {
let totalSize = 0;
// Calculate size of the message body (JSON payload without attachments)
const messagePayloadWithoutAttachments = {
...requestBody,
attachments: undefined,
};
const messagePayloadString = JSON.stringify(objKeysToSnakeCase(messagePayloadWithoutAttachments));
totalSize += Buffer.byteLength(messagePayloadString, 'utf8');
// Add attachment sizes
const attachmentSize = requestBody.attachments?.reduce((total, attachment) => {
return total + (attachment.size || 0);
}, 0) || 0;
totalSize += attachmentSize;
return totalSize;
}