@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
795 lines (794 loc) • 24.7 kB
JavaScript
/**
* @athenna/database
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import { Collection, Is, Options } from '@athenna/common';
import { QueryBuilder } from '#src/database/builders/QueryBuilder';
import { ModelGenerator } from '#src/models/factories/ModelGenerator';
import { UniqueValueException } from '#src/exceptions/UniqueValueException';
import { NotFoundDataException } from '#src/exceptions/NotFoundDataException';
import { NullableValueException } from '#src/exceptions/NullableValueException';
export class ModelQueryBuilder extends QueryBuilder {
constructor(model, driver) {
super(driver, model.table());
this.isToSetAttributes = true;
this.isToValidateUnique = true;
this.isToValidateNullable = true;
this.selectColumns = [];
this.DELETED_AT_PROP = null;
this.DELETED_AT_NAME = null;
this.isSoftDelete = false;
this.hasCustomSelect = false;
this.Model = model;
this.schema = model.schema();
this.generator = new ModelGenerator(this.Model, this.schema);
this.primaryKeyName = this.schema.getMainPrimaryKeyName();
this.primaryKeyProperty = this.schema.getMainPrimaryKeyProperty();
const deletedAtColumn = this.schema.getDeletedAtColumn();
if (deletedAtColumn) {
this.isSoftDelete = true;
this.DELETED_AT_NAME = deletedAtColumn.name;
this.DELETED_AT_PROP = deletedAtColumn.property;
}
this.selectColumns = this.schema.getAllColumnNames();
this.setPrimaryKey(this.primaryKeyName);
}
/**
* Calculate the average of a given column.
*/
async avg(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.avg(name);
}
/**
* Calculate the average of a given column.
*/
async avgDistinct(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.avgDistinct(name);
}
/**
* Get the max number of a given column.
*/
async max(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.max(name);
}
/**
* Get the min number of a given column.
*/
async min(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.min(name);
}
/**
* Sum all numbers of a given column.
*/
async sum(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.sum(name);
}
/**
* Sum all numbers of a given column.
*/
async sumDistinct(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.sumDistinct(name);
}
/**
* Increment a value of a given column.
*/
async increment(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.increment(name);
}
/**
* Decrement a value of a given column.
*/
async decrement(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
await super.decrement(name);
}
/**
* Calculate the average of a given column using distinct.
*/
async count(column) {
this.setInternalQueries();
if (!column) {
return super.count();
}
const name = this.schema.getColumnNameByProperty(column);
return super.count(name);
}
/**
* Calculate the average of a given column using distinct.
*/
async countDistinct(column) {
this.setInternalQueries();
const name = this.schema.getColumnNameByProperty(column);
return super.countDistinct(name);
}
/**
* Find value in database but returns only the value of
* selected column directly.
*/
async pluck(column) {
this.setInternalQueries();
const columnName = this.schema.getColumnNameByProperty(column);
return super.pluck(columnName);
}
/**
* Find many values in database but returns only the
* values of selected column directly.
*/
async pluckMany(column) {
this.setInternalQueries();
const columnName = this.schema.getColumnNameByProperty(column);
return super.pluckMany(columnName);
}
/**
* Find a value in database.
*/
async find() {
this.setInternalQueries();
const data = await super.find();
return this.generator.generateOne(data);
}
/**
* Find a value in database or throw exception if undefined.
*/
async findOrFail() {
const data = await this.find();
if (!data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
throw new NotFoundDataException(this.Model.connection());
}
return data;
}
/**
* Return a single data or, if no results are found,
* execute the given closure.
*/
async findOr(closure) {
const data = (await this.find());
if (!data) {
return closure();
}
return data;
}
/**
* Find many values in database.
*/
async findMany() {
this.setInternalQueries();
const data = await super.findMany();
return this.generator.generateMany(data);
}
/**
* Find many values in database and return paginated.
*/
async paginate(page = { page: 0, limit: 10, resourceUrl: '/' }, limit = 10, resourceUrl = '/') {
this.setInternalQueries();
const data = await super.paginate(page, limit, resourceUrl);
data.data = await this.generator.generateMany(data.data);
return data;
}
/**
* Find many values in database and return
* as a collection instance.
*/
async collection() {
const models = await this.findMany();
return new Collection(models);
}
/**
* Create a value in database.
*/
async create(data = {}, cleanPersist = true) {
const created = await this.createMany([data], cleanPersist);
return created[0];
}
/**
* Create many values in database.
*/
async createMany(data, cleanPersist = true) {
data = await Promise.all(data.map(async (d) => {
const date = new Date();
const createdAt = this.schema.getCreatedAtColumn();
const updatedAt = this.schema.getUpdatedAtColumn();
const deletedAt = this.schema.getDeletedAtColumn();
const attributes = this.isToSetAttributes ? this.Model.attributes() : {};
const parsed = this.schema.propertiesToColumnNames(d, {
attributes,
cleanPersist
});
if (createdAt && parsed[createdAt.name] === undefined) {
parsed[createdAt.name] = date;
}
if (updatedAt && parsed[updatedAt.name] === undefined) {
parsed[updatedAt.name] = date;
}
if (deletedAt && parsed[deletedAt.name] === undefined) {
parsed[deletedAt.name] = null;
}
this.validateNullable(parsed);
await this.validateUnique(parsed);
return parsed;
}));
const created = await super.createMany(data);
return this.generator.generateMany(created);
}
/**
* Create or update a value in database.
*/
async createOrUpdate(data, cleanPersist = true) {
const hasValue = await this.find();
if (hasValue) {
const pk = this.primaryKeyProperty;
return this.where(pk, hasValue[pk]).update(data, cleanPersist);
}
return this.create(data, cleanPersist);
}
/**
* Update a value in database.
*/
async update(data, cleanPersist = true) {
this.setInternalQueries();
const date = new Date();
const updatedAt = this.schema.getUpdatedAtColumn();
const attributes = this.isToSetAttributes ? this.Model.attributes() : {};
const parsed = this.schema.propertiesToColumnNames(data, {
attributes,
cleanPersist
});
if (updatedAt && parsed[updatedAt.name] === undefined) {
parsed[updatedAt.name] = date;
}
await this.validateUnique(parsed, true);
const updated = await super.update(parsed);
if (Is.Array(updated)) {
return this.generator.generateMany(updated);
}
return this.generator.generateOne(updated);
}
/**
* Delete or soft delete a value in database.
*/
async delete(force = false) {
this.setInternalQueries({ addSelect: false });
if (!this.DELETED_AT_NAME || force) {
await super.delete();
return;
}
await this.update({ [this.DELETED_AT_PROP]: new Date() });
}
/**
* Restore one or multiple soft deleted models.
*/
async restore() {
this.setInternalQueries({ addSoftDelete: false });
if (!this.DELETED_AT_PROP) {
return;
}
const updatedAt = this.schema.getUpdatedAtColumn();
const data = { [this.DELETED_AT_PROP]: null };
if (updatedAt) {
data[updatedAt.name] = new Date();
}
const updated = await super.update(data);
if (Is.Array(updated)) {
return this.generator.generateMany(updated);
}
return this.generator.generateOne(updated);
}
/**
* Retrieve only the values that are soft deleted in
* database.
*/
onlyTrashed() {
this.isSoftDelete = false;
if (!this.DELETED_AT_PROP) {
return this;
}
return this.whereNotNull(this.DELETED_AT_PROP);
}
/**
* Retrieve values that are soft deleted in database.
*/
withTrashed() {
this.isSoftDelete = false;
if (!this.DELETED_AT_PROP) {
return;
}
if (this.schema.getModelDriverName() === 'mongo') {
this.orWhereNull(this.DELETED_AT_PROP);
}
return this.orWhereNotNull(this.DELETED_AT_PROP);
}
/**
* Enable/disable setting the default attributes properties
* when creating/updating models.
*/
setAttributes(value) {
this.isToSetAttributes = value;
return this;
}
/**
* Enable/disable the `isUnique` property validation of
* models columns.
*/
uniqueValidation(value) {
this.isToValidateUnique = value;
return this;
}
/**
* Enable/disable the `isNullable` property validation of
* models columns.
*/
nullableValidation(value) {
this.isToValidateNullable = value;
return this;
}
/**
* Eager load a relation in your query.
*/
with(relation, closure) {
this.schema.includeRelation(relation, closure);
return this;
}
/**
* Executes the given closure when the first argument is true.
*/
when(criteria, closure) {
if (criteria) {
closure(this, criteria);
return this;
}
return this;
}
/**
* Set the columns that should be selected on query.
*/
select(...columns) {
if (!this.hasCustomSelect) {
this.hasCustomSelect = true;
this.selectColumns = columns.map(c => this.schema.getColumnNameByProperty(c));
return this;
}
columns.forEach(column => {
const index = this.selectColumns.indexOf(column);
if (index) {
return;
}
this.selectColumns.push(this.schema.getColumnNameByProperty(column));
});
return this;
}
/**
* Set a group by statement in your query.
*/
groupBy(...columns) {
super.groupBy(...this.schema.getColumnNamesByProperties(columns));
return this;
}
/**
* Set a having statement in your query.
*/
having(column, operation, value) {
const name = this.schema.getColumnNameByProperty(column);
super.having(name, operation, value);
return this;
}
/**
* Set a having in statement in your query.
*/
havingIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.havingIn(name, values);
return this;
}
/**
* Set a having not in statement in your query.
*/
havingNotIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.havingNotIn(name, values);
return this;
}
/**
* Set a having between statement in your query.
*/
havingBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.havingBetween(name, values);
return this;
}
/**
* Set a having not between statement in your query.
*/
havingNotBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.havingNotBetween(name, values);
return this;
}
/**
* Set a having null statement in your query.
*/
havingNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.havingNull(name);
return this;
}
/**
* Set a having not null statement in your query.
*/
havingNotNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.havingNotNull(name);
return this;
}
/**
* Set a orHaving statement in your query.
*/
orHaving(column, operation, value) {
const name = this.schema.getColumnNameByProperty(column);
super.orHaving(name, operation, value);
return this;
}
/**
* Set a orHaving in statement in your query.
*/
orHavingIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingIn(name, values);
return this;
}
/**
* Set a orHaving not in statement in your query.
*/
orHavingNotIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingNotIn(name, values);
return this;
}
/**
* Set a orHaving between statement in your query.
*/
orHavingBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingBetween(name, values);
return this;
}
/**
* Set a orHaving not between statement in your query.
*/
orHavingNotBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingNotBetween(name, values);
return this;
}
/**
* Set a orHaving null statement in your query.
*/
orHavingNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingNull(name);
return this;
}
/**
* Set a orHaving not null statement in your query.
*/
orHavingNotNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.orHavingNotNull(name);
return this;
}
/**
* Set a where statement in your query.
*/
where(statement, operation, value) {
if (!operation) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.where(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.where(name, operation, value);
return this;
}
/**
* Set a where not statement in your query.
*/
whereNot(statement, value) {
if (!value) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.whereNot(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.whereNot(name, value);
return this;
}
/**
* Set a where like statement in your query.
*/
whereLike(column, value) {
const name = this.schema.getColumnNameByProperty(column);
super.whereLike(name, value);
return this;
}
/**
* Set a where ILike statement in your query.
*/
whereILike(column, value) {
const name = this.schema.getColumnNameByProperty(column);
super.whereILike(name, value);
return this;
}
/**
* Set a where in statement in your query.
*/
whereIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.whereIn(name, values);
return this;
}
/**
* Set a where not in statement in your query.
*/
whereNotIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.whereNotIn(name, values);
return this;
}
/**
* Set a where between statement in your query.
*/
whereBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.whereBetween(name, values);
return this;
}
/**
* Set a where not between statement in your query.
*/
whereNotBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.whereNotBetween(name, values);
return this;
}
/**
* Set a where null statement in your query.
*/
whereNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.whereNull(name);
return this;
}
/**
* Set a where not null statement in your query.
*/
whereNotNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.whereNotNull(name);
return this;
}
/**
* Set a orWhere statement in your query.
*/
orWhere(statement, operation, value) {
if (!operation) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.orWhere(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.orWhere(name, operation, value);
return this;
}
/**
* Set a orWhere not statement in your query.
*/
orWhereNot(statement, value) {
if (!value) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.orWhereNot(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.orWhereNot(name, value);
return this;
}
/**
* Set a orWhere like statement in your query.
*/
orWhereLike(statement, value) {
if (!value) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.orWhereLike(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.orWhereLike(name, value);
return this;
}
/**
* Set a orWhere ILike statement in your query.
*/
orWhereILike(statement, value) {
if (!value) {
const parsed = this.schema.propertiesToColumnNames(statement);
super.orWhereILike(parsed);
return this;
}
const name = this.schema.getColumnNameByProperty(statement);
super.orWhereILike(name, value);
return this;
}
/**
* Set a orWhere in statement in your query.
*/
orWhereIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereIn(name, values);
return this;
}
/**
* Set a orWhere not in statement in your query.
*/
orWhereNotIn(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereNotIn(name, values);
return this;
}
/**
* Set a orWhere between statement in your query.
*/
orWhereBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereBetween(name, values);
return this;
}
/**
* Set a orWhere not between statement in your query.
*/
orWhereNotBetween(column, values) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereNotBetween(name, values);
return this;
}
/**
* Set a orWhere null statement in your query.
*/
orWhereNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereNull(name);
return this;
}
/**
* Set a orWhere not null statement in your query.
*/
orWhereNotNull(column) {
const name = this.schema.getColumnNameByProperty(column);
super.orWhereNotNull(name);
return this;
}
/**
* Set an order by statement in your query.
*/
orderBy(column, direction = 'ASC') {
const name = this.schema.getColumnNameByProperty(column);
super.orderBy(name, direction);
return this;
}
/**
* Order the results easily by the latest date. By default, the result will
* be ordered by the table's "createdAt" column.
*/
latest(column) {
if (!column) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
column = 'createdAt';
}
const name = this.schema.getColumnNameByProperty(column);
super.latest(name);
return this;
}
/**
* Order the results easily by the oldest date. By default, the result will
* be ordered by the table's "createdAt" column.
*/
oldest(column) {
if (!column) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
column = 'createdAt';
}
const name = this.schema.getColumnNameByProperty(column);
super.oldest(name);
return this;
}
/**
* Set the internal selected properties and soft delete
* queries.
*/
setInternalQueries(options) {
options = Options.create(options, {
addSelect: true,
addSoftDelete: true
});
if (options.addSelect) {
super.select(...this.selectColumns);
}
if (options.addSoftDelete) {
super.when(this.isSoftDelete, query => query.whereNull(this.DELETED_AT_NAME));
}
}
/**
* Verify that columns with `isNullable` property
* can be created in database.
*/
validateNullable(data) {
if (!this.isToValidateNullable) {
return;
}
const records = [];
for (const column of this.schema.getAllNotNullableColumns()) {
const value = data[column.name];
if (value === undefined || value === null) {
records.push(column.property);
}
}
if (!Is.Empty(records)) {
throw new NullableValueException(records);
}
}
/**
* Verify that columns with isUnique property
* can be created in database.
*/
async validateUnique(data, isUpdate = false) {
if (!this.isToValidateUnique) {
return;
}
const records = {};
for (const column of this.schema.getAllUniqueColumns()) {
const value = data[column.name];
if (value === undefined) {
continue;
}
if (isUpdate) {
const data = await this.Model.query()
.where(column.name, value)
.findMany();
if (data.length > 1) {
records[column.property] = value;
continue;
}
}
const isDuplicated = await this.Model.query()
.where(column.name, value)
.exists();
if (isDuplicated) {
records[column.property] = value;
}
}
if (!Is.Empty(records)) {
throw new UniqueValueException(records);
}
}
}