UNPKG

@athenna/database

Version:

The Athenna database handler for SQL/NoSQL.

309 lines (308 loc) 9.73 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 { Database } from '#src/facades/Database'; import { Annotation } from '#src/helpers/Annotation'; import { Json, Options, Macroable } from '@athenna/common'; import { NotImplementedRelationException } from '#src/exceptions/NotImplementedRelationException'; export class ModelSchema extends Macroable { constructor(model) { super(); this.Model = model; this.columns = Json.copy(Annotation.getColumnsMeta(model)); this.relations = Json.copy(Annotation.getRelationsMeta(model)); } /** * Sync the database creating migrations * or schemas in the database connection. */ async sync() { await this.getModelDriver().sync(this); } /** * Get the model name set for the schema. */ getModelName() { return this.Model.name; } /** * Get the model table set for the schema. */ getModelTable() { return this.Model.table(); } /** * Get the model driver name. */ getModelDriverName() { const connection = this.getModelConnection(); return Config.get(`database.connections.${connection}.driver`); } /** * Get the model driver. */ getModelDriver() { const connection = this.getModelConnection(); return Database.connection(connection).driver; } /** * Get the model connection name. */ getModelConnection() { const connection = this.Model.connection(); if (connection === 'default') { return Config.get('database.default'); } return connection; } /** * Get the column options of the main primary key. */ getMainPrimaryKey() { let options = this.columns.find(c => c.isMainPrimary); if (!options) { options = this.columns.find(c => c.name === 'id'); if (options) { if (!options.hasSetName && this.getModelDriverName() === 'mongo') { options.name = '_id'; } } } if (!options) { options = this.columns.find(c => c.name === '_id'); } return options; } /** * Get the main primary key column name. */ getMainPrimaryKeyName() { const options = this.getMainPrimaryKey(); return options?.name || 'id'; } /** * Get the main primary key property. */ getMainPrimaryKeyProperty() { const options = this.getMainPrimaryKey(); return options?.property || 'id'; } /** * Convert an object using properties to database use * column names. */ propertiesToColumnNames(data, options = {}) { options = Options.create(options, { attributes: {}, cleanPersist: false }); const parsed = {}; Object.keys(data).forEach(key => { const column = this.getColumnByProperty(key) || { name: key, persist: false }; if (!column.persist && options.cleanPersist) { return; } if (data[key] === undefined) { return; } parsed[column.name] = data[key]; }); Object.keys(options.attributes).forEach(key => { const column = this.getColumnByProperty(key) || { name: key, persist: false }; if (parsed[column.name] !== undefined) { return; } parsed[column.name] = options.attributes[key]; }); return parsed; } /** * Get the column options where column has isCreateDate * as true. */ getCreatedAtColumn() { return this.columns.find(c => c.isCreateDate); } /** * Get the column options where column has isUpdateDate * as true. */ getUpdatedAtColumn() { return this.columns.find(c => c.isUpdateDate); } /** * Get the column options where column has isDeleteDate * as true. */ getDeletedAtColumn() { return this.columns.find(c => c.isDeleteDate); } /** * Get all column properties as an array of string. */ getAllColumnProperties() { return this.columns.map(column => column.property); } /** * Get all column names as an array of string. */ getAllColumnNames() { return this.columns.map(column => column.name); } /** * Get all columns where unique option is true. */ getAllUniqueColumns() { return this.columns.filter(column => column.isUnique); } /** * Get all columns where hidden option is true. */ getAllHiddenColumns() { return this.columns.filter(column => column.isHidden); } /** * Get all columns where nullable option is false. */ getAllNotNullableColumns() { return this.columns.filter(column => !column.isNullable); } /** * Validate that model has createdAt and updatedAt * column defined. */ hasTimestamps() { return !!this.getCreatedAtColumn() && !!this.getUpdatedAtColumn(); } /** * Get the column options by the column database name. */ getColumnByName(column) { return this.columns.find(c => c.name === column); } /** * Get the column options by the column database name. * * If property cannot be found, the column name will be used. */ getPropertyByColumnName(column) { return this.getColumnByName(column)?.property || column; } /** * Get all the properties names by an array of column database names. * * If property cannot be found, the column name will be used. */ getPropertiesByColumnNames(columns) { return columns.map(column => this.getPropertyByColumnName(column)); } /** * Get the column options by the model class property. */ getColumnByProperty(property) { return this.columns.find(c => c.property === property); } /** * Get the column name by the model class property. * * If the column name cannot be found, the property will be used. */ getColumnNameByProperty(property) { return this.getColumnByProperty(property)?.name || property; } /** * Get all the columns names by an array of model class properties. * * If the column name cannot be found, the property will be used. */ getColumnNamesByProperties(properties) { return properties.map(property => this.getColumnNameByProperty(property)); } /** * Get the relation by the class property name. */ getRelationByProperty(property) { return this.relations.find(c => c.property === property); } /** * Relation options used only when eager-loading related rows (`with()` / * {@link ModelSchema.includeRelation includeRelation}). Constraints from * `whereHas()` are not included here; use `with()` when the response must * contain related models. */ getIncludedRelations() { return this.relations.filter(r => r.isIncluded); } /** * Return the relation properties. */ getRelationProperties() { return this.relations.map(r => r.property); } /** * Include a relation by setting the isIncluded * option to true. */ includeRelation(property, closure) { const model = this.Model.name; if (property.includes('.')) { const [first, ...rest] = property.split('.'); property = first; closure = this.createdNestedRelationClosure(rest); } const options = this.getRelationByProperty(property); if (!options) { throw new NotImplementedRelationException(property, model, this.relations.map(r => r.property).join(', ')); } const i = this.relations.indexOf(options); options.isIncluded = true; options.withClosure = closure; this.relations[i] = options; return options; } /** * Marks relation metadata for a `whereHas()` constraint (stores closure). * Does not eager-load related rows; only {@link ModelSchema.includeRelation} * participates in {@link ModelSchema.getIncludedRelations eager loading}. */ includeWhereHasRelation(property, closure) { const model = this.Model.name; if (property.includes('.')) { const [first, ...rest] = property.split('.'); property = first; closure = this.createdNestedRelationClosure(rest); } const options = this.getRelationByProperty(property); if (!options) { throw new NotImplementedRelationException(property, model, this.relations.map(r => r.property).join(', ')); } const i = this.relations.indexOf(options); options.isWhereHasIncluded = true; options.closure = closure; this.relations[i] = options; return options; } /** * Created nested relationships closure to * load relationship's relationships */ createdNestedRelationClosure(relationships) { if (relationships.length === 1) { return (query) => query.with(relationships[0]); } const [first, ...rest] = relationships; const closure = this.createdNestedRelationClosure(rest); return (query) => query.with(first, closure); } }