inibase
Version:
A file-based & memory-efficient, serverless, ACID compliant, relational database management system
631 lines (630 loc) • 24.7 kB
JavaScript
/**
* 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);
}