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
JavaScript
/**
* 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;