UNPKG

react-native-sst-storage-db

Version:

A powerful file-based MongoDB-like database for React Native. Single JSON file storage with collections, advanced querying, indexing, and atomic operations. No AsyncStorage dependency.

401 lines (341 loc) 11.8 kB
/** * QueryEngine - MongoDB-like query operations * Handles find, update, delete operations with MongoDB query operators */ class QueryEngine { constructor() { // MongoDB-like query operators this.operators = { // Comparison operators '$eq': (value, condition) => value === condition, '$ne': (value, condition) => value !== condition, '$gt': (value, condition) => value > condition, '$gte': (value, condition) => value >= condition, '$lt': (value, condition) => value < condition, '$lte': (value, condition) => value <= condition, '$in': (value, condition) => { if (!Array.isArray(condition)) return false; if (Array.isArray(value)) { return value.some(v => condition.includes(v)); } return condition.includes(value); }, '$nin': (value, condition) => { if (!Array.isArray(condition)) return false; if (Array.isArray(value)) { return !value.some(v => condition.includes(v)); } return !condition.includes(value); }, // String operators '$regex': (value, condition) => { if (typeof value !== 'string') return false; const regex = new RegExp(condition, 'i'); return regex.test(value); }, // Array operators '$size': (value, condition) => Array.isArray(value) && value.length === condition, '$all': (value, condition) => { if (!Array.isArray(value) || !Array.isArray(condition)) return false; return condition.every(item => value.includes(item)); }, '$elemMatch': (value, condition) => { if (!Array.isArray(value)) return false; return value.some(item => this.matchesQuery(item, condition)); }, // Existence operators '$exists': (value, condition) => { return condition ? value !== undefined && value !== null : value === undefined || value === null; }, // Type operator '$type': (value, condition) => { const type = Array.isArray(value) ? 'array' : typeof value; return type === condition; } }; } /** * Find documents matching query */ find(documents, query = {}, options = {}) { try { // Filter documents let results = documents.filter(doc => this.matchesQuery(doc, query)); // Apply sorting if (options.sort) { results = this.sortDocuments(results, options.sort); } // Apply skip if (options.skip && options.skip > 0) { results = results.slice(options.skip); } // Apply limit if (options.limit && options.limit > 0) { results = results.slice(0, options.limit); } return results; } catch (error) { console.error('QueryEngine.find error:', error); throw error; } } /** * Find one document matching query */ findOne(documents, query = {}, options = {}) { const results = this.find(documents, query, { ...options, limit: 1 }); return results.length > 0 ? results[0] : null; } /** * Count documents matching query */ count(documents, query = {}) { return documents.filter(doc => this.matchesQuery(doc, query)).length; } /** * Check if a document matches a query */ matchesQuery(document, query) { if (!query || Object.keys(query).length === 0) { return true; // Empty query matches all } // Handle logical operators if (query.$and) { return query.$and.every(subQuery => this.matchesQuery(document, subQuery)); } if (query.$or) { return query.$or.some(subQuery => this.matchesQuery(document, subQuery)); } if (query.$nor) { return !query.$nor.some(subQuery => this.matchesQuery(document, subQuery)); } if (query.$not) { return !this.matchesQuery(document, query.$not); } // Check each field condition for (const [field, condition] of Object.entries(query)) { if (field.startsWith('$')) { continue; // Skip logical operators already handled } const value = this.getNestedValue(document, field); if (!this.matchesCondition(value, condition)) { return false; } } return true; } /** * Check if a value matches a condition */ matchesCondition(value, condition) { // Direct equality check if (typeof condition !== 'object' || condition === null || Array.isArray(condition)) { return value === condition; } // Handle MongoDB operators for (const [operator, operatorValue] of Object.entries(condition)) { if (operator.startsWith('$')) { const operatorFunc = this.operators[operator]; if (!operatorFunc) { throw new Error(`Unsupported query operator: ${operator}`); } if (!operatorFunc(value, operatorValue)) { return false; } } else { // Nested object comparison if (typeof value === 'object' && value !== null) { const nestedValue = value[operator]; if (!this.matchesCondition(nestedValue, operatorValue)) { return false; } } else { return false; } } } return true; } /** * Get nested value from object using dot notation */ getNestedValue(obj, path) { if (!path || typeof path !== 'string') { return obj; } return path.split('.').reduce((current, key) => { return current && current[key] !== undefined ? current[key] : undefined; }, obj); } /** * Set nested value in object using dot notation */ setNestedValue(obj, path, value) { if (!path || typeof path !== 'string') { return; } const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { if (current[key] === undefined) { current[key] = {}; } return current[key]; }, obj); target[lastKey] = value; } /** * Sort documents */ sortDocuments(documents, sort) { return documents.sort((a, b) => { for (const [field, direction] of Object.entries(sort)) { const aValue = this.getNestedValue(a, field); const bValue = this.getNestedValue(b, field); let comparison = 0; // Handle different types if (aValue === bValue) { comparison = 0; } else if (aValue === null || aValue === undefined) { comparison = -1; } else if (bValue === null || bValue === undefined) { comparison = 1; } else if (typeof aValue === 'string' && typeof bValue === 'string') { comparison = aValue.localeCompare(bValue); } else { comparison = aValue < bValue ? -1 : 1; } if (comparison !== 0) { return direction === -1 ? -comparison : comparison; } } return 0; }); } /** * Apply update operations to a document */ applyUpdate(document, update) { const result = { ...document }; for (const [operator, operations] of Object.entries(update)) { switch (operator) { case '$set': for (const [field, value] of Object.entries(operations)) { this.setNestedValue(result, field, value); } break; case '$unset': for (const field of Object.keys(operations)) { this.unsetNestedValue(result, field); } break; case '$inc': for (const [field, value] of Object.entries(operations)) { const currentValue = this.getNestedValue(result, field) || 0; this.setNestedValue(result, field, currentValue + value); } break; case '$mul': for (const [field, value] of Object.entries(operations)) { const currentValue = this.getNestedValue(result, field) || 0; this.setNestedValue(result, field, currentValue * value); } break; case '$push': for (const [field, value] of Object.entries(operations)) { let currentArray = this.getNestedValue(result, field); if (!Array.isArray(currentArray)) { currentArray = []; } currentArray.push(value); this.setNestedValue(result, field, currentArray); } break; case '$pull': for (const [field, condition] of Object.entries(operations)) { let currentArray = this.getNestedValue(result, field); if (Array.isArray(currentArray)) { const filteredArray = currentArray.filter(item => !this.matchesCondition(item, condition) ); this.setNestedValue(result, field, filteredArray); } } break; case '$addToSet': for (const [field, value] of Object.entries(operations)) { let currentArray = this.getNestedValue(result, field); if (!Array.isArray(currentArray)) { currentArray = []; } if (!currentArray.includes(value)) { currentArray.push(value); } this.setNestedValue(result, field, currentArray); } break; default: // Direct field assignment (non-operator updates) if (!operator.startsWith('$')) { result[operator] = operations; } } } return result; } /** * Remove nested property */ unsetNestedValue(obj, path) { if (!path || typeof path !== 'string') { return; } const keys = path.split('.'); const lastKey = keys.pop(); const target = keys.reduce((current, key) => { return current && current[key] ? current[key] : null; }, obj); if (target && target.hasOwnProperty(lastKey)) { delete target[lastKey]; } } /** * Generate aggregation pipeline results (basic implementation) */ aggregate(documents, pipeline) { let results = [...documents]; for (const stage of pipeline) { const stageType = Object.keys(stage)[0]; const stageOptions = stage[stageType]; switch (stageType) { case '$match': results = this.find(results, stageOptions); break; case '$sort': results = this.sortDocuments(results, stageOptions); break; case '$limit': results = results.slice(0, stageOptions); break; case '$skip': results = results.slice(stageOptions); break; case '$project': results = results.map(doc => { const projected = {}; for (const [field, include] of Object.entries(stageOptions)) { if (include) { projected[field] = this.getNestedValue(doc, field); } } return projected; }); break; default: console.warn(`Unsupported aggregation stage: ${stageType}`); } } return results; } } module.exports = QueryEngine;