UNPKG

zapier-platform-cli

Version:

The CLI for managing integrations in Zapier Developer Platform.

241 lines (217 loc) 7.01 kB
const _ = require('lodash'); // Datetime related imports const chrono = require('chrono-node'); const { DateTime, IANAZone } = require('luxon'); const FALSE_STRINGS = new Set([ 'noo', 'no', 'n', 'false', 'nope', 'f', 'never', 'no thanks', 'no thank you', 'nul', '0', 'none', 'nil', 'nill', 'null', ]); const TRUE_STRINGS = new Set([['yes', 'yeah', 'y', 'true', 't', '1']]); const NUMBER_CHARSET = '0123456789.-,'; /** * Parses a string value to a boolean like how Zapier production does. * Recognizes common truthy/falsy strings like 'yes', 'no', 'true', 'false', etc. * @param {string} s - The string to parse * @returns {boolean} The parsed boolean value */ const parseBoolean = (s) => { s = s.toLowerCase(); if (TRUE_STRINGS.has(s)) { return true; } if (FALSE_STRINGS.has(s)) { return false; } return Boolean(s); }; /** * Parses a string to a decimal number like how Zapier production does. * Extracts numeric characters and handles various number formats. * @param {string} s - The string to parse * @returns {number} The parsed decimal number */ const parseDecimal = (s) => { const chars = []; for (const c of s) { if (NUMBER_CHARSET.includes(c)) { chars.push(c); } } const cleaned = chars.join('').replace(/[.,-]$/, ''); return parseFloat(cleaned); }; /** * Parses a string to an integer. * Falls back to parseDecimal if parseInt fails. * @param {string} s - The string to parse * @returns {number} The parsed integer */ const parseInteger = (s) => { const n = parseInt(s); if (!isNaN(n)) { return n; } return Math.floor(parseDecimal(s)); }; /** * Parses a Unix timestamp string to an ISO datetime string. * Handles both seconds and milliseconds timestamps. * @param {string} dtString - String potentially containing a timestamp * @param {string} tzName - IANA timezone name * @returns {string|null} ISO datetime string or null if not a timestamp */ const parseTimestamp = (dtString, tzName) => { const match = dtString.match(/-?\d{10,14}/); if (!match) { return null; } dtString = match[0]; let timestamp = parseInt(dtString); if (dtString.length <= 12) { timestamp *= 1000; } return DateTime.fromMillis(timestamp, { zone: tzName }).toFormat( "yyyy-MM-dd'T'HH:mm:ssZZ", ); }; /** * Checks if chrono parsing components contain time information. * @param {Object} parsingComps - Chrono parsing components * @returns {boolean} True if time info is present */ const hasTimeInfo = (parsingComps) => { const tags = [...parsingComps.tags()]; for (const tag of tags) { if (tag.includes('ISOFormat') || tag.includes('Time')) { return true; } } return false; }; /** * Adds default time info (09:00:00) to parsing components if not present. * @param {Object} parsingComps - Chrono parsing components * @returns {Object} The modified parsing components */ const maybeImplyTimeInfo = (parsingComps) => { if (!hasTimeInfo(parsingComps)) { parsingComps.imply('hour', 9); parsingComps.imply('minute', 0); parsingComps.imply('second', 0); parsingComps.imply('millisecond', 0); } return parsingComps; }; /** * Converts chrono parsing components to an ISO datetime string (without timezone). * @param {Object} parsingComps - Chrono parsing components * @returns {string} ISO datetime string like "2024-01-15T09:00:00" */ const parsingCompsToString = (parsingComps) => { const yyyy = parsingComps.get('year'); const mm = String(parsingComps.get('month')).padStart(2, '0'); const dd = String(parsingComps.get('day')).padStart(2, '0'); const hh = String(parsingComps.get('hour')).padStart(2, '0'); const ii = String(parsingComps.get('minute')).padStart(2, '0'); const ss = String(parsingComps.get('second')).padStart(2, '0'); return `${yyyy}-${mm}-${dd}T${hh}:${ii}:${ss}`; }; /** * Parses a datetime string using chrono-node with timezone support. * Handles timestamps, natural language dates, and ISO formats. * @param {string} dtString - The datetime string to parse * @param {string} tzName - IANA timezone name * @param {Date} now - Reference date for relative parsing * @returns {string} ISO datetime string with timezone offset */ const parseDatetime = (dtString, tzName, now) => { const timestampResult = parseTimestamp(dtString, tzName); if (timestampResult) { return timestampResult; } const offset = IANAZone.create(tzName).offset(now.getTime()); const results = chrono.parse(dtString, { instant: now, timezone: offset, }); let isoString; if (results.length) { const parsingComps = results[0].start; if (parsingComps.get('timezoneOffset') == null) { // No timezone info in the input string => interpret the datetime string // exactly as it is and append the timezone isoString = parsingCompsToString(maybeImplyTimeInfo(parsingComps)); } else { // Timezone info is present or implied in the input string => convert the // datetime to the specified timezone isoString = maybeImplyTimeInfo(parsingComps).date().toISOString(); } } else { // No datetime info in the input string => just return the current time isoString = now.toISOString(); } return DateTime.fromISO(isoString, { zone: tzName }).toFormat( "yyyy-MM-dd'T'HH:mm:ssZZ", ); }; /** * Resolves input data types based on field definitions. * Converts string values to appropriate types (integer, number, boolean, datetime). * Also applies default values for fields that have them. * @param {Object} inputData - The input data object (will be mutated) * @param {Array<Object>} inputFields - Array of field definitions with type info * @param {string} timezone - IANA timezone name for datetime parsing * @returns {Object} The mutated inputData object with resolved types */ const resolveInputDataTypes = (inputData, inputFields, timezone) => { const fieldsWithDefault = inputFields.filter((f) => f.default); for (const f of fieldsWithDefault) { if (!inputData[f.key]) { inputData[f.key] = f.default; } } const inputFieldsByKey = _.keyBy(inputFields, 'key'); for (const [k, v] of Object.entries(inputData)) { const inputField = inputFieldsByKey[k]; if (!inputField) { continue; } switch (inputField.type) { case 'integer': inputData[k] = parseInteger(v); break; case 'number': inputData[k] = parseDecimal(v); break; case 'boolean': inputData[k] = parseBoolean(v); break; case 'datetime': inputData[k] = parseDatetime(v, timezone, new Date()); break; case 'file': // TODO: How to handle a file field? break; // TODO: Handle 'list' and 'dict' types? default: // No need to do anything with 'string' type? break; } } // TODO: Handle line items (fields with "children") return inputData; }; module.exports = resolveInputDataTypes;