UNPKG

@ostro/database

Version:
949 lines (734 loc) 20.3 kB
const { Macroable } = require('@ostro/support/macro') const Query = require('../query/builder') const MethodNotAvailable = require('@ostro/support/exceptions/methodNotAvailable') const DateTime = require('@ostro/support/dateTime') const GuardsAttributes = require('./concern/guardsAttributes') const HasRelationships = require('./concern/hasRelationships') const HidesAttributes = require('./concern/hidesAttributes') const QueriesRelationships = require('./concern/queriesRelationships') const HasAttributes = require('./concern/hasAttributes') const HasTimestamps = require('./concern/hasTimestamps') const BelongsToMany = require('./relations/belongsToMany') const Relation = require('./relations/relation') const RelationNotFoundException = require('./relationNotFoundException') const ModelNotFoundException = require('./modelNotFoundException') const kResolver = Symbol('resolver') const kQuery = Symbol('query') const kEagerLoad = Symbol('eagerLoad') const kModel = Symbol('model') const kScopes = Symbol('scopes') const kRemovedScopes = Symbol('removeScopes') const kWasRecentlyCreated = Symbol('wasRecentlyCreated') const kPerformRelationQuery = Symbol('performRelationQuery') const kLazyQueries = Symbol('lazyQueries') const Collection = require('./collection') const ModelInterface = require('@ostro/contracts/database/eloquent/model') const { is_array, count, in_array, clone } = require('@ostro/support/function') class Model extends implement(ModelInterface, Query, GuardsAttributes, QueriesRelationships, HasRelationships, HasAttributes, HidesAttributes, HasTimestamps) { $table = String.snakeCase(this.constructor.name).toLowerCase().plural(); get $query() { return this[kQuery] = this[kQuery] || Model.getConnectionResolver().table(this.$table) } set $query($value) { return this[kQuery] = $value } $connection = ''; $primaryKey = 'id'; $keyType = 'string'; CREATED_AT = 'created_at'; UPDATED_AT = 'updated_at'; $incrementing = true; static [kResolver] = null; $exists = false; [kScopes] = {}; [kRemovedScopes] = []; [kEagerLoad] = {}; [kLazyQueries] = []; [kWasRecentlyCreated] = false; [kModel] = null; constructor($attributes = {}, newInstance = true) { super() Object.defineProperty(this, kQuery, { value: null, writable: true }); const instance = newInstance == true ? this.newInstance($attributes, this.$exists, false) : this; if (instance.$fillable.length) { instance.fill($attributes); } return instance } raw() { return Model.getConnectionResolver().raw(...arguments) } getConnection() { return get_class(this).resolveConnection(this.getConnectionName()); } getConnectionName() { return this.$connection; } setConnection($name) { this.$connection = $name; return this; } static resolveConnection($connection = null) { return this[kResolver].connection($connection); } static getConnectionResolver() { return this[kResolver]; } static setConnectionResolver($resolver) { this[kResolver] = $resolver; } static unsetConnectionResolver() { this[kResolver] = null; } fillable(obj = {}) { let fillKeys = Object.keys(obj) let $massAssign = this.$fillable.intersection(fillKeys) if ($massAssign.length == 0 && fillKeys.length) { throw Error('Add column to fillable property to allow mass assignment on [' + this.constructor.name + '].') } for (let fillable of $massAssign) { if (obj.hasOwnProperty(fillable)) { let fn = this['set' + fillable.ucfirst() + 'Attribute'] if (typeof fn == 'function') { this['set' + fillable.ucfirst() + 'Attribute'](obj[fillable]) } else { this.setAttribute(fillable, obj[fillable]) } } } } fill($attributes = {}) { let $totallyGuarded = this.totallyGuarded(); let $fillableAttributes = this.fillableFromArray($attributes) for (let $key of $fillableAttributes) { let $value = $attributes[$key] if (this.isFillable($key)) { this.setAttribute($key, $value); } else if ($totallyGuarded) { throw new Error(sprintf( 'Add [%s] to fillable property to allow mass assignment on [%s].', $key, get_class(this) )); } } return this; } addTimestampsToInsertValues(datas) { if (this.$timestamps == true) { let datetime = DateTime.now().format(this.$dateFormat); datas = !Array.isArray(datas) ? [datas] : datas; for (var i = 0; i < datas.length; i++) { datas[i][this.CREATED_AT] = datetime datas[i][this.UPDATED_AT] = datetime } } } updateInserdtId($datas, $ids) { const keys = this.getKeyName(); for (var i = 0; i < $datas.length; i++) { let value = $ids[i]; if (Array.isArray(keys)) { for (let key of keys) { if (typeof value == 'object') { value = value[key] } $datas[i][key] = value } } else { if (typeof value == 'object') { value = value[keys] } $datas[i][keys] = value } } } getTable() { return this.$table; } setTable($table) { this.$table = $table; return this; } getKeyName() { return this.$primaryKey; } setKeyName($key) { this.$primaryKey = $key; return this; } getKeyType() { return this.$keyType; } setKeyType($type) { this.$keyType = $type; return this; } getKey() { return this.getAttribute(this.getKeyName()); } first() { this.$query.limit(1) // if (this.$attributes && Object.keys(this.$attributes).length) { // this.where(this.$attributes) // } return this.get().then(res => { if (res.length) { return res[0] } return null }) } forceFill($attributes) { return this.constructor.unguarded(() => { return this.fill($attributes); }); } create(data = {}) { if (typeof data != 'object' || Array.isArray(data)) throw Error('Only json object allowed') this.fillable(data) this.addTimestampsToInsertValues(this.getAttributes()) return this.$query.insert(this.getAttributes()).then(res => { this.updateInserdtId([this.getAttributes()], res) return this }) } upsert(datas, $uniqueBy, $update = null) { throw Error('Under development') } async insert($values, $ids) { this.addTimestampsToInsertValues($values) let query = this.$query.insert($values) if ($ids) { query.returning($ids) } return query.then($ids => { this.updateInserdtId($values, $ids) return $values }) } createSelectWithConstraint($name) { return [$name.split(':')[0], function ($query) { $query.select($name.split(':')[1].split(',').map(function ($column) { if (String.contains($column, '.')) { return $column; } return $query instanceof BelongsToMany ? $query.getRelated().getTable() + '.' + $column : $column; })); }]; } parseWithRelations($relations) { let $results = {}; $relations = Array.isArray($relations) ? { ...$relations } : $relations for (let $name in $relations) { let $constraints = $relations[$name] if (is_numeric($name)) { $name = $constraints; [$name, $constraints] = String.contains($name, ':') ? this.createSelectWithConstraint($name) : [$name, function () { }]; } $results = this.addNestedWiths($name, $results); $results[$name] = $constraints; } return $results; } addNestedWiths($name, $results = {}) { let $progress = []; for (let $segment of $name.split('.')) { $progress.push($segment); let $last = $progress.join('.') if (!isset($results[$last])) { $results[$last] = function () { // }; } } return $results; } with($relations, $callback = null) { let $eagerLoad = null if (typeof $callback == 'function') { $eagerLoad = this.parseWithRelations({ [$relations]: $callback }); } else { $eagerLoad = this.parseWithRelations(is_string($relations) ? [...arguments] : $relations); } this[kEagerLoad] = Object.assign(this[kEagerLoad], $eagerLoad); return this; } defaultKeyName() { return this.getModel().getKeyName(); } getModel() { return this[kModel] || this; } setModel($model) { this[kModel] = $model; this.$query.from($model.getTable()); return this; } instanceValues($values) { return $values.map($value => { let $instance = this.newInstance() return $instance }) } newInstance(attributes, $exists = false, newInstance = false) { let $model = new (this.constructor)(attributes, newInstance) $model.$exists = $exists; $model.setConnection( this.getConnectionName() ); $model.setTable(this.getTable()); return $model } newModelInstance($attributes) { const $instance = this.newInstance(); $instance.fill($attributes); return $instance.setConnection( this.getConnection().getName() ); } newModelQuery() { return this.newEloquentBuilder( this.newBaseQueryBuilder() ).setModel(this); } newBaseQueryBuilder() { return Model.getConnectionResolver().table(this.$table) } newEloquentBuilder($query) { const $model = this.newInstance() $model.$query = $query return $model } hydrate($items) { let $instance = this.newInstance(); return $instance.newCollection($items.map(function ($item) { return $instance.newFromBuilder($item); })); } newCollection($data) { return Collection.collect($data || []) } async getModels() { return this.hydrate( await this.$query.get() ); } newFromBuilder($attributes = {}, $connection = null) { let $model = this.newInstance({}, true, false); $model.setRawAttributes($attributes, true); $model.setConnection($connection || this.getConnectionName()); return $model; } newQueryWithoutRelationships() { return this.newModelQuery(); } getQuery() { return this.$query.$query } qualifyColumn($column) { if (String.contains($column, '.')) { return $column; } return this.getTable() + '.' + $column; } isNestedUnder($relation, $name) { return String.contains($name, '.') && String.startsWith($name, $relation + '.'); } relationsNestedUnder($relation) { let $nested = {}; for (let $name in this[kEagerLoad]) { let $constraints = this[kEagerLoad][$name] if (this.isNestedUnder($relation, $name)) { $nested[$name.substr(($relation + '.').length)] = $constraints; } } return $nested; } getForeignKey() { return String.snakeCase(class_basename(this)) + '_' + this.getKeyName(); } getRelation($name) { let $relation = Relation.noConstraints(() => { let instance = this.getModel().newInstance() if (typeof instance[$name] != 'function') { throw RelationNotFoundException.make(this.getModel(), $name); } return instance[$name]() }); let $nested = this.relationsNestedUnder($name); if (count($nested) > 0) { $relation.getQuery().with($nested); } return $relation; } toJSON() { return this.serialize() } serialize() { return this.toJson() } toJson() { return this.attributesToJson(); } async eagerLoadRelation($models, $name, $constraints) { let $relation = this.getRelation($name); $relation.addEagerConstraints($models); $constraints($relation); $relation = await $relation.match( $relation.initRelation($models, $name), await $relation.getEager(), $name ); return $relation; } async eagerLoadRelations($models) { for (let $name in this[kEagerLoad]) { let $constraints = this[kEagerLoad][$name] if ($name.includes('.') === false) { $models = await this.eagerLoadRelation($models, $name, $constraints); } } return $models; } clone() { const $model = this.newModelInstance(); $model.$query.$query = this.getQuery().clone(); $model.forceFill(clone(this.$attributes)); return $model; } withAttributes(skipColumns = [], withTimestamp = false) { const $attributes = this.getAttributes(); if (typeof skipColumns == "boolean") { withTimestamp = skipColumns; } if (withTimestamp === false) { skipColumns.push(this.CREATED_AT); skipColumns.push(this.UPDATED_AT); } const filteredAttributes = Object.keys($attributes).reduce((result, key) => { if (!skipColumns.includes(key)) { result[key] = $attributes[key]; } return result; }, {}); this.where(filteredAttributes); return this } async get() { let $models = await this.getModels(); if ($models.length > 0) { $models = await this.eagerLoadRelations($models); } return $models } async save($options = {}) { let $saved = false let $query = this.newModelQuery(); if (this.$exists) { $saved = this.isDirty() ? await this.performUpdate($query) : true; } else { $saved = await this.performInsert($query); let $connection = $query.getConnection() if (!this.getConnectionName() && $connection) { this.setConnection($connection.getName()); } } if ($saved) { this.finishSave($options); await this[kPerformRelationQuery]() } return $saved; } finishSave($options = {}) { if (this.isDirty() && ($options['touch'] || true)) { this.touchOwners(); } this.syncOriginal(); } async performUpdate($query) { if (this.usesTimestamps()) { this.updateTimestamps(); } let $dirty = this.getDirty(); if (count($dirty) > 0) { await this.setKeysForSaveQuery($query).update($dirty); this.syncChanges(); } return true; } setKeysForSaveQuery($query) { const keys = this.getKeyName(); const obj = { }; if (Array.isArray(keys)) { for (let key of keys) { obj[key] = this.getKeyForSaveQuery(key) } } else { obj[keys] = this.getKeyForSaveQuery() } $query.where(obj); return $query; } getKeyForSaveQuery(key) { return this.getOriginal(key || this.getKeyName()) || this.getKey(); } getIncrementing() { return this.$incrementing; } async insertAndSetId($query, $attributes) { let $keyName = this.getKeyName(); const $id = await $query.insert([$attributes], $keyName); if (Array.isArray($keyName)) { for (let key of $keyName) { return this.setAttribute(key, $id[0][key]); } } else { return this.setAttribute($keyName, $id[0][$keyName]); } } async performInsert($query) { if (this.usesTimestamps()) { this.updateTimestamps(); } let $attributes = this.getAttributesForInsert(); if (this.getIncrementing()) { await this.insertAndSetId($query, $attributes); } else { if (empty($attributes)) { return true; } await $query.getQuery().insert($attributes); } this.$exists = true; this[kWasRecentlyCreated] = true; return true; } [kPerformRelationQuery]() { return Promise.all(this[kLazyQueries].map(fn => fn())) } setLazyQuery(fn) { if (Array.isArray(fn)) { return this[kLazyQueries] = this[kLazyQueries].concat(fn) } return this[kLazyQueries].push(fn) } whereKey($id) { if ($id instanceof Model) { $id = $id.getKey(); } if (is_array($id)) { if (in_array(this.getModel().getKeyType(), ['int', 'integer'])) { this.$query.whereIntegerInRaw(this.getModel().getQualifiedKeyName(), $id); } else { this.$query.whereIn(this.getModel().getQualifiedKeyName(), $id); } return this; } if ($id !== null && this.getModel().getKeyType() === 'string') { $id = $id.toString(); } return this.where(this.getModel().getQualifiedKeyName(), '=', $id); } whereKeyNot($id) { if ($id instanceof Model) { $id = $id.getKey(); } if (is_array($id)) { if (in_array(this.getModel().getKeyType(), ['int', 'integer'])) { this.$query.whereIntegerNotInRaw(this.getModel().getQualifiedKeyName(), $id); } else { this.$query.whereNotIn(this.getModel().getQualifiedKeyName(), $id); } return this; } if ($id !== null && this.getModel().getKeyType() === 'string') { $id = $id.toString(); } return this.where(this.getModel().getQualifiedKeyName(), '!=', $id); } destroy(id) { let where = { [this.$primaryKey]: id } this.where(where) return this.delete(); } async delete() { if (is_null(this.getKeyName())) { throw new Error('No primary key defined on model.'); } this.touchOwners(); await this.performDeleteOnModel(); return true; } async performDeleteOnModel() { if (Object.keys(this.getAttributes()).length) { if (this.$primaryKey in this.getAttributes()) { let where = { [this.$primaryKey]: this.getAttribute(this.$primaryKey) } this.where(where) return this.$query.delete(); } } else { await this.$query.delete() } this.$exists = false; } async firstOrNew($attributes = {}, $values = {}) { const $instance = await this.where($attributes).first() if (!is_null($instance)) { return $instance; } return this.newModelInstance(Object.assign($attributes, $values)); } async findOrFail($id = [], $columns = ['*']) { const $result = await this.find($id, $columns); if (is_array($id)) { if (!$result || count($result) !== count($id.unique())) { throw (new ModelNotFoundException).setModel( get_class(this.getModel()), $id.difference($result && $result.modelKeys()) ); } return $result; } if (is_null($result)) { throw (new ModelNotFoundException).setModel( get_class(this.getModel()), [$id] ); } return $result; } async findOrNew($id, $columns = ['*']) { const $model = await this.find($id, $columns); if (!is_null($model)) { return $model; } return this.newModelInstance(); } async findOr($id, $columns = ['*'], $callback = null) { if (typeof $columns == 'function') { $callback = $columns; $columns = ['*']; } const $model = await this.find($id, $columns) if (!is_null($model)) { return $model; } return $callback(); } async firstOrFail($columns = ['*']) { const $model = await this.first($columns); if (!is_null($model)) { return $model; } throw (new ModelNotFoundException).setModel(get_class(this.getModel())); } firstOrCreate(where, create = {}) { return this.where(where).first().then(res => { if (!res) { return this.create({ ...where, ...create }) } return res }) } firstOr($columns = ['*'], $callback = null) { if (typeof $columns == 'function') { $callback = $columns; $columns = ['*']; } const $model = this.first($columns) if (!is_null($model)) { return $model; } return $callback(); } updateOrCreate(where = {}, update = {}) { return this.where(where).first().then(async res => { if (res) { if (this.$timestamps) update[this.UPDATED_AT] = this.freshTimestampString() return res.where(where).update(update); } return this.create({ ...where, ...update }) }) } find($id) { if (!this.$primaryKey) { throw Error('No primary key defined on model'); } return this.where({ [this.$primaryKey]: $id }).first() } withSavepointIfNeeded($scope) { return $scope() } callScope($scope, $parameters = []) { $parameters.unshift(this); const $query = this.getQuery(); const wheres = $query._statements.filter(s => s.type == 'where'); const $originalWhereCount = is_null(wheres) ? 0 : count(wheres); const $result = $scope(...$parameters) || this; return $result; } withoutGlobalScopes($scopes = null) { if (!is_array($scopes)) { $scopes = Object.keys(this.scopes); } for (const $scope of $scopes) { this.withoutGlobalScope($scope); } return this; } withGlobalScope($identifier, $scope) { this.scopes[$identifier] = $scope; if (method_exists($scope, 'extend')) { $scope.extend(this); } return this; } withoutGlobalScope($scope) { if (!is_string($scope)) { $scope = get_class($scope); } delete this[kScopes][$scope]; this[kRemovedScopes].push($scope); return this; } removedScopes() { return this[kRemovedScopes]; } toBase() { return this.getQuery(); } __set(target, key, value) { if (!target.$exists) { throw Error('Instance is not alive.') } if (typeof key == 'symbol') { return target[key] = value } return target.setAttribute(key, value) } __get(target, key) { return target.getAttribute(key) } static __call(target, method, args) { target = (new target()) if (typeof target[method] == 'function') { return target[method](...args) } throw new MethodNotAvailable('Method [' + method + '] was not available on [' + target.constructor.name + ']') } } module.exports = Macroable(Model)