UNPKG

adonis-odm

Version:

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

438 lines (437 loc) 13.4 kB
import { DateTime } from 'luxon'; import { ObjectId } from 'mongodb'; /** * WhereConditionsBuilder - Handles all WHERE clause construction for MongoDB queries * * This class encapsulates all the logic for building MongoDB filter conditions, * including AND, OR, NOT operations, and various comparison operators. */ export class WhereConditionsBuilder { filters = {}; orConditions = []; /** * Get the current filters */ getFilters() { return this.filters; } /** * Get the current OR conditions */ getOrConditions() { return this.orConditions; } /** * Set filters (used for cloning) */ setFilters(filters) { this.filters = { ...filters }; } /** * Set OR conditions (used for cloning) */ setOrConditions(conditions) { this.orConditions = [...conditions]; } /** * Get the final MongoDB filter object */ getFinalFilters() { if (this.orConditions.length === 0) { return this.filters; } // If we have OR conditions, we need to restructure the query // We want: baseFilters AND (lastAndCondition OR orCondition1 OR orCondition2...) const filterEntries = Object.entries(this.filters); if (filterEntries.length === 0) { // Only OR conditions return { $or: this.orConditions }; } // If we have both AND and OR conditions, we need to be smart about combining them // The last AND condition should be combined with OR conditions const baseFilters = {}; let lastAndField = null; let lastAndCondition = null; // Separate the last AND condition from base filters for (let i = 0; i < filterEntries.length; i++) { const [field, condition] = filterEntries[i]; if (i === filterEntries.length - 1) { // This is the last condition - it should be ORed with orConditions lastAndField = field; lastAndCondition = condition; } else { baseFilters[field] = condition; } } // Create the OR array with the last AND condition and all OR conditions const orArray = []; if (lastAndField && lastAndCondition) { orArray.push({ [lastAndField]: lastAndCondition }); } orArray.push(...this.orConditions); // Build final query if (Object.keys(baseFilters).length > 0) { return { $and: [baseFilters, { $or: orArray }], }; } else { return { $or: orArray }; } } where(field, operatorOrValue, value) { if (value === undefined) { // Simple equality check this.filters = { ...this.filters, [field]: this.serializeValue(operatorOrValue, field), }; } else { // Operator-based condition const operator = operatorOrValue; const mongoOperator = this.mapOperatorToMongo(operator); let serializedValue = this.serializeValue(value, field); // Handle 'like' operator by converting to regex pattern if (operator === 'like') { const pattern = String(value).replace(/%/g, '.*'); serializedValue = new RegExp(pattern, 'i'); } const existingFilter = this.filters[field]; if (existingFilter && typeof existingFilter === 'object' && existingFilter !== null) { this.filters = { ...this.filters, [field]: { ...existingFilter, [mongoOperator]: serializedValue }, }; } else { this.filters = { ...this.filters, [field]: { [mongoOperator]: serializedValue }, }; } } return this; } andWhere(field, operatorOrValue, value) { if (value === undefined) { return this.where(field, operatorOrValue); } return this.where(field, operatorOrValue, value); } whereNot(field, operatorOrValue, value) { if (value === undefined) { this.filters = { ...this.filters, [field]: { $ne: this.serializeValue(operatorOrValue, field) }, }; } else { const operator = operatorOrValue; const mongoOperator = this.mapOperatorToMongo(operator); let serializedValue = this.serializeValue(value, field); // For whereNot, we need to negate the condition if (operator === 'like') { const pattern = String(value).replace(/%/g, '.*'); serializedValue = { $not: new RegExp(pattern, 'i') }; } else { serializedValue = { $not: { [mongoOperator]: serializedValue } }; } this.filters = { ...this.filters, [field]: serializedValue, }; } return this; } /** * Alias for whereNot method */ andWhereNot(field, operatorOrValue, value) { if (value === undefined) { return this.whereNot(field, operatorOrValue); } return this.whereNot(field, operatorOrValue, value); } /** * Add an OR where condition */ orWhere(field, operatorOrValue, value) { let condition; if (value === undefined) { // Simple equality check condition = { [field]: this.serializeValue(operatorOrValue, field) }; } else { // Operator-based condition const operator = operatorOrValue; const mongoOperator = this.mapOperatorToMongo(operator); let serializedValue = this.serializeValue(value, field); // Handle 'like' operator by converting to regex pattern if (operator === 'like') { const pattern = String(value).replace(/%/g, '.*'); serializedValue = new RegExp(pattern, 'i'); } condition = { [field]: { [mongoOperator]: serializedValue } }; } this.orConditions.push(condition); return this; } /** * Add an OR where not condition */ orWhereNot(field, operatorOrValue, value) { let condition; if (value === undefined) { condition = { [field]: { $ne: this.serializeValue(operatorOrValue, field) } }; } else { const operator = operatorOrValue; const mongoOperator = this.mapOperatorToMongo(operator); let serializedValue = this.serializeValue(value, field); // For orWhereNot, we need to negate the condition if (operator === 'like') { const pattern = String(value).replace(/%/g, '.*'); serializedValue = { $not: new RegExp(pattern, 'i') }; } else { serializedValue = { $not: { [mongoOperator]: serializedValue } }; } condition = { [field]: serializedValue }; } this.orConditions.push(condition); return this; } /** * Add a LIKE condition (case-sensitive) */ whereLike(field, value) { const pattern = value.replace(/%/g, '.*'); this.filters = { ...this.filters, [field]: new RegExp(pattern), }; return this; } /** * Add an ILIKE condition (case-insensitive) */ whereILike(field, value) { const pattern = value.replace(/%/g, '.*'); this.filters = { ...this.filters, [field]: new RegExp(pattern, 'i'), }; return this; } /** * Add a NULL condition */ whereNull(field) { this.filters = { ...this.filters, [field]: null }; return this; } /** * Add an OR NULL condition */ orWhereNull(field) { this.orConditions.push({ [field]: null }); return this; } /** * Add a NOT NULL condition */ whereNotNull(field) { this.filters = { ...this.filters, [field]: { $ne: null } }; return this; } /** * Add an OR NOT NULL condition */ orWhereNotNull(field) { this.orConditions.push({ [field]: { $ne: null } }); return this; } /** * Add an EXISTS condition */ whereExists(field) { this.filters = { ...this.filters, [field]: { $exists: true } }; return this; } /** * Add an OR EXISTS condition */ orWhereExists(field) { this.orConditions.push({ [field]: { $exists: true } }); return this; } /** * Add a NOT EXISTS condition */ whereNotExists(field) { this.filters = { ...this.filters, [field]: { $exists: false } }; return this; } /** * Add an OR NOT EXISTS condition */ orWhereNotExists(field) { this.orConditions.push({ [field]: { $exists: false } }); return this; } /** * Add an IN condition */ whereIn(field, values) { const serializedValues = values.map((value) => this.serializeValue(value, field)); this.filters = { ...this.filters, [field]: { $in: serializedValues }, }; return this; } /** * Add an OR IN condition */ orWhereIn(field, values) { const serializedValues = values.map((value) => this.serializeValue(value, field)); this.orConditions.push({ [field]: { $in: serializedValues } }); return this; } /** * Add a NOT IN condition */ whereNotIn(field, values) { const serializedValues = values.map((value) => this.serializeValue(value, field)); this.filters = { ...this.filters, [field]: { $nin: serializedValues }, }; return this; } /** * Add an OR NOT IN condition */ orWhereNotIn(field, values) { const serializedValues = values.map((value) => this.serializeValue(value, field)); this.orConditions.push({ [field]: { $nin: serializedValues } }); return this; } /** * Add a BETWEEN condition */ whereBetween(field, range) { const [min, max] = range; this.filters = { ...this.filters, [field]: { $gte: this.serializeValue(min, field), $lte: this.serializeValue(max, field), }, }; return this; } /** * Add an OR BETWEEN condition */ orWhereBetween(field, range) { const [min, max] = range; this.orConditions.push({ [field]: { $gte: this.serializeValue(min, field), $lte: this.serializeValue(max, field), }, }); return this; } /** * Add a NOT BETWEEN condition */ whereNotBetween(field, range) { const [min, max] = range; this.filters = { ...this.filters, [field]: { $not: { $gte: this.serializeValue(min, field), $lte: this.serializeValue(max, field), }, }, }; return this; } /** * Add an OR NOT BETWEEN condition */ orWhereNotBetween(field, range) { const [min, max] = range; this.orConditions.push({ [field]: { $not: { $gte: this.serializeValue(min, field), $lte: this.serializeValue(max, field), }, }, }); return this; } /** * Map query operator to MongoDB operator */ mapOperatorToMongo(operator) { const operatorMap = { 'eq': '$eq', 'ne': '$ne', 'gt': '$gt', 'gte': '$gte', 'lt': '$lt', 'lte': '$lte', 'in': '$in', 'nin': '$nin', 'exists': '$exists', 'regex': '$regex', 'like': '$regex', '=': '$eq', '!=': '$ne', '>': '$gt', '>=': '$gte', '<': '$lt', '<=': '$lte', }; return operatorMap[operator]; } /** * Serialize value for MongoDB */ serializeValue(value, field) { if (value instanceof DateTime) { return value.toJSDate(); } if (value instanceof Date) { return value; } // Handle ObjectId conversion for _id fields if (field === '_id' && typeof value === 'string') { try { return new ObjectId(value); } catch (error) { // If it's not a valid ObjectId string, return as is return value; } } return value; } /** * Clone the where conditions builder */ clone() { const cloned = new WhereConditionsBuilder(); cloned.setFilters(this.filters); cloned.setOrConditions(this.orConditions); return cloned; } }