@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
309 lines (308 loc) • 9.73 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 { 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);
}
}