UNPKG

nylas

Version:

A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.

261 lines (260 loc) 9.97 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.createFileRequestBuilder = createFileRequestBuilder; exports.streamToBase64 = streamToBase64; exports.attachmentStreamToFile = attachmentStreamToFile; exports.encodeAttachmentContent = encodeAttachmentContent; 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 = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const mime = __importStar(require("mime-types")); const node_stream_1 = require("node: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); }); }); } /** * Converts a ReadableStream to a File-like object that can be used with FormData. * @param attachment The attachment containing the stream and metadata. * @param mimeType The MIME type for the file (optional). * @returns A File-like object that properly handles the stream. */ function attachmentStreamToFile(attachment, mimeType) { if (mimeType != null && typeof mimeType !== 'string') { throw new Error('Invalid mimetype, expected string.'); } const content = attachment.content; if (typeof content === 'string' || Buffer.isBuffer(content)) { throw new Error('Invalid attachment content, expected ReadableStream.'); } // Create a file-shaped object that FormData can handle properly const fileObject = { type: mimeType || attachment.contentType, name: attachment.filename, [Symbol.toStringTag]: 'File', stream() { return content; }, }; // Add size if available if (attachment.size !== undefined) { fileObject.size = attachment.size; } return fileObject; } /** * Encodes the content of each attachment to base64. * Handles ReadableStream, Buffer, and string content types. * @param attachments The attachments to encode. * @returns The attachments with their content encoded to base64. */ async function encodeAttachmentContent(attachments) { return await Promise.all(attachments.map(async (attachment) => { let base64EncodedContent; if (attachment.content instanceof node_stream_1.Readable) { // ReadableStream -> base64 base64EncodedContent = await streamToBase64(attachment.content); } else if (Buffer.isBuffer(attachment.content)) { // Buffer -> base64 base64EncodedContent = attachment.content.toString('base64'); } else { // string (assumed to already be base64) base64EncodedContent = attachment.content; } return { ...attachment, content: base64EncodedContent }; })); } /** * @deprecated Use encodeAttachmentContent instead. This alias is provided for backwards compatibility. * 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 encodeAttachmentContent(attachments); } /** * 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; }