UNPKG

inibase

Version:

A file-based & memory-efficient, serverless, ACID compliant, relational database management system

631 lines (630 loc) 24.7 kB
/** * Type guard function to check if the input is an array of objects. * * @param input - The value to be checked. * @returns boolean - True if the input is an array of objects, false otherwise. * * Note: Considers empty arrays and arrays where every element is an object. */ export const isArrayOfObjects = (input) => Array.isArray(input) && (input.length === 0 || input.every(isObject)); /** * Type guard function to check if the input is an array of arrays. * * @param input - The value to be checked. * @returns boolean - True if the input is an array of arrays, false otherwise. * * Note: Considers empty arrays and arrays where every element is also an array. */ export const isArrayOfArrays = (input) => Array.isArray(input) && input.length > 0 && input.every(Array.isArray); /** * Type guard function to check if the input is an array of nulls or an array of arrays of nulls. * * @param input - The value to be checked. * @returns boolean - True if the input is an array consisting entirely of nulls or arrays of nulls, false otherwise. * * Note: Recursively checks each element, allowing for nested arrays of nulls. */ export const isArrayOfNulls = (input) => Array.isArray(input) && input.every((_input) => Array.isArray(_input) ? isArrayOfNulls(_input) : _input === null || _input === 0 || _input === undefined); /** * Type guard function to check if the input is an object. * * @param obj - The value to be checked. * @returns boolean - True if the input is an object (excluding arrays), false otherwise. * * Note: Checks if the input is non-null and either has 'Object' as its constructor name or is of type 'object' without being an array. */ export const isObject = (object) => object != null && ((typeof object === "object" && !Array.isArray(object)) || object.constructor?.name === "Object"); /** * Type guard function to check if the input is a number. * * @param input - The value to be checked. * @returns boolean - True if the input is a number, false otherwise. * * Note: Validates that the input can be parsed as a float and that subtracting zero results in a number, ensuring it's a numeric value. */ export const isNumber = (input) => { // Case 1: It's already a number (and not NaN/Infinity). if (typeof input === "number") return !Number.isNaN(input) && Number.isFinite(input); // Case 2: It's a string that can parse to a finite number. if (typeof input === "string") { const trimmed = input.trim(); // Empty string or whitespace-only => not numeric if (!trimmed) return false; const parsed = Number(trimmed); // or parseFloat(trimmed) return !Number.isNaN(parsed) && Number.isFinite(parsed); } // Otherwise, not a numeric string or number return false; }; // As a literal (no double-escaping). const emailPattern = /^[A-Za-z0-9!#%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/; /** * Checks if the input is a valid email format. * * @param input - The value to be checked. * @returns boolean - True if the input matches the email format, false otherwise. * * Note: Uses a regular expression to validate the email format, ensuring it has parts separated by '@' and contains a domain with a period. */ export const isEmail = (input) => typeof input === "string" && emailPattern.test(String(input)); const urlPattern = new RegExp("^" + // Optional protocol "(https?:\\/\\/)?" + // domain name (with underscore allowed), localhost, or ipv4 "((([a-z\\d_]([a-z\\d_\\-]*[a-z\\d_])*)\\.)+[a-z]{2,}|" + "localhost|" + "((\\d{1,3}\\.){3}\\d{1,3}))" + // optional port "(\\:\\d+)?" + // path "(\\/[-a-z\\d%_.~+]*)*" + // query string "(\\?[;&a-z\\d%_.~+=-]*)?" + // fragment "(\\#[-a-z\\d_]*)?$", "i"); /** * Checks if the input is a valid URL format. * * @param input - The value to be checked. * @returns boolean - True if the input matches the URL format, false otherwise. * * Note: Validates URLs including protocols (http/https), domain names, IP addresses, ports, paths, query strings, and fragments. * Also recognizes 'tel:' and 'mailto:' as valid URL formats, as well as strings starting with '#' without spaces. */ export const isURL = (input) => { if (typeof input !== "string") return false; if ((input[0] === "#" && !input.includes(" ")) || input.startsWith("tel:") || input.startsWith("mailto:") || URL.canParse(input)) return true; return urlPattern.test(input); }; const htmlPattern = /<([A-Za-z][A-Za-z0-9-]*)(\s+[A-Za-z-]+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)*\s*>/; /** * Checks if the input contains HTML tags or entities. * * @param input - The value to be checked. * @returns boolean - True if the input contains HTML tags or entities, false otherwise. * * Note: Uses a regular expression to detect HTML tags (like <tag>) and entities (like &entity;). * Recognizes both opening and closing tags, as well as self-closing tags. */ export const isHTML = (input) => typeof input === "string" && htmlPattern.test(input); /** * Type guard function to check if the input is a string, excluding strings that match specific formats (number, boolean, email, URL, IP). * * @param input - The value to be checked. * @returns boolean - True if the input is a string that doesn't match the specific formats, false otherwise. * * Note: Validates the input against being a number, boolean, email, URL, or IP address to ensure it's a general string. */ export const isString = (input) => Object.prototype.toString.call(input) === "[object String]" && (!isNumber(input) || String(input).at(0) === "0"); const ipPattern = /^(?:(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)\.){3}(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)$/; /** * Checks if the input is a valid IP address format. * * @param input - The value to be checked. * @returns boolean - True if the input matches the IP address format, false otherwise. * * Note: Uses a regular expression to validate IP addresses, ensuring they consist of four octets, each ranging from 0 to 255. */ export const isIP = (input) => typeof input === "string" && ipPattern.test(input); /** * Type guard function to check if the input is a boolean or a string representation of a boolean. * * @param input - The value to be checked. * @returns boolean - True if the input is a boolean value or 'true'/'false' strings, false otherwise. * * Note: Recognizes both boolean literals (true, false) and their string representations ("true", "false"). */ export const isBoolean = (input) => typeof input === "boolean" || input === "true" || input === "false"; /** * Type guard function to check if the input is a password based on a specific length criterion. * * @param input - The value to be checked. * @returns boolean - True if the input is a string with a length of 161 characters, false otherwise. * * Note: Specifically checks for string length to determine if it matches the defined password length criterion. */ export const isPassword = (input) => typeof input === "string" && input.length === 97; /** * Checks if the input can be converted to a valid date. * * @param input - The input to be checked, can be of any type. * @returns A boolean indicating whether the input is a valid date. */ export const isDate = (input) => { // Check if the input is null, undefined, or an empty string if (input == null || input === "") return false; // Convert to number and check if it's a valid number const numTimestamp = Number(input); // Check if the converted number is NaN or not finite if (Number.isNaN(numTimestamp) || !Number.isFinite(numTimestamp)) return false; // Create a Date object from the timestamp const date = new Date(numTimestamp); // Check if the date is valid return date.getTime() === numTimestamp; }; /** * Checks if the input is a valid ID. * * @param input - The input to be checked, can be of any type. * @returns A boolean indicating whether the input is a string representing a valid ID of length 32. */ export const isValidID = (input) => { return typeof input === "string" && input.length === 32; }; /** * Checks if a given string is a valid JSON. * * @param {string} input - The string to be checked. * @returns {boolean} Returns true if the string is valid JSON, otherwise false. */ export const isStringified = (input) => typeof input === "string" && (input === "null" || input === "undefined" || input[0] === "{" || input[0] === "["); /** * Recursively merges properties from a source object into a target object. If a property exists in both, the source's value overwrites the target's. * * @param target - The target object to merge properties into. * @param source - The source object from which properties are merged. * @returns any - The modified target object with merged properties. * * Note: Performs a deep merge for nested objects. Non-object properties are directly overwritten. */ export const deepMerge = (target, source) => { for (const key in source) { if (Object.hasOwn(source, key)) { if (isObject(source[key]) && isObject(target[key])) target[key] = deepMerge(target[key], source[key]); else if (source[key] !== null) target[key] = source[key]; } } return target; }; /** * Identifies and returns properties that have changed between two objects. * * @param obj1 - The first object for comparison, with string keys and values. * @param obj2 - The second object for comparison, with string keys and values. * @returns A record of changed properties with original values from obj1 and new values from obj2, or null if no changes are found. */ export const findChangedProperties = (obj1, obj2) => { const result = {}; for (const key1 in obj1) { if (Object.hasOwn(obj2, key1)) { if (obj1[key1] !== obj2[key1]) result[obj1[key1]] = obj2[key1]; } else result[obj1[key1]] = null; } return Object.keys(result).length ? result : null; }; /** * Detects the field type of an input based on available types. * * @param input - The input whose field type is to be detected. * @param availableTypes - An array of potential field types to consider. * @returns The detected field type as a string, or undefined if no matching type is found. */ export const detectFieldType = (input, availableTypes) => { if (input !== null && input !== undefined) if (!Array.isArray(input)) { if ((input === "0" || input === "1" || input === "true" || input === "false") && availableTypes.includes("boolean")) return "boolean"; if (isNumber(input)) { if (availableTypes.includes("table")) return "table"; if (availableTypes.includes("date")) return "date"; if (availableTypes.includes("number")) return "number"; if (availableTypes.includes("string") && String(input).at(0) === "0") return "string"; if (availableTypes.includes("id")) return "id"; } else if (typeof input === "string") { if (availableTypes.includes("table") && isValidID(input)) return "table"; if (input.startsWith("[") && availableTypes.includes("array")) return "array"; if (availableTypes.includes("email") && isEmail(input)) return "email"; if (availableTypes.includes("url") && isURL(input)) return "url"; if (availableTypes.includes("password") && isPassword(input)) return "password"; if (availableTypes.includes("json") && isStringified(input)) return "json"; if (availableTypes.includes("json") && isDate(input)) return "json"; if (availableTypes.includes("string") && isString(input)) return "string"; if (availableTypes.includes("ip") && isIP(input)) return "ip"; } } else return "array"; return undefined; }; export const isFieldType = (field, compareAtType) => { if (Array.isArray(field.type)) { if (field.type.some((type) => Array.isArray(compareAtType) ? compareAtType.includes(type) : compareAtType === type)) return true; } else if ((Array.isArray(compareAtType) && compareAtType.includes(field.type)) || compareAtType === field.type) return true; if (field.children) { if (Array.isArray(field.children)) { if (!isArrayOfObjects(field.children)) { if (field.children.some((type) => Array.isArray(compareAtType) ? compareAtType.includes(type) : compareAtType === type)) return true; } } else if ((Array.isArray(compareAtType) && compareAtType.includes(field.children)) || compareAtType === field.children) return true; } return false; }; // Function to recursively flatten an array of objects and their nested children export const flattenSchema = (schema, keepParents = false) => { const result = []; const _flattenHelper = (item, parentKey) => { if (item.children && isArrayOfObjects(item.children)) { if (keepParents) result.push((({ children, ...rest }) => rest)(item)); for (const child of item.children) _flattenHelper(child, item.key); } else result.push({ ...item, key: parentKey ? `${parentKey}.${item.key}` : item.key, }); }; for (const item of schema) _flattenHelper(item, ""); return result; }; export const filterSchema = (schema, callback) => schema.filter((field) => { if (field.children && isArrayOfObjects(field.children)) field.children = filterSchema(field.children, callback); return callback(field); }); /** * Validates if the given value matches the specified field type(s). * * @param value - The value to be validated. * @param field - Field object config. * @returns A boolean indicating whether the value matches the specified field type(s). */ export const validateFieldType = (value, field) => { if (value === null) return true; let _fieldType = field.type; if (Array.isArray(_fieldType)) { const detectedFieldType = detectFieldType(value, _fieldType); if (!detectedFieldType) return false; _fieldType = detectedFieldType; } if (_fieldType === "array" && field.children) return (Array.isArray(value) && (isArrayOfObjects(field.children) || value.every((v) => { let _fieldChildrenType = field.children; if (Array.isArray(_fieldChildrenType)) { const detectedFieldType = detectFieldType(v, _fieldChildrenType); if (!detectedFieldType) return false; _fieldChildrenType = detectedFieldType; } return validateFieldType(v, { key: "BLABLA", type: _fieldChildrenType, }); }))); switch (_fieldType) { case "string": return isString(value); case "password": return !Array.isArray(value) && !isObject(value); // accept case "number": return isNumber(value); case "html": return isHTML(value); case "ip": return isIP(value); case "boolean": return isBoolean(value); case "date": return isDate(value); case "object": return isObject(value); case "array": return Array.isArray(value); case "email": return isEmail(value); case "url": return isURL(value); case "table": // feat: check if id exists if (Array.isArray(value)) return ((isArrayOfObjects(value) && value.every((element) => Object.hasOwn(element, "id") && (isValidID(element.id) || isNumber(element.id)))) || value.every(isNumber) || isValidID(value)); if (isObject(value)) return (Object.hasOwn(value, "id") && (isValidID(value.id) || isNumber(value.id))); return isNumber(value) || isValidID(value); case "id": return isNumber(value) || isValidID(value); case "json": return isStringified(value) || Array.isArray(value) || isObject(value); default: return false; } }; export const FormatObjectCriteriaValue = (value) => { switch (value[0]) { case ">": case "<": return value[1] === "=" ? [ value.slice(0, 2), value.slice(2), ] : [ value.slice(0, 1), value.slice(1), ]; case "[": return value[1] === "]" ? [ "[]", value.slice(2) .toString() .split(",") .map((v) => (isNumber(v) ? Number(v) : v)), ] : ["[]", value.slice(1)]; case "!": return ["=", "*"].includes(value[1]) ? [ value.slice(0, 2), value.slice(2), ] : value[1] === "[" ? [ "![]", value.slice(3) .toString() .split(",") .map((v) => (isNumber(v) ? Number(v) : v)), ] : [ `${value.slice(0, 1)}=`, value.slice(1), ]; case "=": return ["=", value.slice(1)]; case "*": return ["*", value.slice(1)]; default: return ["=", value]; } }; /** * Get field from schema * * @export * @param {string} keyPath Support dot notation path * @param {Schema} schema */ export const getField = (keyPath, schema) => { let RETURN = schema; const keyPathSplited = keyPath.split("."); for (const [index, key] of keyPathSplited.entries()) { if (!isArrayOfObjects(RETURN)) return null; const foundItem = RETURN.find((item) => item.key === key); if (!foundItem) return null; if (index === keyPathSplited.length - 1) RETURN = foundItem; if ((foundItem.type === "array" || foundItem.type === "object") && foundItem.children && isArrayOfObjects(foundItem.children)) RETURN = foundItem.children; } if (!RETURN) return null; return isArrayOfObjects(RETURN) ? RETURN[0] : RETURN; }; /** * Override a schema field, key, type or other properties * * @export * @param {string} keyPath Support dot notation path * @param {Schema} schema * @param {(Omit<Field, "key" | "type"> & { * key?: string; * type?: FieldType | FieldType[]; * })} field */ export const setField = (keyPath, schema, field) => { const keyPathSplited = keyPath.split("."); for (const [index, key] of keyPathSplited.entries()) { const foundItem = schema.find((item) => item.key === key); if (!foundItem) return null; if (index === keyPathSplited.length - 1) { Object.assign(foundItem, field); return foundItem; } if ((foundItem.type === "array" || foundItem.type === "object") && foundItem.children && isArrayOfObjects(foundItem.children)) schema = foundItem.children; } }; /** * Remove field from schema * * @export * @param {string} keyPath Support dot notation path * @param {Schema} schema */ export const unsetField = (keyPath, schema) => { const keyPathSplited = keyPath.split("."); let parent = null; let targetIndex; for (const [index, key] of keyPathSplited.entries()) { const foundItem = schema.find((item) => item.key === key); if (!foundItem) return null; if (index === keyPathSplited.length - 1) { if (parent) { if (Array.isArray(parent)) { if (targetIndex !== undefined) parent.splice(targetIndex, 1); } else delete parent[key]; } else { const indexToRemove = schema.indexOf(foundItem); if (indexToRemove !== -1) schema.splice(indexToRemove, 1); } return foundItem; } if ((foundItem.type === "array" || foundItem.type === "object") && foundItem.children && isArrayOfObjects(foundItem.children)) { parent = foundItem.children; targetIndex = schema.indexOf(foundItem); schema = foundItem.children; } else { parent = foundItem; targetIndex = undefined; } } }; export function toDotNotation(obj, skipKeys, currentPath = "") { const result = {}; for (const key in obj) { if (Object.hasOwn(obj, key)) { const value = obj[key]; const newKey = currentPath ? `${currentPath}.${key}` : key; if (skipKeys?.includes(key.toLowerCase())) { // Preserve "or" and "and" keys with their exact values result[newKey] = value; } else if (typeof value === "object" && value !== null && !Array.isArray(value)) { // Recursively process nested objects const nested = toDotNotation(value, skipKeys, newKey); Object.assign(result, nested); } else { // Add primitive values directly result[newKey] = value; } } } return result; } // Function to recursively flatten an array of objects and their nested children export const extractIdsFromSchema = (schema) => { const result = []; for (const field of schema) { if (field.id) result.push(field.id); if (field.children && isArrayOfObjects(field.children)) result.push(...extractIdsFromSchema(field.children)); } return result; }; /** * Finds the last ID number in a schema, potentially decoding it if encrypted. * * @param schema - The schema to search, defined as an array of schema objects. * @returns The last ID number in the schema, decoded if necessary. */ export const findLastIdNumber = (schema) => Math.max(...extractIdsFromSchema(schema)); /** * Adds or updates IDs in a schema, encoding them using a provided secret key or salt. * * @param schema - The schema to update, defined as an array of schema objects. * @param startWithID - An object containing the starting ID for generating new IDs. * @returns The updated schema with encoded IDs. */ export function addIdToSchema(schema, startWithID) { const clonedSchema = structuredClone(schema); function addIdToField(field) { if (!field.id) { startWithID.value++; field.id = startWithID.value; } if ((field.type === "array" || field.type === "object") && isArrayOfObjects(field.children)) field.children = addIdToSchemaHelper(field.children); return field; } const addIdToSchemaHelper = (schema) => schema.map(addIdToField); return addIdToSchemaHelper(clonedSchema); }