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
JavaScript
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;
}
}