UNPKG

@athenna/database

Version:

The Athenna database handler for SQL/NoSQL.

543 lines (542 loc) 16.9 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 { Is, Json, String, Options, Collection } from '@athenna/common'; import { Database } from '#src/facades/Database'; import { faker } from '@faker-js/faker'; import { ModelSchema } from '#src/models/schemas/ModelSchema'; import { ORIGINAL_SYMBOL } from '#src/constants/OriginalSymbol'; import { ModelFactory } from '#src/models/factories/ModelFactory'; import { ModelGenerator } from '#src/models/factories/ModelGenerator'; import { ModelQueryBuilder } from '#src/models/builders/ModelQueryBuilder'; export class BaseModel { /** * Set if the `attributes` method should be called or not. */ static get isToSetAttributes() { return Config.get(`database.connections.${this.connection()}.validations.isToSetAttributes`, true); } /** * Set if the option annotation `isUnique` * should be verified or not. */ static get isToValidateUnique() { return Config.get(`database.connections.${this.connection()}.validations.isToValidateUnique`, true); } /** * Set if the option annotation `isNullable` * should be verified or not. */ static get isToValidateNullable() { return Config.get(`database.connections.${this.connection()}.validations.isToValidateNullable`, true); } /** * The faker instance to create fake data in * definition instance. */ static get faker() { return faker; } /** * Set the connection name that model will use * to access database. */ static connection() { return Config.get('database.default'); } /** * Set if model should automatically be sync with * database when running DatabaseProvider. * * @default true */ static sync() { const connection = this.connection(); const driver = Config.get(`database.connections.${connection}.driver`); if (driver === 'mongo') { return true; } return false; } /** * Set the table name of this model instance. */ static table() { return String.pluralize(String.toSnakeCase(this.name).toLowerCase()); } /** * Set the default values that should be set when creating or * updating the model. */ static attributes() { return {}; } /** * Set the definition data that will be used when fabricating * instances of your model using factories. */ static async definition() { return {}; } /** * Create a new ModelSchema instance from your model. */ static schema() { return new ModelSchema(this); } /** * Create a new ModelFactory instance from your model. */ static factory() { return new ModelFactory(this); } /** * Enable/disable setting the default attributes properties * when creating/updating models. */ static setAttributes(value) { Config.set(`database.connections.${this.connection()}.validations.isToSetAttributes`, value); return this; } /** * Enable/disable the `isUnique` property validation of * models columns. */ static uniqueValidation(value) { Config.set(`database.connections.${this.connection()}.validations.isToValidateUnique`, value); return this; } /** * Enable/disable the `isNullable` property validation of * models columns. */ static nullableValidation(value) { Config.set(`database.connections.${this.connection()}.validations.isToValidateNullable`, value); return this; } /** * Create a query builder for the model. */ static query() { const driver = Database.connection(this.connection()).driver; return new ModelQueryBuilder(this, driver) .setAttributes(this.isToSetAttributes) .uniqueValidation(this.isToValidateUnique) .nullableValidation(this.isToValidateNullable); } /** * Remove all data inside model table * and restart the identity of the table. */ static async truncate() { await Database.connection(this.connection()).truncate(this.table()); } static async pluck(key, where) { const query = this.query(); if (where) { query.where(where); } return query.pluck(key); } static async pluckMany(key, where) { const query = this.query(); if (where) { query.where(where); } return query.pluckMany(key); } /** * Find a value in database. */ static async find(where) { const query = this.query(); if (where) { query.where(where); } return query.find(); } /** * Find a value in database. */ static async exists(where) { const query = this.query(); if (where) { query.where(where); } return query.exists(); } /** * Find a value in database or throw exception if undefined. */ static async findOrFail(where) { const query = this.query(); if (where) { query.where(where); } return query.findOrFail(); } /** * Find a value in database or create a new one if it doesn't exist. */ static async findOrCreate(where, data) { const query = this.query(); if (where) { query.where(where); } return query.findOrCreate(data); } /** * Return a single data or, if no results are found, * execute the given closure. */ static async findOr(where, closure) { const query = this.query(); if (where) { query.where(where); } return query.findOr(closure); } /** * Find many values in database. */ static async findMany(where) { const query = this.query(); if (where) { query.where(where); } return query.findMany(); } /** * Find many values in database and return paginated. */ static async paginate(options, where) { const query = this.query(); if (where) { query.where(where); } return query.paginate(options); } /** * Find many values in database and return * as a collection instance. */ static async collection(where) { const query = this.query(); if (where) { query.where(where); } return query.collection(); } /** * Create a value in database. */ static async create(data = {}, cleanPersist = true) { return this.query().create(data, cleanPersist); } /** * Create many values in database. */ static async createMany(data, cleanPersist = true) { return this.query().createMany(data, cleanPersist); } /** * Create or update a value in database. */ static async createOrUpdate(where, data, cleanPersist = true) { const query = this.query(); if (where) { query.where(where); } return query.createOrUpdate(data, cleanPersist); } /** * Update a value in database. */ static async update(where, data, cleanPersist = true) { const query = this.query(); if (where) { query.where(where); } return query.update(data, cleanPersist); } /** * Restore a soft deleted value from database. */ static async restore(where, data) { const query = this.query(); if (where) { query.where(where); } return query.restore(data); } /** * Delete or soft delete a value in database. */ static async delete(where, force = false) { const query = this.query(); if (where) { query.where(where); } return query.delete(force); } /** * Set the original model values by deep copying * the model state. */ setOriginal() { this[ORIGINAL_SYMBOL] = {}; const copied = Json.copy(this); Object.keys(copied).forEach(key => { const value = this[key]; if (Is.Array(value) && value[0] && ORIGINAL_SYMBOL in value[0]) { return; } if (Is.Object(value) && value && ORIGINAL_SYMBOL in value) { return; } this[ORIGINAL_SYMBOL][key] = copied[key]; }); return this; } /** * Return a Json object from the actual subclass instance. */ toJSON(options) { options = Options.create(options, { withHidden: false }); const _Model = this.constructor; const json = {}; const relations = _Model.schema().getRelationProperties(); /** * Execute the toJSON of relations. */ Object.keys(this).forEach(key => { if (relations.includes(key)) { if (Is.Array(this[key])) { json[key] = this[key].map(d => (d.toJSON ? d.toJSON() : d)); return; } json[key] = this[key]?.toJSON ? this[key].toJSON() : this[key]; return; } if (!options.withHidden && _Model.schema().getColumnByProperty(key)?.isHidden) { return; } json[key] = this[key]; }); return json; } /** * Eager load a model relation from model instance. */ async load(relation, closure) { const Model = this.constructor; const schema = Model.schema(); const generator = new ModelGenerator(Model, schema); await generator.includeRelation(this, schema.includeRelation(relation, closure)); if (relation.includes('.')) { return Json.get(this, relation); } return this[relation]; } /** * Eager load a model relation without mutating * the current model instance. */ async loadOnly(relation, closure) { const Model = this.constructor; const schema = Model.schema(); const generator = new ModelGenerator(Model, schema); const copy = new Model(); const relations = schema.getRelationProperties(); Object.keys(this).forEach(key => { if (relations.includes(key)) { return; } copy[key] = Json.copy(this[key]); }); await generator.includeRelation(copy, schema.includeRelation(relation, closure)); if (relation.includes('.')) { return Json.get(copy, relation); } return copy[relation]; } /** * Validate if model is persisted in database * or if it's a fresh instance. */ isPersisted() { return !!this[ORIGINAL_SYMBOL]; } /** * Get values only that are different from * the original symbol to avoid updating * data that was not changed. */ dirty() { if (!this.isPersisted()) { return this; } const dirty = {}; Object.keys(this).forEach(key => { const orig = this[ORIGINAL_SYMBOL][key]; const curr = this[key]; if (Json.isEqual(orig, curr)) { return; } if (Is.Object(curr) || Is.Array(curr)) { dirty[key] = Json.copy(curr); return; } dirty[key] = Json.diff(orig, curr); }); return dirty; } /** * Validate if model has been changed from * it initial state when it was retrieved from * database. */ isDirty() { return Object.keys(this.dirty()).length > 0; } /** * Save the changes done in the model in database. */ async save(cleanPersist = true) { const Model = this.constructor; const schema = Model.schema(); const primaryKey = schema.getMainPrimaryKeyProperty(); const date = new Date(); const createdAt = schema.getCreatedAtColumn(); const updatedAt = schema.getUpdatedAtColumn(); const deletedAt = schema.getDeletedAtColumn(); const attributes = Model.isToSetAttributes ? Model.attributes() : {}; Object.keys(attributes).forEach(key => { if (this[key]) { return; } this[key] = attributes[key]; }); if (createdAt && this[createdAt.property] === undefined) { this[createdAt.property] = date; } if (updatedAt && this[updatedAt.property] === undefined) { this[updatedAt.property] = date; } if (deletedAt && this[deletedAt.property] === undefined) { this[deletedAt.property] = null; } const data = this.dirty(); if (!this.isPersisted()) { const created = await Model.create(data, cleanPersist); Object.keys(created).forEach(key => (this[key] = created[key])); return this.setOriginal(); } /** * Means data is not dirty because there are any * value that is different from original symbol. */ if (!Object.keys(data).length) { return this; } const where = { [primaryKey]: this[primaryKey] }; const updated = await Model.update(where, data, cleanPersist); Object.keys(updated).forEach(key => (this[key] = updated[key])); return this.setOriginal(); } /** * Create a new instance of the model from retrieving * again the data from database. The existing * model instance WILL NOT BE affected. */ async fresh() { const Model = this.constructor; const primaryKey = Model.schema().getMainPrimaryKeyProperty(); return Model.query() .where(primaryKey, this[primaryKey]) .withTrashed() .find(); } /** * Refresh the model instance data retrieving * model data using the main primary key. The * existing model instance WILL BE affected. */ async refresh() { const Model = this.constructor; const schema = Model.schema(); const relations = schema.getRelationProperties(); const primaryKey = schema.getMainPrimaryKeyProperty(); const query = Model.query() .where(primaryKey, this[primaryKey]) .withTrashed(); Object.keys(this).forEach(key => { if (!relations.includes(key)) { return; } query.with(key); }); const data = await query.find(); Object.keys(data).forEach(key => (this[key] = data[key])); } /** * Verify if model is soft deleted. */ isTrashed() { const Model = this.constructor; const deletedAt = Model.schema().getDeletedAtColumn(); return !!this[deletedAt.property]; } /** * Delete or soft delete your model from database. */ async delete(force = false) { const Model = this.constructor; const primaryKey = Model.schema().getMainPrimaryKeyProperty(); await Model.query().where(primaryKey, this[primaryKey]).delete(force); } /** * Restore a soft deleted model from database. */ async restore() { const Model = this.constructor; const schema = Model.schema(); const primaryKey = schema.getMainPrimaryKeyProperty(); const date = new Date(); const createdAt = schema.getCreatedAtColumn(); const updatedAt = schema.getUpdatedAtColumn(); const deletedAt = schema.getDeletedAtColumn(); const attributes = Model.isToSetAttributes ? Model.attributes() : {}; Object.keys(attributes).forEach(key => { if (this[key]) { return; } this[key] = attributes[key]; }); if (createdAt && this[createdAt.property] === undefined) { this[createdAt.property] = date; } if (updatedAt && this[updatedAt.property] === undefined) { this[updatedAt.property] = date; } /** * Forcing the deleted at column to be null to restore the model. */ if (deletedAt) { this[deletedAt.property] = null; } const data = this.dirty(); const where = { [primaryKey]: this[primaryKey] }; const restored = await Model.restore(where, data); Object.keys(restored).forEach(key => (this[key] = restored[key])); return this.setOriginal(); } }