@themost/data
Version:
MOST Web Framework Codename Blueshift - Data module
344 lines (334 loc) • 18.3 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 {eachSeries} = require('async');
var {TypeParser, DataObjectState} = require('./types');
var {DataError} = require('@themost/common');
var {HasParentJunction} = require('./has-parent-junction');
var {DataObjectJunction} = require('./data-object-junction');
var {DataObjectTag} = require('./data-object-tag');
var parseBoolean = TypeParser.parseBoolean;
var {hasOwnProperty} = require('./has-own-property');
var {isObjectDeep} = require('./is-object');
class DataObjectAssociationError extends DataError {
constructor(model, field) {
super('E_ASSOCIATION', 'An associated object cannot be found or is inaccessible.',null, model, field);
}
}
class DataObjectMultiAssociationError extends DataError {
constructor(model, field) {
super('E_MULTI_ASSOCIATION', 'An associated object is defined more than once and cannot be bound.',null, model, field);
}
}
class DataObjectAssociationListener {
/**
* @param {DataEventArgs} event
* @param {function(Error=)} callback
*/
beforeSave(event, callback) {
try {
if (event.target == null) {
return callback();
}
var keys = Object.keys(event.target);
var mappings = [];
keys.forEach(function(key) {
if (Object.prototype.hasOwnProperty.call(event.target, key) && event.target[key] != null) {
//try to find field mapping, if any
var mapping = event.model.inferMapping(key);
if (mapping && mapping.associationType === 'association' &&
mapping.childModel===event.model.name) {
// on update
if (event.state === DataObjectState.Update) {
var field = event.model.getAttribute(key);
// exclude non-editable attributes
if (field) {
var editable = Object.prototype.hasOwnProperty.call(field,'editable') ? field.editable : true;
if (editable) {
mappings.push(mapping);
}
}
} else {
mappings.push(mapping);
}
}
}
});
return eachSeries(mappings,
/**
* @param {DataAssociationMapping} mapping
* @param {function(Error=)} cb
*/
function(mapping, cb) {
if (mapping.associationType==='association' && mapping.childModel === event.model.name) {
/**
* @type {DataField|*}
*/
var field = event.model.field(mapping.childField);
var childField = field.property || field.name;
//change:21-Mar 2016
//description: check if association belongs to this model, or it's inherited from any base model
//if current association belongs to base model
if ((event.model.name !== field.model) && (!parseBoolean(field.cloned))) {
//do nothing and exit
return cb();
}
var silentMode = event.model.isSilent();
//get associated model
var associatedModel = event.model.context.model(mapping.parentModel);
var associatedObject;
var value = event.target[childField];
// if value is a plain object e.g. { id: 100, name: '' }
if (isObjectDeep(value)) {
// if plain object has a property equal to mapping parent field
if (hasOwnProperty(value, mapping.parentField)) {
// get property value
var propertyValue = value[mapping.parentField];
// if property value is null
if (propertyValue == null) {
// set associated object to null
event.target[childField] = null;
// and exit
return cb();
}
// otherwise, create a new object which contains only that value
// e.g. { id: 400 }
associatedObject = {};
Object.defineProperty(associatedObject, mapping.parentField, {
configurable: true,
enumerable: true,
writable: true,
value: propertyValue
});
} else {
// use this value
associatedObject = event.target[childField];
}
} else {
// else create an empty object
associatedObject = {};
// and define parent field e.g. { identifier: '00000-11111-22222-FFFFF-00000' }
Object.defineProperty(associatedObject, mapping.parentField, {
configurable: true,
enumerable: true,
writable: true,
value: event.target[childField]
});
}
// try to find associated object
return associatedModel.find(associatedObject).select(mapping.parentField)
.flatten().silent(silentMode).take(1).getList().then(function(result) {
if (result == null) {
return cb(new DataObjectAssociationError(mapping.childModel, mapping.childField));
}
else if (result.total === 0) {
/**
* this operation will try to validate if the given value represents an empty object
* e.g. an object with id=0 where `id` is an auto increment identifier
* the operation didn't find any object, we can set null value
* otherwise, throw an exception for missing associated object
*/
if (isObjectDeep(value) === false) {
/**
* @type {import('./types').DataField}
*/
var parentField = associatedModel.getAttribute(mapping.parentField);
// try to find parse
/**
* @type {function(*)}
*/
var parser = TypeParser.hasParser(parentField.type);
if (typeof parser === 'function') {
// try to parse value
var converted = parser(null);
// if the given value is equal that returned by parsing null
if (converted === value) {
// set null value
event.target[childField] = null;
// and exit
return cb()
}
}
}
return cb(new DataObjectAssociationError(mapping.childModel, mapping.childField));
}
else if (result.total > 1) {
return cb(new DataObjectMultiAssociationError(mapping.childModel, mapping.childField));
}
else {
if ( typeof associatedObject === 'object' && Object.prototype.hasOwnProperty.call(associatedObject, mapping.parentField) === false) {
// set foreign key
Object.defineProperty(associatedObject, mapping.parentField, {
configurable: true,
enumerable: true,
writable: true,
value: result.value[0][mapping.parentField]
});
}
return cb();
}
}).catch(function(err) {
return cb(err);
});
}
else {
return cb();
}
}, function(err) {
return callback(err);
});
}
catch (err) {
return callback(err);
}
}
/**
* @param {DataEventArgs} event
* @param {function(Error=)} callback
*/
afterSave(event, callback) {
try {
if (event.target == null) {
return callback();
}
else {
var keys = Object.keys(event.target);
var mappings = [];
keys.forEach(function(key) {
if (Object.prototype.hasOwnProperty.call(event.target, key) && event.target[key] != null) {
/**
* @type DataAssociationMapping
*/
var mapping = event.model.inferMapping(key);
if (mapping != null) {
var attribute = event.model.getAttribute(key);
// get only many-to-many associations
if (mapping.associationType==='junction' && attribute.multiplicity === 'Many') {
mappings.push({ name:key, mapping:mapping });
}
}
}
});
if (mappings.length === 0) {
return callback();
}
eachSeries(mappings,
/**
* @param {{name:string,mapping:DataAssociationMapping}} x
* @param {function(Error=)} cb
*/
function(x, cb) {
var silentMode = event.model.isSilent();
if (x.mapping.associationType==='junction') {
var obj = event.model.convert(event.target);
/**
* @type {*|{deleted:Array}}
*/
var childs = obj[x.name]
var junction;
if (!Array.isArray(childs)) {
return cb();
}
if (x.mapping.childModel===event.model.name) {
junction = new HasParentJunction(obj, x.mapping);
if (event.state===1 || event.state===2) {
var toBeRemoved = [], toBeInserted = [];
childs.forEach(function(x) {
if (x.$state === 4) {
toBeRemoved.push(x);
}
else {
toBeInserted.push(x);
}
});
junction.silent(silentMode).insert(toBeInserted, function(err) {
if (err) { return cb(err); }
junction.silent(silentMode).remove(toBeRemoved, function(err) {
if (err) { return cb(err); }
return cb();
});
});
}
else {
return cb();
}
} else if (x.mapping.parentModel===event.model.name) {
if (event.state===1 || event.state===2) {
if (typeof x.mapping.childModel === 'undefined') {
/**
* @type {DataObjectTag}
*/
var tags = new DataObjectTag(obj, x.mapping);
return tags.silent(silentMode).all().then(function(result) {
var toBeRemoved = result.filter(function(x) { return childs.indexOf(x)<0; });
var toBeInserted = childs.filter(function(x) { return result.indexOf(x)<0; });
if (toBeRemoved.length>0) {
return tags.silent(silentMode).remove(toBeRemoved).then(function() {
if (toBeInserted.length===0) { return cb(); }
return tags.silent(silentMode).insert(toBeInserted).then(function() {
return cb();
});
}).catch(function (err) {
return cb(err);
});
}
if (toBeInserted.length===0) { return cb(); }
return tags.silent(silentMode).insert(toBeInserted).then(function() {
return cb();
});
}).catch(function (err) {
return cb(err);
});
}
else {
junction = new DataObjectJunction(obj, x.mapping);
junction.silent(silentMode).insert(childs, function(err) {
if (err) { return cb(err); }
var toBeRemoved = [], toBeInserted = [];
childs.forEach(function(x) {
if (x.$state === 4) {
toBeRemoved.push(x);
}
else {
toBeInserted.push(x);
}
});
junction.silent(silentMode).insert(toBeInserted, function(err) {
if (err) {
return cb(err);
}
junction.silent(silentMode).remove(toBeRemoved, function(err) {
if (err) {
return cb(err);
}
return cb();
});
});
});
}
}
else {
cb();
}
}
else {
cb();
}
} else {
cb();
}
}, function(err) {
callback(err);
});
}
}
catch (err) {
callback(err);
}
}
}
module.exports = {
DataObjectAssociationListener,
DataObjectAssociationError,
DataObjectMultiAssociationError
};