UNPKG

@themost/data

Version:

MOST Web Framework Codename Blueshift - Data module

640 lines (617 loc) 23.5 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 _ = require('lodash'); var {QueryUtils} = require('@themost/query'); var async = require('async'); var {DataError} = require('@themost/common'); var {hasOwnProperty} = require('./has-own-property'); var {isObjectDeep} = require('./is-object'); /** * * @param attr * @param event * @param callback * @returns {*} * @private */ function beforeSave_(attr, event, callback) { var context = event.model.context; var name = attr.property || attr.name; var key = event.model.getPrimaryKey(); var nestedObj = event.target[name]; // if attribute is null or undefined do nothing if (nestedObj == null) { // do nothing return callback(); } if (isObjectDeep(nestedObj) === false) { // validate object state based on the given value // on insert if (event.state === 1) { // throw error return callback(new DataError('E_NESTED', 'A nested object cannot be forcibly set during insert.', null, event.model.name, name)); } // on update if (event.state === 2) { return event.model.where(key).equal(event.target[key]).select(name).flatten().silent().value().then(function(value) { // if the given value is different from the one that has been already set let compareValue = nestedObj; if (typeof value === 'number' && typeof nestedObj !== 'number') { compareValue = Number(nestedObj); } if (value !== compareValue) { // throw error return callback(new DataError('E_NESTED', 'A nested object cannot be forcibly updated.', null, event.model.name, name)); } return callback(); }).catch(function(err) { return callback(err); }); } } //get target model var nestedModel = context.model(attr.type); if (_.isNil(nestedModel)) { return callback(); } if (event.state===1) { //save nested object nestedModel.silent().save(nestedObj, function(err) { callback(err); }); } else if (event.state === 2) { //first of all get original address from db event.model.where(key) .equal(event.target[key]) .select(key, name) .expand(name) .silent() .first().then(function( result) { if (_.isNil(result)) { return callback(new Error('Invalid object state.')); } var nestedKey = nestedModel.getPrimaryKey(); if (_.isNil(result[name])) { //first of all delete nested object id (for insert) delete nestedObj[nestedKey]; //save data nestedModel.silent().save(nestedObj).then(function() { return callback(); }).catch(function(err) { return callback(err); }); } else { //set nested object id (for update) nestedObj[nestedKey] = result[name][nestedKey]; nestedModel.silent().save(nestedObj).then(function() { return callback(); }).catch(function(err) { return callback(err); }); } }).catch(function(err) { return callback(err); }); } else { return callback(); } } /** * * @param attr * @param event * @param callback * @returns {*} * @private */ // eslint-disable-next-line no-unused-vars function beforeSaveMany_(attr, event, callback) { var context = event.model.context; var name = attr.property || attr.name; var key = event.model.getPrimaryKey(); var nestedObj = event.target[name]; //if attribute is null or undefined if (_.isNil(nestedObj)) { //do nothing return callback(); } //if nested object is not an array if (!_.isArray(nestedObj)) { //throw exception return callback(new DataError('EJUNCT','Invalid argument type. Expected array.',null, event.model.name, name)); } //if nested array does not have any data if (nestedObj.length===0) { //do nothing return callback(); } //get target model var nestedModel = context.model(attr.type); //if target model cannot be found if (_.isNil(nestedModel)) { return callback(); } //get nested primary key var nestedKey = nestedModel.getPrimaryKey(); //on insert if (event.state===1) { //enumerate nested objects and set state to new _.forEach(nestedObj, function(x) { //delete identifier delete x[nestedKey]; //force state to new ($state=1) x.$state = 1; }); //save nested objects nestedModel.silent().save(nestedObj, function(err) { //remove $state attribute nestedObj.forEach(function(x) { delete x.$state; }); //and return callback(err); }); } //on update else if (event.state === 2) { //first of all get original associated object, if any event.model.where(key) .equal(event.target[key]) .select(key,name) .expand(name) .silent() .first(function(err, result) { if (err) { return callback(err); } //if original object cannot be found, throw an invalid state exception if (_.isNil(result)) { return callback(new Error('Invalid object state.')); } //get original nested objects var originalNestedObjects = result[name] || []; //enumerate nested objects _.forEach(nestedObj, function(x) { var obj = _.find(originalNestedObjects, function(y) { return y[nestedKey] === x[nestedKey]; }); if (obj) { //force state to update ($state=2) x.$state = 2; } else { //delete identifier delete x[nestedKey]; //force state to new ($state=1) x.$state = 1; } }); _.forEach(originalNestedObjects, function(x) { var obj = _.find(nestedObj, function(y) { return y[nestedKey] === x[nestedKey]; }); if (_.isNil(obj)) { //force state to delete ($state=4) x.$state = 4; nestedObj.push(x); } }); //and finally save objects nestedModel.silent().save(nestedObj, function(err) { //remove $state attribute _.forEach(nestedObj, function(x) { delete x.$state; }); if (err) { return callback(err); } return callback(); }); }); } else { return callback(); } } /** * @module @themost/data/data-nested-object-listener * @ignore */ /** * @class * @constructor */ function DataNestedObjectListener() { } /** * @param {DataEventArgs} event * @param {Function} callback */ DataNestedObjectListener.prototype.beforeSave = function (event, callback) { try { //get attributes with nested property set to on var nested = event.model.attributes.filter(function(x) { //only if these attributes belong to current model return x.nested && (x.model === event.model.name); }); //if there are no attribute defined as nested do nothing if (nested.length === 0) { return callback(); } async.eachSeries(nested, function(attr, cb) { if (attr.many===true) { return cb(); } return beforeSave_(attr, event, cb); }, function(err) { return callback(err); }); } catch (err) { return callback(err); } }; function beforeRemove_(attr, event, callback) { try { if (event.state !== 4) { return callback(); } var context = event.model.context, name = attr.property || attr.name, key = event.model.getPrimaryKey(); /** * @type {DataModel} */ var nestedModel = context.model(attr.type); if (_.isNil(nestedModel)) { return callback(); } event.model.where(key).equal(event.target[key]).select(key,name).flatten().silent().first(function(err, result) { if (err) { return callback(err); } if (_.isNil(result)) { return callback(); } if (_.isNil(result[name])) { return callback(); } //set silent mode (if parent model is in silent mode) if (event.model.isSilent()) { nestedModel.silent(); } var nestedKey = result[name]; //Update target object (remove the association between target object and nested object). //This operation must be done before trying to remove nested object otherwise the operation will fail with foreign key reference error var updated = {}; updated[name] = null; var q = QueryUtils.update(event.model.sourceAdapter).set(updated).where(event.model.primaryKey).equal(result[event.model.primaryKey]); return context.db.execute(q, null, function(err) { if (err) { return callback(err); } nestedModel.silent().remove({id:nestedKey}).then(function() { return callback(); }).catch(function(err) { return callback(err); }); }); }); } catch (err) { callback(err) } } // eslint-disable-next-line no-unused-vars function beforeRemoveMany_(attr, event, callback) { try { if (event.state !== 4) { return callback(); } var context = event.model.context, name = attr.property || attr.name; var nestedModel = context.model(attr.type); if (_.isNil(nestedModel)) { return callback(); } //get junction var junction = event.target.property(name); //select object identifiers (get all objects in silent mode to avoid orphaned objects) junction.select(nestedModel.getPrimaryKey()).silent().all().then(function(result) { //first of all remove all associations junction.clear(function(err) { if (err) { return callback(err); } //and afterwards remove nested objects nestedModel.silent().remove(result, function(err) { if (err) { return callback(); } }); }); }).catch(function(err) { return callback(err); }); } catch (err) { callback(err) } } DataNestedObjectListener.prototype.beforeRemove = function (event, callback) { try { //get attributes with nested property set to on var nested = event.model.attributes.filter(function(x) { //only if these attributes belong to current model return x.nested && (x.model === event.model.name); }); //if there are no attribute defined as nested, do nothing and exit if (nested.length === 0) { return callback(); } async.eachSeries(nested, function(attr, cb) { return beforeRemove_(attr, event, cb); }, function(err) { return callback(err); }); } catch (err) { return callback(err); } }; /** * Handles after save event for one-to-one associations where the parent model is the current model. * This operation uses interactive user (or in-process) privileges for insert, update, delete * @param {DataField} attr * @param {DataEventArgs} event * @param {Function} callback * @returns {*} * @private */ function afterSave_(attr, event, callback) { // get context var context = event.model.context; // get attribute var name = attr.property || attr.name; // if target object does not have a property with the specified name if (hasOwnProperty(event.target, name) === false) { // return return callback(); } // get nested object var nestedObject = event.target[name]; //if attribute is null or undefined and state is insert if (nestedObject == null && event.state === 1) { //do nothing return callback(); } /** * get nested model * @type {DataModel} */ var nestedModel = context.model(attr.type); //if target model cannot be found if (_.isNil(nestedModel)) { // do nothing return callback(); } // get mapping var mapping = event.model.inferMapping(attr.name); if (_.isNil(mapping)) { // throw error return callback(new DataError('EASSOCIATION','Association mapping may not be empty.', null, event.model.name, attr.name)); } // check if mapping parent model is the same with event target model if (mapping.parentModel !== event.model.name) { // do nothing return callback(); } // validate nested object if (_.isArray(nestedObject)) { // throw error for invalid nested object type return callback(new DataError('EASSOCIATION', 'Expected object.', null, event.model.name, name)); } // get in-process silent mode var silent = event.model.isSilent(); // get nested primary key var nestedKey = nestedModel.getPrimaryKey(); if (nestedObject) { // safe delete identifier delete nestedObject[nestedKey]; // set associated value nestedObject[mapping.childField] = event.target[mapping.parentField]; } if (event.state === 1) { // save nested object (with interactive user privileges) return nestedModel.silent(silent).save(nestedObject).then(function() { // and return return callback(); }).catch(function(err) { return callback(err); }); } else if (event.state === 2) { if (nestedObject == null) { // try to find nested object return nestedModel.where(mapping.childField).equal(event.target[mapping.parentField]) .silent().getItem().then(function (originalObject) { if (originalObject) { // try to remove (with interactive user privileges) return nestedModel.silent(silent).remove(originalObject).then(function() { // and return return callback(); }); } // else do nothing return callback(); }).catch(function(err) { return callback(err); }); } else { // update nested object (with interactive user privileges) return nestedModel.silent(silent).save(nestedObject).then(function() { // and return return callback(); }).catch(function(err) { return callback(err); }); } } // otherwise do nothing return callback(); } function afterSaveMany_(attr, event, callback) { var context = event.model.context; var name = attr.property || attr.name; var key = event.model.getPrimaryKey(); var nestedArr = event.target[name]; //if attribute is null or undefined if (_.isNil(nestedArr)) { //do nothing return callback(); } //if nested object is not an array if (!_.isArray(nestedArr)) { //throw exception return callback(new DataError('EASSOCIATION','Invalid argument type. Expected array.',null, event.model.name, name)); } //get mapping var mapping = event.model.inferMapping(attr.name); if (_.isNil(mapping)) { return callback(new DataError('EASSOCIATION','Association mapping may not be empty.', null, event.model.name, attr.name)); } if (mapping.associationType === 'junction') { return callback(new DataError('EASSOCIATION','Junction nested association type is not supported.', null, event.model.name, attr.name)); } if (mapping.associationType === 'association' && mapping.parentModel !== event.model.name) { return callback(new DataError('EASSOCIATION','Invalid nested association type.', null, event.model.name, attr.name)); } //get target model var nestedModel = context.model(attr.type); //if target model cannot be found if (_.isNil(nestedModel)) { return callback(); } // validate parent object association key if (mapping.parentField && mapping.parentField !== key) { // validate that parentField is unique constraint of parent model var constraint = _.find(event.model.constraintCollection, function(constraint) { return constraint.type === 'unique' && constraint.fields && constraint.fields.length === 1 && constraint.fields[0] === mapping.parentField; }); if (constraint == null) { return callback(new DataError('EASSOCIATION', 'Nested association uses a foreign key that is not a primary key or a unique constraint field.')); } } //get nested primary key var nestedKey = nestedModel.getPrimaryKey(); //on insert if (event.state===1) { //enumerate nested objects and set state to new _.forEach(nestedArr, function(x) { //delete identifier delete x[nestedKey]; //force state to new ($state=1) x.$state = 1; //set parent field for mapping x[mapping.childField] = event.target[mapping.parentField]; }); //save nested objects nestedModel.silent().save(nestedArr, function(err) { //remove $state attribute nestedArr.forEach(function(x) { delete x.$state; }); //and return callback(err); }); } //on update else if (event.state === 2) { //first of all get original associated object, if any event.model.where(key) .equal(event.target[key]) .select(mapping.parentField, name) .expand(name) .silent() .first(function(err, result) { if (err) { return callback(err); } //if original object cannot be found, throw an invalid state exception if (result == null) { return callback(new Error('Invalid object state.')); } //get original nested objects var originalNestedArr = result[name] || []; //enumerate nested objects _.forEach(nestedArr, function(x) { var obj = _.find(originalNestedArr, function (y) { return y[nestedKey] === x[nestedKey]; }); if (obj) { // if object is marked for deletion set delete state // otherwise set update state x.$state = (x.$state === 4) ? 4 : 2; } else { // delete identifier delete x[nestedKey]; // force set insert state ($state=1) x.$state = 1; } x[mapping.childField] = event.target[mapping.parentField]; }); // automatically remove other nested items // todo::this operation is going to be deprecated _.forEach(originalNestedArr, function(x) { var obj = _.find(nestedArr, function(y) { return y[nestedKey] === x[nestedKey]; }); if (_.isNil(obj)) { // force state to delete ($state=4) x.$state = 4; nestedArr.push(x); } }); // and finally save objects nestedModel.silent().save(nestedArr, function(err) { //remove $state attribute _.remove(nestedArr, function(y) { return y.$state === 4; }); _.forEach(nestedArr, function(x) { delete x.$state; }); if (err) { return callback(err); } return callback(); }); }); } else { return callback(); } } /** * @param {DataEventArgs} event * @param {Function} callback */ DataNestedObjectListener.prototype.afterSave = function (event, callback) { try { //get attributes with nested property set to on var nested = event.model.attributes.filter(function(x) { //only if these attributes belong to current model return x.nested && (x.model === event.model.name); }); //if there are no attribute defined as nested do nothing if (nested.length === 0) { return callback(); } async.eachSeries(nested, function(attr, cb) { // get mapping var mapping = event.model.inferMapping(attr.name); if (mapping && mapping.parentModel === event.model.name) { // check constraints var childModel = event.model.context.model(mapping.childModel); // if child model was found if (childModel && // has constraints childModel.constraints && // constraints is not empty childModel.constraints.length && // and there is a constraint that has one key and this key is the mapping child field childModel.constraints.find(function(constraint) { return constraint.fields && constraint.fields.length === 1 && constraint.fields.indexOf(mapping.childField) === 0; })) { // try to save one-to-one nested association where parent model is the current model return afterSave_(attr, event, cb); } } if (attr.many===true) { return afterSaveMany_(attr, event, cb); } return cb(); }, function(err) { return callback(err); }); } catch (err) { return callback(err); } }; module.exports = { DataNestedObjectListener }