UNPKG

appwrite-utils

Version:

`appwrite-utils` is a comprehensive TypeScript library designed to streamline the development process for Appwrite projects. It provides a suite of utilities and helper functions that facilitate data manipulation, schema management, and seamless integrati

677 lines (676 loc) 24 kB
import { DateTime } from "luxon"; import { validationRules } from "./validationRules.js"; export const converterFunctions = { /** * Converts any value to a string. Handles null and undefined explicitly. * @param {any} value The value to convert. * @return {string | null} The converted string or null if the value is null or undefined. */ anyToString(value) { if (value == null) return null; return typeof value === "string" ? value : JSON.stringify(value); }, /** * Converts any value to a number. Returns null for non-numeric values, null, or undefined. * @param {any} value The value to convert. * @return {number | null} The converted number or null. */ anyToNumber(value) { if (value == null) return null; const number = Number(value); return isNaN(number) ? null : number; }, /** * Converts any value to a boolean. Specifically handles string representations. * @param {any} value The value to convert. * @return {boolean | null} The converted boolean or null if conversion is not possible. */ anyToBoolean(value) { if (value == null) return null; if (typeof value === "string") { const trimmedValue = value.trim().toLowerCase(); if (["true", "yes", "1"].includes(trimmedValue)) return true; if (["false", "no", "0"].includes(trimmedValue)) return false; return null; // Return null for strings that don't explicitly match } return Boolean(value); }, /** * Converts any value to an array, attempting to split strings by a specified separator. * @param {any} value The value to convert. * @param {string | undefined} separator The separator to use when splitting strings. * @return {any[]} The resulting array after conversion. */ anyToAnyArray(value) { if (value === null || value === undefined || value === "") return []; if (Array.isArray(value)) { return value; } else if (typeof value === "string") { // Let's try a few return converterFunctions.trySplitByDifferentSeparators(value); } return [value]; }, /** * Converts any value to an array of strings. If the input is already an array, returns it as is. * Otherwise, if the input is a string, returns an array with the string as the only element. * Otherwise, returns an empty array. * @param {any} value The value to convert. * @return {string[]} The resulting array after conversion. */ anyToStringArray(value) { if (value === null || value === undefined || value === "") return []; if (Array.isArray(value)) { return value.map((item) => String(item)); } else if (typeof value === "string" && value.length > 0) { return [value]; } return []; }, /** * Converts any null, empty, or undefined value to an empty array * Meant to be used if the value needs to be an array but you can't guarantee it is one */ onlyUnsetToArray(value) { if (value === null || value === undefined || value === "") return []; return value; }, /** * Flattens an array or a nested array of arrays into a single array. * * @param {any} value - The value to be flattened. Can be an array or a nested array of arrays. * @return {any[]} - A flattened array of all the elements from the input array(s). * If the input is null/undefined/empty, an empty array is returned. * If the input is not an array or a nested array of arrays, the input is returned as is. */ flattenArray(value) { if (value === null || value === undefined || value === "") return []; if (Array.isArray(value)) { return value.flat(Infinity); } return value; }, /** * A function that converts any type of value to an array of numbers. * * @param {any} value - the value to be converted * @return {number[]} an array of numbers */ anyToNumberArray(value) { if (value === null || value === undefined || value === "") return value; if (Array.isArray(value)) { return value.map((item) => Number(item)); } else if (typeof value === "string") { return [Number(value)]; } return []; }, trim(value) { if (value === null || value === undefined || value === "") return ""; try { return value.trim(); } catch (error) { return value; } }, /** * Removes the start and end quotes from a string. * @param value The string to remove quotes from. * @return The string with quotes removed. **/ removeStartEndQuotes(value) { if (value === null || value === undefined || value === "") return ""; return value.replace(/^["']|["']$/g, ""); }, /** * Tries to split a string by different separators and returns the split that has the most uniform segment lengths. * This can be particularly useful for structured data like phone numbers. * @param value The string to split. * @return The split string array that has the most uniform segment lengths. */ trySplitByDifferentSeparators(value) { if (value === null || value === undefined || value === "") return []; const separators = [",", ";", "|", ":", "/", "\\"]; let bestSplit = []; let bestScore = -Infinity; for (const separator of separators) { const split = value.split(separator).map((s) => s.trim()); // Ensure we trim spaces if (split.length <= 1) continue; // Skip if no actual splitting occurred // Calculate uniformity in segment length const lengths = split.map((segment) => segment.length); const averageLength = lengths.reduce((a, b) => a + b, 0) / lengths.length; const lengthVariance = lengths.reduce((total, length) => total + Math.pow(length - averageLength, 2), 0) / lengths.length; // Adjust scoring to prioritize splits with lower variance and/or specific segment count if needed const score = split.length / (1 + lengthVariance); // Adjusted to prioritize lower variance // Update bestSplit if this split has a better score if (score > bestScore) { bestSplit = split; bestScore = score; } } // If no suitable split was found, return the original value as a single-element array if (bestSplit.length === 0) { return [value]; } return bestSplit; }, joinValues(values) { if (values === null || values === undefined) return values; try { return values.join(""); } catch (error) { return values; } }, joinBySpace(values) { if (values === null || values === undefined) return values; try { return values.join(" "); } catch (error) { return values; } }, makeArrayUnique(values) { if (values === null || values === undefined) return []; return [...new Set(values)]; }, joinByComma(values) { if (values === null || values === undefined) return values; try { return values.join(","); } catch (error) { return values; } }, joinByPipe(values) { if (values === null || values === undefined) return values; try { return values.join("|"); } catch (error) { return values; } }, joinBySemicolon(values) { if (values === null || values === undefined) return values; try { return values.join(";"); } catch (error) { return values; } }, joinByColon(values) { if (values === null || values === undefined) return values; try { return values.join(":"); } catch (error) { return values; } }, joinBySlash(values) { if (values === null || values === undefined) return values; try { return values.join("/"); } catch (error) { return values; } }, joinByHyphen(values) { if (values === null || values === undefined) return values; try { return values.join("-"); } catch (error) { return values; } }, splitByComma(value) { if (value === null || value === undefined || value === "") return value; try { return value.split(","); } catch (error) { return value; } }, splitByPipe(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("|"); } catch (error) { return value; } }, splitBySemicolon(value) { if (value === null || value === undefined || value === "") return value; try { return value.split(";"); } catch (error) { return value; } }, splitByColon(value) { if (value === null || value === undefined || value === "") return value; try { return value.split(":"); } catch (error) { return value; } }, splitBySlash(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("/"); } catch (error) { return value; } }, splitByBackslash(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("\\"); } catch (error) { return value; } }, splitBySpace(value) { if (value === null || value === undefined || value === "") return value; try { return value.split(" "); } catch (error) { return value; } }, splitByDot(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("."); } catch (error) { return value; } }, splitByUnderscore(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("_"); } catch (error) { return value; } }, splitByHyphen(value) { if (value === null || value === undefined || value === "") return value; try { return value.split("-"); } catch (error) { return value; } }, /** * Takes the first element of an array and returns it. * @param {any[]} value The array to take the first element from. * @return {any} The first element of the array. */ pickFirstElement(value) { if (value === null || value === undefined || value.length === 0) return value; try { return value[0]; } catch (error) { return value; } }, /** * Takes the last element of an array and returns it. * @param {any[]} value The array to take the last element from. * @return {any} The last element of the array. */ pickLastElement(value) { try { return value[value.length - 1]; } catch (error) { return value; } }, /** * Converts an object to a JSON string. * @param {any} object The object to convert. * @return {string} The JSON string representation of the object. */ stringifyObject(object) { return JSON.stringify(object); }, /** * Converts a JSON string to an object. * @param {string} jsonString The JSON string to convert. * @return {any} The object representation of the JSON string. */ parseObject(jsonString) { try { return JSON.parse(jsonString); } catch (error) { return jsonString; } }, convertPhoneStringToUSInternational(value) { // Normalize input: Remove all non-digit characters except the leading + const normalizedValue = value.startsWith("+") ? "+" + value.slice(1).replace(/\D/g, "") : value.replace(/\D/g, ""); // Check if the value is not a string or doesn't contain digits, return as is if (typeof normalizedValue !== "string" || !/\d/.test(normalizedValue)) return value; // Handle numbers with a leading + (indicating an international format) if (normalizedValue.startsWith("+")) { // If the number is already in a valid international format, return as is if (normalizedValue.length > 11 && normalizedValue.length <= 15) { return normalizedValue; } } else { // For numbers without a leading +, check the length and format if (normalizedValue.length === 10) { // US numbers without country code, prepend +1 return `+1${normalizedValue}`; } else if (normalizedValue.length === 11 && normalizedValue.startsWith("1")) { // US numbers with country code but missing +, prepend + return `+${normalizedValue}`; } } // For numbers that don't fit expected formats, return the original value return value; }, convertEmptyToNull(value) { if (Array.isArray(value)) { return value.map((item) => this.convertEmptyToNull(item)); } if (validationRules.isEmpty(value)) return null; return value; }, /** * A function that removes invalid elements from an array * * @param {any[]} array - the input array * @return {any[]} the filtered array without invalid elements */ removeInvalidElements(array) { if (!Array.isArray(array)) return array; return array.filter((element) => element !== null && element !== undefined && element !== "" && element !== "undefined" && element !== "null" && !validationRules.isEmpty(element)); }, validateOrNullEmail(email) { if (!email) return null; const emailRegex = /^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$/; return emailRegex.test(email) ? email : null; }, /** * Tries to parse a date from various formats using Luxon with enhanced error reporting. * @param {string | number} input The input date as a string or timestamp. * @return {string | null} The parsed date in ISO 8601 format or null if parsing failed. */ safeParseDate(input) { const formats = [ "M/d/yyyy HH:mm:ss", // U.S. style with time "d/M/yyyy HH:mm:ss", // Rest of the world style with time "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // ISO 8601 format "yyyy-MM-dd'T'HH:mm:ss", // ISO 8601 without milliseconds "yyyy-MM-dd HH:mm:ss", // SQL datetime format (common log format) "M/d/yyyy", // U.S. style without time "d/M/yyyy", // Rest of the world style without time "yyyy-MM-dd", // ISO date format "dd-MM-yyyy", "MM-dd-yyyy", "dd/MM/yyyy", "MM/dd/yyyy", "dd.MM.yyyy", "MM.dd.yyyy", "yyyy.MM.dd", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy-MM-dd HH:mm", // 12-hour clock formats with and without spaces "M/d/yyyy h:mm:ss a", // U.S. style with 12-hour clock "M/d/yyyy h:mm:ss tt", // Alternative AM/PM format "M/d/yyyy h:mma", // Compact format without seconds "M/d/yyyy h:mm a", // With space "d/M/yyyy h:mm:ss a", // Rest of world with 12-hour clock "d/M/yyyy h:mm:ss tt", "d/M/yyyy h:mma", "d/M/yyyy h:mm a", "h:mm:ss a", // Time only with 12-hour clock "h:mm:ss tt", "h:mma", "h:mm a", "HH:mm:ss", // Time only with 24-hour clock "HH:mm", // Time only without seconds, 24-hour clock "h:mm a M/d/yyyy", // 12-hour clock time followed by U.S. style date "h:mma M/d/yyyy", "h:mm tt M/d/yyyy", "h:mm a d/M/yyyy", // 12-hour clock time followed by Rest of world style date "h:mma d/M/yyyy", "h:mm tt d/M/yyyy", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", // ISO 8601 with timezone offset "yyyy-MM-dd'T'HH:mm:ssZ", // ISO 8601 without milliseconds but with timezone offset "E, dd MMM yyyy HH:mm:ss z", // RFC 2822 format "EEEE, MMMM d, yyyy", // Full textual date "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", // ISO 8601 with extended timezone offset "yyyy-MM-dd'T'HH:mm:ssXXX", // ISO 8601 without milliseconds but with extended timezone offset "dd-MMM-yyyy", // Textual month with day and year ]; // Normalize input string to handle various AM/PM formats let normalizedInput = String(input) .replace(/(\d)(AM|PM)/i, "$1 $2") // Add space between time and AM/PM if missing .replace(/(\d)([AaPp])([Mm])?/, "$1 $2M"); // Normalize 'a' or 'p' to 'AM' or 'PM' // Attempt to parse as a timestamp first if input is a number if (typeof input === "number") { const dateFromMillis = DateTime.fromMillis(input); if (dateFromMillis.isValid) { return dateFromMillis.toISO(); } } // Attempt to parse as an ISO string or SQL string let date = DateTime.fromISO(normalizedInput); if (!date.isValid) date = DateTime.fromSQL(normalizedInput); // Try each custom format if still not valid for (const format of formats) { if (!date.isValid) { date = DateTime.fromFormat(normalizedInput, format); } } // Return null if no valid date could be parsed if (!date.isValid) { return null; } return date.toISO(); }, }; /** * Deeply converts all properties of an object (or array) to strings. * @param data The input data to convert. * @returns The data with all its properties converted to strings. */ export const deepAnyToString = (data) => { if (Array.isArray(data)) { return data.map((item) => deepAnyToString(item)); } else if (validationRules.isObject(data)) { return Object.keys(data).reduce((acc, key) => { acc[key] = deepAnyToString(data[key]); return acc; }, {}); } else { return converterFunctions.anyToString(data); } }; /** * Performs a deep conversion of all values in a nested structure to the specified type. * Uses a conversion function like anyToString, anyToNumber, etc. * @param data The data to convert. * @param convertFn The conversion function to apply. * @returns The converted data. */ export const deepConvert = (data, convertFn) => { if (Array.isArray(data)) { return data.map((item) => deepConvert(item, convertFn)); } else if (validationRules.isObject(data)) { return Object.keys(data).reduce((acc, key) => { acc[key] = deepConvert(data[key], convertFn); return acc; }, {}); } else { return convertFn(data); } }; /** * Converts an entire object's properties to different types based on a provided schema. * @param obj The object to convert. * @param schema A mapping of object keys to conversion functions. * @returns The converted object. */ export const convertObjectBySchema = (obj, schema) => { return Object.keys(obj).reduce((acc, key) => { const convertFn = schema[key]; acc[key] = convertFn ? convertFn(obj[key]) : obj[key]; return acc; }, {}); }; /** * Converts the keys of an object based on a provided attributeMappings. * Each key in the object is checked against attributeMappings; if a matching entry is found, * the key is renamed to the targetKey specified in attributeMappings. * * @param obj The object to convert. * @param attributeMappings The attributeMappings defining how keys in the object should be converted. * @returns The converted object with keys renamed according to attributeMappings. */ export const convertObjectByAttributeMappings = (obj, attributeMappings) => { const result = {}; // Correctly handle [any] notation by mapping or aggregating over all elements or keys const resolveValue = (obj, path) => { const parts = path.split("."); let current = obj; for (let i = 0; i < parts.length; i++) { if (parts[i] === "[any]") { if (Array.isArray(current)) { // If current is an array, apply resolution to each item return current.map((item) => resolveValue(item, parts.slice(i + 1).join("."))); } else if (typeof current === "object" && current !== null) { // If current is an object, aggregate values from all keys return Object.values(current).map((value) => resolveValue(value, parts.slice(i + 1).join("."))); } } else { current = current[parts[i]]; if (current === undefined) return undefined; } } return current; }; for (const mapping of attributeMappings) { if (Array.isArray(mapping.oldKeys)) { // Collect and flatten values from multiple oldKeys const values = mapping.oldKeys .map((oldKey) => resolveValue(obj, oldKey)) .flat(Infinity); result[mapping.targetKey] = values.filter((value) => value !== undefined); } else if (mapping.oldKey) { // Resolve single oldKey const value = resolveValue(obj, mapping.oldKey); if (value !== undefined) { result[mapping.targetKey] = Array.isArray(value) ? value.flat(Infinity) : value; } } } return result; }; /** * Ensures data conversion without mutating the original input. * @param data The data to convert. * @param convertFn The conversion function to apply. * @returns The converted data. */ export const immutableConvert = (data, convertFn) => { const clonedData = structuredClone(data); return convertFn(clonedData); }; /** * Validates a string against a regular expression and returns the string if valid, or null. * @param value The string to validate. * @param pattern The regex pattern to validate against. * @returns The original string if valid, otherwise null. */ export const validateString = (value, pattern) => { return pattern.test(value) ? value : null; };