nylas
Version:
A NodeJS wrapper for the Nylas REST API for email, contacts, and calendar.
261 lines (260 loc) • 9.97 kB
JavaScript
;
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;
}