UNPKG

@themost/data

Version:

MOST Web Framework Codename Blueshift - Data module

538 lines (516 loc) 21.2 kB
// MOST Web Framework 2.0 Codename Blueshift BSD-3-Clause license Copyright (c) 2017-2022, THEMOST LP All rights reserved /*eslint no-var: "off"*/ // noinspection ES6ConvertVarToLetConst var {LangUtils, DataError} = require('@themost/common'); var async = require('async'); var {QueryField} = require('@themost/query'); var {DataAssociationMapping} = require('./types'); var {DataConfigurationStrategy} = require('./data-configuration'); var {DataQueryable} = require('./data-queryable'); var {DataObjectJunction} = require('./data-object-junction'); var {hasOwnProperty} = require('./has-own-property'); const {isObjectDeep} = require('./is-object'); /** * @class * @constructor * @augments DataQueryable * @param {DataObject} obj The parent data object reference * @param {string|*} association - A string that represents the name of the field which holds association mapping or the association mapping itself. * @property {DataModel} baseModel - The model associated with this data object junction * @property {DataObject} parent - Gets or sets the parent data object associated with this instance of DataObjectJunction class. * @property {DataAssociationMapping} mapping - Gets or sets the mapping definition of this data object association. */ function HasParentJunction(obj, association) { var self = this; /** * @type {DataObject} * @private */ var _parent = obj; /** * @type {DataModel} */ var _model; var DataModel = require('./data-model').DataModel; /** * Gets or sets the parent data object * @type DataObject */ Object.defineProperty(this, 'parent', { get: function () { return _parent; }, set: function (value) { _parent = value; }, configurable: false, enumerable: false}); //get association mapping if (typeof association === 'string') { if (_parent) { _model = _parent.getModel(); if (_model!==null) self.mapping = _model.inferMapping(association); } } else if (typeof association === 'object' && association !== null) { //get the specified mapping if (association instanceof DataAssociationMapping) self.mapping = association; else self.mapping = Object.assign(new DataAssociationMapping(), association); } var relatedModel = this.parent.context.model(self.mapping.parentModel); //call super class constructor HasParentJunction.super_.call(this, relatedModel); //modify query (add join model) var adapter = relatedModel.viewAdapter; var left = {}, right = {}; this.query.select(relatedModel.attributes.filter(function(x) { return !x.many; }).map(function(x) { return QueryField.select(x.name).from(adapter); })); var baseModel; Object.defineProperty(this, 'baseModel', { get: function() { if (baseModel) return baseModel; /** * @type {*|DataConfigurationStrategy} */ var conf = self.parent.context.getConfiguration().getStrategy(DataConfigurationStrategy); //search in cache (configuration.current.cache) if (conf.getModelDefinition(self.mapping.associationAdapter)) { baseModel = new DataModel(conf.getModelDefinition(self.mapping.associationAdapter)); baseModel.context = self.parent.context; return baseModel; } // otherwise, create model var parentModel = self.parent.getModel(); var childModel = self.parent.context.model(self.mapping.childModel); var adapter = self.mapping.associationAdapter; baseModel = self.parent.context.model(adapter); if (baseModel == null) { var associationObjectField = self.mapping.associationObjectField || DataObjectJunction.DEFAULT_OBJECT_FIELD ; var associationValueField = self.mapping.associationValueField || DataObjectJunction.DEFAULT_VALUE_FIELD; var modelDefinition = { name:adapter, title: adapter, sealed:false, hidden:true, type:'hidden', source:adapter, view:adapter, version:'1.0', fields:[ { name: 'id', type:'Counter', primary: true }, { name: associationObjectField, indexed: true, nullable:false, type: self.mapping.parentModel, mapping: { associationType: 'association', parentModel: self.mapping.parentModel, parentField: self.mapping.parentField, childModel: adapter, childField: associationObjectField, } }, { name: associationValueField, indexed: true, nullable:false, type: self.mapping.childModel, mapping: { associationType: 'association', parentModel: self.mapping.childModel, parentField: self.mapping.childField, childModel: adapter, childField: associationValueField, } } ], constraints: [ { description: 'The relation between two objects must be unique.', type:'unique', fields: [ associationObjectField, associationValueField ] } ], 'privileges': self.mapping.privileges || [ { 'mask':15, 'type':'global' }, { 'mask':15, 'type':'global', 'account': 'Administrators' } ]}; // add unique constraint if child model mapping is Zero Or One var attribute = parentModel.attributes.filter(function (x) { return x.type === childModel.name; }).filter(function (y) { var mapping = parentModel.inferMapping(y.name); return mapping && mapping.associationAdapter === adapter; }); if (attribute) { if (attribute && (attribute.multiplicity === 'ZeroOrOne' || attribute.multiplicity === 'One')) { modelDefinition.constraints[0].fields = [associationObjectField]; } } conf.setModelDefinition(modelDefinition); //initialize base model baseModel = new DataModel(modelDefinition); baseModel.context = self.parent.context; } return baseModel; },configurable:false, enumerable:false }); /** * @method * @description Gets an instance of DataModel class which represents the data adapter of this association * @name HasParentJunction#getBaseModel * @returns {DataModel} */ /** * Gets an instance of DataModel class which represents the data adapter of this association * @returns {DataModel} */ this.getBaseModel = function() { return this.baseModel; }; // get association adapter var associationAdapter = self.mapping.associationAdapter; // get parent field var parentField = QueryField.select(this.getObjectField()).from(associationAdapter).$name; // get child field var childField = QueryField.select(this.getValueField()).from(associationAdapter).$name; // set left operand of join expression left[adapter] = [ this.mapping.parentField ]; // set right operand of join expression right[associationAdapter] = [parentField]; // apply native join expression to query this.query.join(this.mapping.associationAdapter, []).with([left, right]).where(childField).equal(obj[this.mapping.childField]).prepare(); } LangUtils.inherits(HasParentJunction, DataQueryable); /** * @returns {string} */ HasParentJunction.prototype.getObjectField = function() { return DataObjectJunction.prototype.getObjectField.bind(this)(); }; /** * @returns {string} */ HasParentJunction.prototype.getValueField = function() { return DataObjectJunction.prototype.getValueField.bind(this)(); }; /** * @this HasParentJunction * Inserts a new association between a parent and a child object. * @param {*} obj An object or an identifier that represents the child object * @param {Function} callback * @private */ function insertSingleObject(obj, callback) { var self = this; //get parent and child var parent = obj; if (typeof obj !== 'object') { parent = {}; parent[self.mapping.parentField] = obj; } var parentValue = parent[self.mapping.parentField]; var childValue = self.parent[self.mapping.childField]; //validate relation existence self.baseModel.silent(self.$silent).where(self.getObjectField()).equal(parentValue).and(self.getValueField()).equal(childValue).first(function(err, result) { if (err) { //on error exit with error return callback(err); } else { if (result) { //if relation already exists, do nothing return callback(); } else { //otherwise, create new item var newItem = { }; newItem[self.getObjectField()] = parentValue; newItem[self.getValueField()] = childValue; //and insert it self.baseModel.silent(self.$silent).insert(newItem, callback); } } }); } /** * @this HasParentJunction * @param {*} obj * @param {Function} callback * @private */ function insertAnyObject(obj, callback) { var self = this; var arr = Array.isArray(obj) ? obj : [ obj ]; self.migrate(function(err) { if (err) callback(err); else { async.eachSeries(arr, function(item, cb) { var parent; // try to find if object is a single value e.g. a number if (isObjectDeep(item) === false) { parent = {}; // and set object key e.g. { "id": 1102 } Object.defineProperty(parent, self.mapping.parentField, { configurable: true, enumerable: true, value: item }); } else { // otherwise get object parent = item; } //validate if child identifier exists if (hasOwnProperty(parent, self.mapping.parentField)) { return insertSingleObject.call(self, parent, function(err) { cb(err); }); } else { // get related model var relatedModel = self.parent.context.model(self.mapping.parentModel); // ensure silent mode var isSilent = !!self.$silent; // find object by querying child object return relatedModel.find(item).select(self.mapping.parentField).silent(isSilent).value().then(function(result) { if (result == null) { return cb(new DataError('E_ASSOCIATION', 'An associated object cannot be found', null, self.mapping.parentModel, self.mapping.parentField)); } // set value Object.defineProperty(parent, self.mapping.parentField, { configurable: true, enumerable: true, value: result }); return insertSingleObject.call(self, parent, function(err) { cb(err); }); }).catch(function (err) { return cb(err); }); } }, callback); } }); } /** * Inserts an association between parent object and the given object or array of objects. * @param {*|Array} obj - An object or an array of objects to be related with parent object * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object. */ HasParentJunction.prototype.insert = function(obj, callback) { var self = this; if (typeof callback === 'undefined') { return new Promise(function(resolve, reject) { return insertAnyObject.bind(self)(obj, function(err) { if (err) { return reject(err); } return resolve(); }); }); } return insertAnyObject.bind(self)(obj, function(err) { return callback(err); }); }; /** * @this HasParentJunction * Removes a relation between a parent and a child object. * @param {*} obj An object or an identifier that represents the child object * @param {Function} callback * @private */ function removeSingleObject(obj, callback) { var self = this; //get parent and child var parent = obj; if (typeof obj !== 'object') { parent = {}; parent[self.mapping.parentField] = obj; } var parentValue = parent[self.mapping.parentField]; if (parentValue == null) { return callback(new DataError('E_PAREN_NULL', 'Parent object identifier cannot be empty', null, self.mapping.parentModel, self.mapping.parentField)); } var childValue = self.parent[self.mapping.childField]; if (childValue == null) { return callback(new DataError('E_CHILD_NULL', 'Child object identifier cannot be empty', null, self.mapping.childModel, self.mapping.childField)); } // get silent mode var isSilent = !!self.$silent; //get relation model self.baseModel.silent(isSilent).where(this.getObjectField()).equal(parentValue) .and(this.getValueField()).equal(childValue).getItem().then(function(result) { if (result == null) { return callback(new DataError('E_NOT_ASSOC', 'The association cannot be found or access is denied', null, self.baseModel.name)); } // otherwise, remove item return self.baseModel.silent(isSilent).remove(result, callback); }).catch(function(err) { return callback(err); }); } /** * @this HasParentJunction * @param obj * @param callback * @private */ function removeAnyObject(obj, callback) { var self = this; var arr = Array.isArray(obj) ? obj : [ obj ]; self.migrate(function(err) { if (err) callback(err); else { async.eachSeries(arr, function(item, cb) { var parent; // try to find if object is a single value e.g. a number if (isObjectDeep(item) === false) { parent = {}; // and set object key e.g. { "id": 1102 } Object.defineProperty(parent, self.mapping.parentField, { configurable: true, enumerable: true, value: item }); } else { // otherwise get object parent = item; } if (hasOwnProperty(parent, self.mapping.parentField)) { return removeSingleObject.call(self, parent, function (err) { return cb(err); }); } // get related model var relatedModel = self.parent.context.model(self.mapping.parentModel); // get silent mode var isSilent = !!self.$silent; // find object relatedModel.find(parent).silent(isSilent).select(self.mapping.parentField).value().then(function(result) { if (result == null) { return cb(new DataError('E_ASSOCIATION', 'An associated object cannot be found', null, self.mapping.parentModel, self.mapping.parentField)); } // set value Object.defineProperty(parent, self.mapping.parentField, { configurable: true, enumerable: true, value: result }); return removeSingleObject.call(self, parent, function(err) { return cb(err); }); }).catch(function (err) { return cb(err); }); }, callback); } }); } /** * Removes the association between parent object and the given object or array of objects. * @param {*|Array} obj - An object or an array of objects to be disconnected from parent object * @param {Function=} callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @returns {Promise<T>|*} - If callback parameter is missing then returns a Promise object. */ HasParentJunction.prototype.remove = function(obj, callback) { var self = this; if (typeof callback === 'undefined') { return new Promise(function(resolve, reject) { return removeAnyObject.bind(self)(obj, function(err) { if (err) { return reject(err); } return resolve(); }); }); } return removeAnyObject.bind(self)(obj, function(err) { return callback(err); }); }; HasParentJunction.prototype.migrate = function(callback) { this.baseModel.migrate(callback); }; /** * @return Promise<void> */ HasParentJunction.prototype.migrateAsync = function () { var self = this; return new Promise(function(resolve, reject) { self.migrate(function (err) { if (err) { return reject(err); } return resolve(); }); }); } /** * Overrides DataQueryable.count() method * @param callback - A callback function where the first argument will contain the Error object if an error occurred, or null otherwise. * @ignore */ HasParentJunction.prototype.count = function(callback) { var self = this; if (typeof callback === 'undefined') { return new Promise(function(resolve, reject) { return self.migrate(function(err) { if (err) { return reject(err); } // noinspection JSPotentiallyInvalidConstructorUsage var superCount = HasParentJunction.super_.prototype.count.bind(self); return superCount().then(function(result) { return resolve(result); }).catch(function(err) { return reject(err); }); }); }); } return self.migrate(function(err) { if (err) { return callback(err); } // noinspection JSPotentiallyInvalidConstructorUsage var superCount = HasParentJunction.super_.prototype.count.bind(self); return superCount(callback); }); }; /** * @returns {Promise<T>|*} */ HasParentJunction.prototype.removeAll = function () { var self = this; return self.migrateAsync().then(function () { // get child id var childValue = self.parent[self.mapping.childField]; if (childValue == null) { throw new DataError('E_ASSOCIATION', 'Associated attribute value cannot be empty at this context', null, self.mapping.childModel, self.mapping.childField); } // get relation model var baseModel = self.getBaseModel(); // validate relation existence return baseModel.where(self.getValueField()).equal(childValue).getAllItems().then(function (result) { // if there are no items if (result.length === 0) { return Promise.resolve(); } // otherwise remove items return baseModel.remove(result); }); }); }; module.exports = { HasParentJunction };