import * as plugins from '../plugins.js'; import type { Document, IStoredDocument, ISortSpecification, ISortDirection } from '../types/interfaces.js'; // Import mingo Query class import { Query } from 'mingo'; /** * Query engine using mingo for MongoDB-compatible query matching */ export class QueryEngine { /** * Filter documents by a MongoDB query filter */ static filter(documents: IStoredDocument[], filter: Document): IStoredDocument[] { if (!filter || Object.keys(filter).length === 0) { return documents; } const query = new Query(filter); return documents.filter(doc => query.test(doc)); } /** * Test if a single document matches a filter */ static matches(document: Document, filter: Document): boolean { if (!filter || Object.keys(filter).length === 0) { return true; } const query = new Query(filter); return query.test(document); } /** * Find a single document matching the filter */ static findOne(documents: IStoredDocument[], filter: Document): IStoredDocument | null { if (!filter || Object.keys(filter).length === 0) { return documents[0] || null; } const query = new Query(filter); for (const doc of documents) { if (query.test(doc)) { return doc; } } return null; } /** * Sort documents by a sort specification */ static sort(documents: IStoredDocument[], sort: ISortSpecification): IStoredDocument[] { if (!sort) { return documents; } // Normalize sort specification to array of [field, direction] pairs const sortFields: Array<[string, number]> = []; if (Array.isArray(sort)) { for (const [field, direction] of sort) { sortFields.push([field, this.normalizeDirection(direction)]); } } else { for (const [field, direction] of Object.entries(sort)) { sortFields.push([field, this.normalizeDirection(direction)]); } } return [...documents].sort((a, b) => { for (const [field, direction] of sortFields) { const aVal = this.getNestedValue(a, field); const bVal = this.getNestedValue(b, field); const comparison = this.compareValues(aVal, bVal); if (comparison !== 0) { return comparison * direction; } } return 0; }); } /** * Apply projection to documents */ static project(documents: IStoredDocument[], projection: Document): Document[] { if (!projection || Object.keys(projection).length === 0) { return documents; } // Determine if this is inclusion or exclusion projection const keys = Object.keys(projection); const hasInclusion = keys.some(k => k !== '_id' && projection[k] === 1); const hasExclusion = keys.some(k => k !== '_id' && projection[k] === 0); // Can't mix inclusion and exclusion (except for _id) if (hasInclusion && hasExclusion) { throw new Error('Cannot mix inclusion and exclusion in projection'); } return documents.map(doc => { if (hasInclusion) { // Inclusion projection const result: Document = {}; // Handle _id if (projection._id !== 0 && projection._id !== false) { result._id = doc._id; } for (const key of keys) { if (key === '_id') continue; if (projection[key] === 1 || projection[key] === true) { const value = this.getNestedValue(doc, key); if (value !== undefined) { this.setNestedValue(result, key, value); } } } return result; } else { // Exclusion projection - start with copy and remove fields const result = { ...doc }; for (const key of keys) { if (projection[key] === 0 || projection[key] === false) { this.deleteNestedValue(result, key); } } return result; } }); } /** * Get distinct values for a field */ static distinct(documents: IStoredDocument[], field: string, filter?: Document): any[] { let docs = documents; if (filter && Object.keys(filter).length > 0) { docs = this.filter(documents, filter); } const values = new Set(); for (const doc of docs) { const value = this.getNestedValue(doc, field); if (value !== undefined) { if (Array.isArray(value)) { // For arrays, add each element for (const v of value) { values.add(this.toComparable(v)); } } else { values.add(this.toComparable(value)); } } } return Array.from(values); } /** * Normalize sort direction to 1 or -1 */ private static normalizeDirection(direction: ISortDirection): number { if (typeof direction === 'number') { return direction > 0 ? 1 : -1; } if (direction === 'asc' || direction === 'ascending') { return 1; } return -1; } /** * Get a nested value from an object using dot notation */ static getNestedValue(obj: any, path: string): any { const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } if (Array.isArray(current)) { // Handle array access const index = parseInt(part, 10); if (!isNaN(index)) { current = current[index]; } else { // Get the field from all array elements return current.map(item => this.getNestedValue(item, part)).flat(); } } else { current = current[part]; } } return current; } /** * Set a nested value in an object using dot notation */ private static setNestedValue(obj: any, path: string, value: any): void { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { current[part] = {}; } current = current[part]; } current[parts[parts.length - 1]] = value; } /** * Delete a nested value from an object using dot notation */ private static deleteNestedValue(obj: any, path: string): void { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (!(part in current)) { return; } current = current[part]; } delete current[parts[parts.length - 1]]; } /** * Compare two values for sorting */ private static compareValues(a: any, b: any): number { // Handle undefined/null if (a === undefined && b === undefined) return 0; if (a === undefined) return -1; if (b === undefined) return 1; if (a === null && b === null) return 0; if (a === null) return -1; if (b === null) return 1; // Handle ObjectId if (a instanceof plugins.bson.ObjectId && b instanceof plugins.bson.ObjectId) { return a.toHexString().localeCompare(b.toHexString()); } // 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); } // Handle booleans if (typeof a === 'boolean' && typeof b === 'boolean') { return (a ? 1 : 0) - (b ? 1 : 0); } // Fall back to string comparison return String(a).localeCompare(String(b)); } /** * Convert a value to a comparable form (for distinct) */ private static toComparable(value: any): any { if (value instanceof plugins.bson.ObjectId) { return value.toHexString(); } if (value instanceof Date) { return value.toISOString(); } if (typeof value === 'object' && value !== null) { return JSON.stringify(value); } return value; } }