UNPKG

adonis-odm

Version:

A comprehensive MongoDB ODM for AdonisJS with Lucid-style API, type-safe relationships, embedded documents, and transaction support

695 lines (694 loc) 22.8 kB
/** * Advanced query builder for embedded documents with full type safety * * Supports: * - Complex filtering and searching * - Pagination for large datasets * - Multiple sorting criteria * - Performance optimizations * - Aggregation operations * - All methods from ModelQueryBuilder for consistency */ export class EmbeddedQueryBuilder { items; filters = []; orFilters = []; sortOptions = []; limitValue; skipValue; selectFields; searchTerm; searchFields; constructor(items) { this.items = items; } where(field, operatorOrValue, value) { let operator; let actualValue; if (arguments.length === 2) { // where(field, value) - equals comparison operator = '='; actualValue = operatorOrValue; } else { // where(field, operator, value) - operator comparison operator = operatorOrValue; actualValue = value; } this.filters.push((item) => { const fieldValue = item[field]; switch (operator) { case '=': case '==': return fieldValue === actualValue; case '!=': case '<>': return fieldValue !== actualValue; case '>': return actualValue !== undefined && actualValue !== null && fieldValue > actualValue; case '>=': return actualValue !== undefined && actualValue !== null && fieldValue >= actualValue; case '<': return actualValue !== undefined && actualValue !== null && fieldValue < actualValue; case '<=': return actualValue !== undefined && actualValue !== null && fieldValue <= actualValue; case 'like': return (typeof fieldValue === 'string' && typeof actualValue === 'string' && fieldValue.includes(actualValue)); case 'in': return Array.isArray(actualValue) && actualValue.includes(fieldValue); case 'not in': return Array.isArray(actualValue) && !actualValue.includes(fieldValue); default: return fieldValue === actualValue; } }); return this; } andWhere(field, operatorOrValue, value) { if (arguments.length === 2) { return this.where(field, operatorOrValue); } else { return this.where(field, operatorOrValue, value); } } orWhere(field, operatorOrValue, value) { let operator; let actualValue; if (arguments.length === 2) { // orWhere(field, value) - equals comparison operator = '='; actualValue = operatorOrValue; } else { // orWhere(field, operator, value) operator = operatorOrValue; actualValue = value; } this.orFilters.push((item) => { const fieldValue = item[field]; switch (operator) { case '=': case '==': return fieldValue === actualValue; case '!=': case '<>': return fieldValue !== actualValue; case '>': return actualValue !== undefined && actualValue !== null && fieldValue > actualValue; case '>=': return actualValue !== undefined && actualValue !== null && fieldValue >= actualValue; case '<': return actualValue !== undefined && actualValue !== null && fieldValue < actualValue; case '<=': return actualValue !== undefined && actualValue !== null && fieldValue <= actualValue; case 'like': return (typeof fieldValue === 'string' && typeof actualValue === 'string' && fieldValue.includes(actualValue)); case 'in': return Array.isArray(actualValue) && actualValue.includes(fieldValue); case 'not in': return Array.isArray(actualValue) && !actualValue.includes(fieldValue); default: return fieldValue === actualValue; } }); return this; } whereNot(field, operatorOrValue, value) { let operator; let actualValue; if (arguments.length === 2) { // whereNot(field, value) - not equals comparison operator = '!='; actualValue = operatorOrValue; } else { // whereNot(field, operator, value) operator = operatorOrValue; actualValue = value; } this.filters.push((item) => { const fieldValue = item[field]; switch (operator) { case '=': case '==': return fieldValue !== actualValue; case '!=': case '<>': return fieldValue === actualValue; case '>': return actualValue === undefined || actualValue === null || fieldValue <= actualValue; case '>=': return actualValue === undefined || actualValue === null || fieldValue < actualValue; case '<': return actualValue === undefined || actualValue === null || fieldValue >= actualValue; case '<=': return actualValue === undefined || actualValue === null || fieldValue > actualValue; case 'like': return !(typeof fieldValue === 'string' && typeof actualValue === 'string' && fieldValue.includes(actualValue)); case 'in': return !(Array.isArray(actualValue) && actualValue.includes(fieldValue)); case 'not in': return Array.isArray(actualValue) && actualValue.includes(fieldValue); default: return fieldValue !== actualValue; } }); return this; } andWhereNot(field, operatorOrValue, value) { return this.whereNot(field, operatorOrValue, value); } orWhereNot(field, operatorOrValue, value) { let operator = operatorOrValue; if (arguments.length === 2) { // orWhereNot(field, value) - not equals comparison value = operatorOrValue; operator = '!='; } this.orFilters.push((item) => { const fieldValue = item[field]; switch (operator) { case '=': case '==': return fieldValue !== value; case '!=': case '<>': return fieldValue === value; case '>': return value === undefined || value === null || fieldValue <= value; case '>=': return value === undefined || value === null || fieldValue < value; case '<': return value === undefined || value === null || fieldValue >= value; case '<=': return value === undefined || value === null || fieldValue > value; case 'like': return !(typeof fieldValue === 'string' && typeof value === 'string' && fieldValue.includes(value)); case 'in': if (!Array.isArray(value) || fieldValue === undefined) return true; return !value.includes(fieldValue); case 'not in': if (!Array.isArray(value) || fieldValue === undefined) return false; return value.includes(fieldValue); default: return fieldValue !== value; } }); return this; } /** * Add a where like condition (case-sensitive) */ whereLike(field, value) { this.filters.push((item) => { const fieldValue = item[field]; if (typeof fieldValue !== 'string') return false; const pattern = value.replace(/%/g, '.*'); const regex = new RegExp(pattern); return regex.test(fieldValue); }); return this; } /** * Add a where ilike condition (case-insensitive) */ whereILike(field, value) { this.filters.push((item) => { const fieldValue = item[field]; if (typeof fieldValue !== 'string') return false; const pattern = value.replace(/%/g, '.*'); const regex = new RegExp(pattern, 'i'); return regex.test(fieldValue); }); return this; } /** * Add a where in condition */ whereIn(field, values) { this.filters.push((item) => { const fieldValue = item[field]; return values.includes(fieldValue); }); return this; } /** * Add an OR where in condition */ orWhereIn(field, values) { this.orFilters.push((item) => { const fieldValue = item[field]; return values.includes(fieldValue); }); return this; } /** * Add a where not in condition */ whereNotIn(field, values) { this.filters.push((item) => { const fieldValue = item[field]; return !values.includes(fieldValue); }); return this; } /** * Add an OR where not in condition */ orWhereNotIn(field, values) { this.orFilters.push((item) => { const fieldValue = item[field]; return !values.includes(fieldValue); }); return this; } /** * Add a where between condition */ whereBetween(field, range) { this.filters.push((item) => { const fieldValue = item[field]; return fieldValue >= range[0] && fieldValue <= range[1]; }); return this; } /** * Add an OR where between condition */ orWhereBetween(field, range) { this.orFilters.push((item) => { const fieldValue = item[field]; return fieldValue >= range[0] && fieldValue <= range[1]; }); return this; } /** * Add a where not between condition */ whereNotBetween(field, range) { this.filters.push((item) => { const fieldValue = item[field]; return fieldValue < range[0] || fieldValue > range[1]; }); return this; } /** * Add an OR where not between condition */ orWhereNotBetween(field, range) { this.orFilters.push((item) => { const fieldValue = item[field]; return fieldValue < range[0] || fieldValue > range[1]; }); return this; } /** * Filter embedded documents where field is not null */ whereNotNull(field) { this.filters.push((item) => { const fieldValue = item[field]; return fieldValue !== null && fieldValue !== undefined; }); return this; } /** * Add an OR where not null condition */ orWhereNotNull(field) { this.orFilters.push((item) => { const fieldValue = item[field]; return fieldValue !== null && fieldValue !== undefined; }); return this; } /** * Filter embedded documents where field is null */ whereNull(field) { this.filters.push((item) => { const fieldValue = item[field]; return fieldValue === null || fieldValue === undefined; }); return this; } /** * Add an OR where null condition */ orWhereNull(field) { this.orFilters.push((item) => { const fieldValue = item[field]; return fieldValue === null || fieldValue === undefined; }); return this; } /** * Add a where exists condition */ whereExists(field) { this.filters.push((item) => { return field in item; }); return this; } /** * Add an OR where exists condition */ orWhereExists(field) { this.orFilters.push((item) => { return field in item; }); return this; } /** * Add a where not exists condition */ whereNotExists(field) { this.filters.push((item) => { return !(field in item); }); return this; } /** * Add an OR where not exists condition */ orWhereNotExists(field) { this.orFilters.push((item) => { return !(field in item); }); return this; } /** * Sort embedded documents with type safety */ orderBy(field, direction = 'asc') { this.sortOptions.push({ field, direction }); return this; } /** * Limit the number of results */ limit(count) { this.limitValue = count; return this; } /** * Skip a number of results */ skip(count) { this.skipValue = count; return this; } /** * Alias for skip - for pagination compatibility */ offset(count) { return this.skip(count); } /** * Set pagination using page and perPage */ forPage(page, perPage) { const skip = (page - 1) * perPage; return this.skip(skip).limit(perPage); } /** * Select specific fields to return (for performance with large objects) */ select(...fields) { this.selectFields = fields; return this; } /** * Search across multiple fields with a single term */ search(term, fields) { this.searchTerm = term; if (fields) { this.searchFields = fields; } return this; } /** * Add multiple where conditions with AND logic */ whereAll(conditions) { conditions.forEach(({ field, operator, value }) => { this.where(field, operator, value); }); return this; } /** * Add multiple where conditions with OR logic */ whereAny(conditions) { this.orFilters.push((item) => { return conditions.some(({ field, operator, value }) => { const fieldValue = item[field]; switch (operator) { case '=': case '==': return fieldValue === value; case '!=': case '<>': return fieldValue !== value; case '>': return value !== undefined && value !== null && fieldValue > value; case '>=': return value !== undefined && value !== null && fieldValue >= value; case '<': return value !== undefined && value !== null && fieldValue < value; case '<=': return value !== undefined && value !== null && fieldValue <= value; case 'like': return (typeof fieldValue === 'string' && typeof value === 'string' && fieldValue.toLowerCase().includes(value.toLowerCase())); case 'in': return Array.isArray(value) && value.includes(fieldValue); case 'not in': return Array.isArray(value) && !value.includes(fieldValue); default: return fieldValue === value; } }); }); return this; } /** * Filter by date range */ whereDateBetween(field, startDate, endDate) { this.filters.push((item) => { const fieldValue = item[field]; if (!fieldValue) return false; const itemDate = fieldValue instanceof Date ? fieldValue : new Date(fieldValue); const start = startDate instanceof Date ? startDate : new Date(startDate); const end = endDate instanceof Date ? endDate : new Date(endDate); return itemDate >= start && itemDate <= end; }); return this; } /** * Filter by array contains */ whereArrayContains(field, value) { this.filters.push((item) => { const fieldValue = item[field]; return Array.isArray(fieldValue) && fieldValue.includes(value); }); return this; } /** * Filter by regex pattern */ whereRegex(field, pattern, flags) { this.filters.push((item) => { const fieldValue = item[field]; if (typeof fieldValue !== 'string') return false; const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, flags); return regex.test(fieldValue); }); return this; } /** * Execute the query and return filtered results */ get() { let results = [...this.items]; // Apply search filter if specified if (this.searchTerm) { const searchLower = this.searchTerm.toLowerCase(); results = results.filter((item) => { const fieldsToSearch = this.searchFields || Object.keys(item); return fieldsToSearch.some((field) => { const value = item[field]; if (typeof value === 'string') { return value.toLowerCase().includes(searchLower); } if (typeof value === 'number') { return value.toString().includes(this.searchTerm); } return false; }); }); } // Apply AND filters for (const filter of this.filters) { results = results.filter(filter); } // Apply OR filters (if any item passes any OR filter, include it) if (this.orFilters.length > 0) { results = results.filter((item) => { return this.orFilters.some((orFilter) => orFilter(item)); }); } // Apply sorting for (const { field, direction } of this.sortOptions) { results.sort((a, b) => { const aValue = a[field]; const bValue = b[field]; if (aValue < bValue) return direction === 'asc' ? -1 : 1; if (aValue > bValue) return direction === 'asc' ? 1 : -1; return 0; }); } // Apply skip if (this.skipValue) { results = results.slice(this.skipValue); } // Apply limit if (this.limitValue) { results = results.slice(0, this.limitValue); } // Apply field selection if specified if (this.selectFields) { results = results.map((item) => { const selected = {}; this.selectFields.forEach((field) => { ; selected[field] = item[field]; }); return selected; }); } return results; } /** * Get the first result */ first() { const results = this.get(); return results.length > 0 ? results[0] : null; } /** * Get the count of results */ count() { return this.get().length; } /** * Check if any results exist */ exists() { return this.count() > 0; } /** * Get paginated results with metadata */ paginate(page, perPage) { // Get total count before pagination const total = this.count(); const totalPages = Math.ceil(total / perPage); // Apply pagination const data = this.forPage(page, perPage).get(); return { data, pagination: { currentPage: page, perPage, total, totalPages, hasNextPage: page < totalPages, hasPrevPage: page > 1, }, }; } /** * Get distinct values for a field */ distinct(field) { const values = this.get().map((item) => item[field]); return [...new Set(values)]; } /** * Group results by a field value */ groupBy(field) { const results = this.get(); const groups = new Map(); results.forEach((item) => { const key = item[field]; if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(item); }); return groups; } /** * Get aggregated statistics for numeric fields */ aggregate(field) { const allValues = this.get().map((item) => item[field]); const numericValues = allValues.filter((value) => typeof value === 'number'); if (numericValues.length === 0) { return null; } const sum = numericValues.reduce((acc, val) => acc + val, 0); const count = numericValues.length; const avg = sum / count; const min = Math.min(...numericValues); const max = Math.max(...numericValues); return { count, sum, avg, min, max }; } /** * Execute a custom callback on the results */ tap(callback) { callback(this.get()); return this; } /** * Clone the query builder for reuse */ clone() { const cloned = new EmbeddedQueryBuilder(this.items); cloned.filters = [...this.filters]; cloned.orFilters = [...this.orFilters]; cloned.sortOptions = [...this.sortOptions]; cloned.limitValue = this.limitValue; cloned.skipValue = this.skipValue; cloned.selectFields = this.selectFields ? [...this.selectFields] : undefined; cloned.searchTerm = this.searchTerm; cloned.searchFields = this.searchFields ? [...this.searchFields] : undefined; return cloned; } }