UNPKG

@athenna/database

Version:

The Athenna database handler for SQL/NoSQL.

795 lines (794 loc) 24.7 kB
/** * @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); } } }