UNPKG

adonis-odm

Version:

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

393 lines (392 loc) 11.9 kB
import { WhereConditionsBuilder } from './where_conditions_builder.js'; import { QueryExecutor } from './query_executor.js'; import { QueryUtilities } from './query_utilities.js'; /** * SEAMLESS TYPE-SAFE MODEL QUERY BUILDER - Like AdonisJS Lucid! * * This enhanced query builder provides automatic type inference for relationships * and seamless type safety without requiring any type assertions or extra steps. * * Key Features: * - Automatic relationship name inference from model definitions * - Type-safe load callbacks with proper IntelliSense * - Compile-time error checking for invalid relationship names * - Method chaining like AdonisJS Lucid * - Bulk loading to prevent N+1 query problems */ export class ModelQueryBuilder { collection; modelConstructor; whereBuilder; queryExecutor; queryUtils; loadRelations = new Map(); embedRelations = new Map(); constructor(collection, modelConstructor, transactionClient) { this.collection = collection; this.modelConstructor = modelConstructor; const modelClass = modelConstructor; const namingStrategy = modelClass.namingStrategy; this.whereBuilder = new WhereConditionsBuilder(namingStrategy, modelClass); this.queryExecutor = new QueryExecutor(collection, modelConstructor, transactionClient); this.queryUtils = new QueryUtilities(); } /** * Associate this query builder with a transaction */ useTransaction(trx) { this.queryExecutor = new QueryExecutor(this.collection, this.modelConstructor, trx); return this; } where(field, operatorOrValue, value) { if (value === undefined) { this.whereBuilder.where(field, operatorOrValue); } else { this.whereBuilder.where(field, operatorOrValue, value); } return this; } andWhere(field, operatorOrValue, value) { if (value === undefined) { this.whereBuilder.andWhere(field, operatorOrValue); } else { this.whereBuilder.andWhere(field, operatorOrValue, value); } return this; } whereNot(field, operatorOrValue, value) { if (value === undefined) { this.whereBuilder.whereNot(field, operatorOrValue); } else { this.whereBuilder.whereNot(field, operatorOrValue, value); } return this; } /** * Alias for whereNot method */ andWhereNot(field, operatorOrValue, value) { if (value === undefined) { this.whereBuilder.andWhereNot(field, operatorOrValue); } else { this.whereBuilder.andWhereNot(field, operatorOrValue, value); } return this; } /** * Add an OR where condition */ orWhere(field, operatorOrValue, value) { this.whereBuilder.orWhere(field, operatorOrValue, value); return this; } /** * Add an OR where not condition */ orWhereNot(field, operatorOrValue, value) { this.whereBuilder.orWhereNot(field, operatorOrValue, value); return this; } /** * Add a where like condition (case-sensitive) */ whereLike(field, value) { this.whereBuilder.whereLike(field, value); return this; } /** * Add a where ilike condition (case-insensitive) */ whereILike(field, value) { this.whereBuilder.whereILike(field, value); return this; } /** * Add a where null condition */ whereNull(field) { this.whereBuilder.whereNull(field); return this; } /** * Add an OR where null condition */ orWhereNull(field) { this.whereBuilder.orWhereNull(field); return this; } /** * Add a where not null condition */ whereNotNull(field) { this.whereBuilder.whereNotNull(field); return this; } /** * Add an OR where not null condition */ orWhereNotNull(field) { this.whereBuilder.orWhereNotNull(field); return this; } /** * Add a where exists condition */ whereExists(field) { this.whereBuilder.whereExists(field); return this; } /** * Add an OR where exists condition */ orWhereExists(field) { this.whereBuilder.orWhereExists(field); return this; } /** * Add a where not exists condition */ whereNotExists(field) { this.whereBuilder.whereNotExists(field); return this; } /** * Add an OR where not exists condition */ orWhereNotExists(field) { this.whereBuilder.orWhereNotExists(field); return this; } /** * Add a where in condition */ whereIn(field, values) { this.whereBuilder.whereIn(field, values); return this; } /** * Add an OR where in condition */ orWhereIn(field, values) { this.whereBuilder.orWhereIn(field, values); return this; } /** * Add a where not in condition */ whereNotIn(field, values) { this.whereBuilder.whereNotIn(field, values); return this; } /** * Add an OR where not in condition */ orWhereNotIn(field, values) { this.whereBuilder.orWhereNotIn(field, values); return this; } /** * Add a where between condition */ whereBetween(field, range) { this.whereBuilder.whereBetween(field, range); return this; } /** * Add an OR where between condition */ orWhereBetween(field, range) { this.whereBuilder.orWhereBetween(field, range); return this; } /** * Add a where not between condition */ whereNotBetween(field, range) { this.whereBuilder.whereNotBetween(field, range); return this; } /** * Add an OR where not between condition */ orWhereNotBetween(field, range) { this.whereBuilder.orWhereNotBetween(field, range); return this; } /** * Add distinct clause */ distinct(field) { this.queryUtils.distinct(field); return this; } /** * Add group by clause */ groupBy(...fields) { this.queryUtils.groupBy(...fields); return this; } /** * Add having clause (for aggregation) */ having(field, operatorOrValue, value) { this.queryUtils.having(field, operatorOrValue, value); return this; } /** * Add order by clause */ orderBy(field, direction = 'asc') { this.queryUtils.orderBy(field, direction); return this; } /** * Set limit */ limit(count) { this.queryUtils.limit(count); return this; } /** * Set skip/offset */ skip(count) { this.queryUtils.skip(count); return this; } /** * Alias for skip method */ offset(count) { this.queryUtils.offset(count); return this; } /** * Set pagination using page and perPage */ forPage(page, perPage) { this.queryUtils.forPage(page, perPage); return this; } /** * Select specific fields */ select(fields) { this.queryUtils.select(fields); return this; } /** * TYPE-SAFE LOAD METHOD - Eager Load Referenced Relationships! * * This method provides automatic type inference for REFERENCED relationship loading. * The callback parameter is automatically typed based on the relationship being loaded. * Only supports REFERENCED relationships (HasOne, HasMany, BelongsTo). * Embedded relationships are excluded because they need to be "embedded" instead. * * Example usage: * ```typescript * const users = await User.query().load('profile', (profileQuery) => { * profileQuery.where('isActive', true).orderBy('createdAt', 'desc') * }) * ``` * * @param relation - The REFERENCED relationship property name (with IntelliSense support) * @param callback - Optional callback to modify the relationship query */ load(relation, callback) { this.loadRelations.set(String(relation), callback || (() => { })); return this; } /** * TYPE-SAFE EMBED METHOD - Query Embedded Documents Directly! * * This method provides automatic type inference for EMBEDDED document querying. * The callback parameter is automatically typed based on the embedded relationship being queried. * Only supports EMBEDDED relationships (EmbeddedSingle, EmbeddedMany). * Referenced relationships are excluded because they need to be "loaded" instead. * * Example usage: * ```typescript * const users = await UserWithEnhancedEmbeddedProfile.query().embed('profiles', (profileQuery) => { * profileQuery.where('age', '>', 25).orderBy('age', 'desc').limit(5) * }) * ``` * * @param relation - The EMBEDDED relationship property name (with IntelliSense support) * @param callback - Optional callback to filter/paginate the embedded documents */ embed(relation, callback) { this.embedRelations.set(String(relation), callback || (() => { })); return this; } /** * Execute query and return first result */ async first() { return this.queryExecutor.first(this.whereBuilder.getFinalFilters(), this.queryUtils.getSelectFields(), this.queryUtils.getSortOptions(), this.loadRelations, this.embedRelations); } /** * Execute query and return first result or throw exception */ async firstOrFail() { return this.queryExecutor.firstOrFail(this.whereBuilder.getFinalFilters(), this.queryUtils.getSelectFields(), this.queryUtils.getSortOptions(), this.loadRelations, this.embedRelations); } /** * Execute query and return all results */ async all() { return this.fetch(); } /** * Execute query and return all results (alias for all) */ async fetch() { return this.queryExecutor.fetch(this.whereBuilder.getFinalFilters(), this.queryUtils.getSelectFields(), this.queryUtils.getSortOptions(), this.queryUtils.getLimitValue(), this.queryUtils.getSkipValue(), this.queryUtils.getDistinctField(), this.queryUtils.getGroupByFields(), this.queryUtils.getHavingConditions(), this.loadRelations, this.embedRelations); } /** * Execute query and return paginated results */ async paginate(page, perPage) { return this.queryExecutor.paginate(page, perPage, this.whereBuilder.getFinalFilters(), this.queryUtils.getSelectFields(), this.queryUtils.getSortOptions(), this.loadRelations); } /** * Count documents matching the query */ async count() { return this.queryExecutor.count(this.whereBuilder.getFinalFilters()); } /** * Get array of IDs for matching documents */ async ids() { return this.queryExecutor.ids(this.whereBuilder.getFinalFilters()); } /** * Update documents matching the query */ async update(updateData) { return this.queryExecutor.update(this.whereBuilder.getFinalFilters(), updateData); } /** * Delete documents matching the query */ async delete() { return this.queryExecutor.delete(this.whereBuilder.getFinalFilters()); } /** * Clone the query builder */ clone() { const cloned = new ModelQueryBuilder(this.collection, this.modelConstructor); cloned.whereBuilder = this.whereBuilder.clone(); cloned.queryUtils = this.queryUtils.clone(); cloned.loadRelations = new Map(this.loadRelations); cloned.embedRelations = new Map(this.embedRelations); return cloned; } }