UNPKG

strata-storage

Version:

Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms

313 lines (312 loc) 10.2 kB
/** * Query Engine Feature * Zero-dependency implementation of MongoDB-like query operators */ /** * Query engine for advanced data filtering */ export class QueryEngine { /** * Check if a value matches a query condition */ matches(value, condition) { // Handle null/undefined values if (value === null || value === undefined) { return this.matchesNull(value, condition); } // If condition is not an object, use direct equality if (typeof condition !== 'object' || condition === null) { return this.equals(value, condition); } // Handle special operators if (this.hasOperators(condition)) { return this.matchesOperators(value, condition); } // Handle nested object matching if (typeof value === 'object' && value !== null) { return this.matchesObject(value, condition); } return false; } /** * Check if object has query operators */ hasOperators(obj) { return Object.keys(obj).some((key) => key.startsWith('$')); } /** * Match value against operators */ matchesOperators(value, operators) { for (const [op, operand] of Object.entries(operators)) { if (!this.matchesOperator(value, op, operand)) { return false; } } return true; } /** * Match single operator */ matchesOperator(value, operator, operand) { switch (operator) { case '$eq': return this.equals(value, operand); case '$ne': return !this.equals(value, operand); case '$gt': return this.compare(value, operand) > 0; case '$gte': return this.compare(value, operand) >= 0; case '$lt': return this.compare(value, operand) < 0; case '$lte': return this.compare(value, operand) <= 0; case '$in': if (!Array.isArray(operand)) return false; // If value is an array, check if any operand value is in the array if (Array.isArray(value)) { return operand.some((v) => value.some((item) => this.equals(item, v))); } // Otherwise check if value equals any operand value return operand.some((v) => this.equals(value, v)); case '$nin': return Array.isArray(operand) && !operand.some((v) => this.equals(value, v)); case '$regex': return this.matchesRegex(value, operand); case '$exists': return (value !== undefined) === Boolean(operand); case '$type': return this.getType(value) === operand; case '$and': return Array.isArray(operand) && operand.every((cond) => this.matches(value, cond)); case '$or': return Array.isArray(operand) && operand.some((cond) => this.matches(value, cond)); case '$not': return !this.matches(value, operand); default: return false; } } /** * Match object against condition */ matchesObject(obj, condition) { for (const [key, value] of Object.entries(condition)) { if (key.startsWith('$')) { // Top-level operator if (!this.matchesOperator(obj, key, value)) { return false; } } else { // Property match const objValue = this.getNestedValue(obj, key); if (!this.matches(objValue, value)) { return false; } } } return true; } /** * Get nested value from object using dot notation */ getNestedValue(obj, path) { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } if (typeof current === 'object' && part in current) { current = current[part]; } else { return undefined; } } return current; } /** * Check equality with proper type handling */ equals(a, b) { // Handle null/undefined if (a === b) return true; if (a === null || b === null) return false; if (a === undefined || b === undefined) return false; // Handle dates if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } // Handle arrays if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((val, i) => this.equals(val, b[i])); } // Handle objects if (typeof a === 'object' && typeof b === 'object') { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; return aKeys.every((key) => this.equals(a[key], b[key])); } // Default comparison return a === b; } /** * Compare values */ compare(a, b) { // Handle null/undefined if (a === b) return 0; if (a === null || a === undefined) return -1; if (b === null || b === undefined) return 1; // Handle dates if (a instanceof Date && b instanceof Date) { return a.getTime() - b.getTime(); } // Handle numbers if (typeof a === 'number' && typeof b === 'number') { return a - b; } // Handle strings if (typeof a === 'string' && typeof b === 'string') { return a.localeCompare(b); } // Type mismatch - convert to string for comparison return String(a).localeCompare(String(b)); } /** * Match regex pattern */ matchesRegex(value, pattern) { if (typeof value !== 'string') return false; const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern); return regex.test(value); } /** * Get JavaScript type of value */ getType(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (Array.isArray(value)) return 'array'; if (value instanceof Date) return 'date'; if (value instanceof RegExp) return 'regexp'; return typeof value; } /** * Handle null/undefined matching */ matchesNull(value, condition) { // Direct null/undefined comparison if (condition === null || condition === undefined) { return value === condition; } // Handle operators if (typeof condition === 'object' && this.hasOperators(condition)) { return this.matchesOperators(value, condition); } return false; } /** * Sort array of items by multiple fields */ sort(items, sortBy) { const sorted = [...items]; const sortKeys = Object.entries(sortBy); sorted.sort((a, b) => { for (const [key, direction] of sortKeys) { const aVal = this.getNestedValue(a, key); const bVal = this.getNestedValue(b, key); const comparison = this.compare(aVal, bVal); if (comparison !== 0) { return comparison * direction; } } return 0; }); return sorted; } /** * Project/transform objects based on projection spec */ project(item, projection) { const result = {}; const isInclusion = Object.values(projection).some((v) => v === 1 || v === true); if (isInclusion) { // Inclusion mode - only include specified fields for (const [key, include] of Object.entries(projection)) { if (include === 1 || include === true) { const value = this.getNestedValue(item, key); if (value !== undefined) { this.setNestedValue(result, key, value); } } } } else { // Exclusion mode - include all except specified fields result.value = { ...item }; for (const [key, exclude] of Object.entries(projection)) { if (exclude === 0 || exclude === false) { this.deleteNestedValue(result, key); } } } return result; } /** * Set nested value in object using dot notation */ setNestedValue(obj, path, value) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current) || typeof current[part] !== 'object') { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } /** * Delete nested value from object using dot notation */ deleteNestedValue(obj, path) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current) || typeof current[part] !== 'object') { return; } current = current[part]; } delete current[parts[parts.length - 1]]; } } /** * Create a query engine instance */ export function createQueryEngine() { return new QueryEngine(); }