@themost/data
Version:
MOST Web Framework Codename Blueshift - Data module
640 lines (617 loc) • 23.5 kB
JavaScript
// 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
}