@themost/data
Version:
MOST Web Framework Codename Blueshift - Data module
395 lines (378 loc) • 15.7 kB
JavaScript
const {DataObjectState} = require('./types');
const {eachSeries} = require('async');
const {DataConfigurationStrategy} = require('./data-configuration');
const {DataError} = require('@themost/common');
require('@themost/promise-sequence');
function isJSON(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
function edmTypeToJsonType(edmType) {
switch (edmType) {
case 'Edm.String':
return 'string';
case 'Edm.Boolean':
return 'boolean';
case 'Edm.Byte':
case 'Edm.SByte':
case 'Edm.Int16':
case 'Edm.Int32':
case 'Edm.Int64':
return 'integer';
case 'Edm.Decimal':
case 'Edm.Double':
return 'number';
case 'Edm.DateTime':
case 'Edm.EdmDateTimeOffset':
case 'Edm.Duration':
return 'string';
case 'Edm.Guid':
return 'string';
case 'Edm.Binary':
case 'Edm.Stream':
return 'string';
default:
return 'string';
}
}
class OnJsonAttribute {
/**
* @param {import('./data-model').DataModel} model
*/
static getJsonSchema(model) {
const { context } = model;
const {dataTypes} = context.getConfiguration().getStrategy(DataConfigurationStrategy);
const additionalProperties = false;
const properties = model.attributes.reduce((prev, attr) => {
/**
* @type {{edmtype: string,type:string}}
*/
const dataType = attr.type !== 'Json' ? dataTypes[attr.type] : null;
let type = 'object';
let assign = {};
if (dataType != null) {
type = edmTypeToJsonType(dataType.edmtype);
assign = {
[attr.name]: {
type
}
}
} else {
// try to get related model
let relatedModel = attr.additionalType != null ? context.model(attr.additionalType) : context.model(attr.type);
// if related model exists
if (relatedModel) {
// get json schema for related model
assign = {
[attr.name]: Object.assign(OnJsonAttribute.getJsonSchema(relatedModel), {
type
})
}
} else {
// if related model does not exist
assign = {
[attr.name]: {
type
}
};
}
}
// set property
Object.assign(prev, assign);
return prev;
}, {});
const required = model.attributes.filter((attr) => {
const primary = attr.primary === true;
const many = attr.many === true;
return attr.nullable === false && many === false && primary === false;
}).map((attr) => attr.name);
return {
properties,
required,
additionalProperties
}
}
/**
* @param {import('./data-model').DataModel} model
* @returns {Array<import('./types').DataField>}
*/
static getJsonAttributes(model) {
return model.attributes.filter((attr) => {
return attr.type === 'Json' && attr.additionalType != null;
});
}
/**
* @param {import('./types').DataEventArgs} event
* @param {function(err?:Error)} callback
* @returns {Promise<void> | Promise<unknown>}
*/
beforeSave(event, callback) {
try {
// get json attributes if any
const attributes= event.model.attributes.filter((attr) => {
const editable = attr.editable !== false;
const insertable = attr.insertable !== false;
const include = event.state === DataObjectState.Insert ? insertable : editable;
return include && attr.type === 'Json' && attr.additionalType != null && attr.model === event.model.name;
}).filter((attr) => {
return Object.prototype.hasOwnProperty.call(event.target, attr.name);
});
// exit if there are no json attributes
if (attributes.length === 0) {
return callback();
}
// iterate over json attributes
void eachSeries(attributes, (attr, cb) => {
// get attribute name
const {name} = attr;
const {[name]: value} = event.target;
if (value == null) {
return cb();
}
try {
const targetModel = event.model.context.model(attr.additionalType);
if (targetModel == null) {
return cb(new DataError('ERR_INVALID_MODEL', 'Property additional type cannot be determined.', 'The target model cannot be found.', event.model.name, attr.name));
}
// execute beforeSave event
// this operation will add calculated values and validate the object against the current state of the model
const items = Array.isArray(value) ? value : [value];
Promise.sequence(items.map((item) => {
return () => {
return new Promise((resolve, reject) => {
void targetModel.emit('before.save', {
target: item,
state: event.state,
model: targetModel
}, (err) => {
if (err) {
return reject(err);
}
// get object properties
const properties = Object.getOwnPropertyNames(item);
// get target model attributes
const attributes = targetModel.attributeNames;
// check if all properties are defined in the target model
const additionalProperty = properties.find((prop) => attributes.indexOf(prop) < 0);
if (additionalProperty != null) {
return reject(new DataError('ERR_INVALID_PROPERTY', `The given structured value seems to be invalid. The property '${additionalProperty}' is not defined in the target model.`, null, event.model.name, attr.name));
}
return resolve();
});
});
};
})).then(() => {
return cb();
}).catch((err) => {
return cb(err);
});
} catch(err) {
return cb(err);
}
}, (err) => {
if (err) {
return callback(err);
}
return callback();
});
} catch (err) {
return callback(err);
}
}
/**
* @protected
* @param {{model: DataModel, result: any, emitter?: import('./data-queryable').DataQueryable}} event
* @param {function(err?:Error): void} callback
* @returns void
*/
static afterSelect(event, callback) {
/**
* @type {string|null}
*/
let from = null;
if (event.emitter && event.emitter.query && event.emitter.query.$select) {
from = Object.keys(event.emitter.query.$select)[0];
}
/**
* @type {{name: string, from: string}[]}
*/
const jsonAttributes = event.model.attributes.filter((attr) => {
return attr.type === 'Json';
}).map((attr) => {
const { name } = attr;
return {
name,
from
}
});
// try to find json attributes in select clause processing expand statements
if (event.emitter && event.emitter.query && event.emitter.query.$expand) {
const joins = event.emitter.query.$expand;
if (Array.isArray(joins)) {
joins.forEach((join) => {
const joinEntityModel = join.$entity && join.$entity.model;
if (joinEntityModel) {
const joinModel = event.model.context.model(joinEntityModel);
if (joinModel) {
const from = join.$entity.$as || joinModel.viewAdapter;
const otherJsonAttributes = joinModel.attributes.filter((attr) => {
return attr.type === 'Json';
}).map((attr) => {
const { name } = attr;
return {
name,
from
}
});
jsonAttributes.push(...otherJsonAttributes);
}
}
});
}
}
if (jsonAttributes.length === 0) {
return callback();
}
let select = [];
const { viewAdapter: entity } = event.model;
if (event.emitter && event.emitter.query && event.emitter.query.$select) {
const querySelect = event.emitter.query.$select[entity] || [];
select.push(...querySelect);
}
let attributes = select.reduce((prev, element) => {
// if select element is a typical query field with $name property
if (element && typeof element.$name === 'string') {
// split $name property by dot
const matches = element.$name.split('.');
// if there are more than one parts
if (matches && matches.length > 1) {
// get collection and field
const [from, name] = matches;
if (jsonAttributes.findIndex((x) => x.name === name && x.from === from) >= 0) {
prev.push(name);
}
}
} else {
// try to get first property which should be attribute alias
const [key] = Object.keys(element);
if (Object.hasOwnProperty.call(element, key)) {
/**
* @type {{$jsonGet?: any[]}}
*/
const selectField = element[key];
if (selectField && typeof selectField.$name === 'string') {
// split $name property by dot
const matches = selectField.$name.split('.');
// if there are more than one parts
if (matches && matches.length > 1) {
// get collection and field
const [from, name] = matches;
if (jsonAttributes.findIndex((x) => x.name === name && x.from === from) >= 0) {
prev.push(key);
return prev;
}
}
}
// if select field has $jsonGet property
if (selectField.$jsonGet) {
const [jsonGet] = selectField.$jsonGet;
// if jsonGet has $name property
if (jsonGet && typeof jsonGet.$name === 'string') {
// split $name property by dot
const matches = jsonGet.$name.split('.');
if (matches && matches.length > 1) {
let index = 1;
let nextModel = event.model;
// iterate over matches
while(index < matches.length) {
let attribute = nextModel.getAttribute(matches[index]);
if (attribute && attribute.type === 'Json') {
if (index + 1 === matches.length) {
prev.push(key);
break;
}
if (attribute.additionalType) {
// get next model
nextModel = event.model.context.model(attribute.additionalType)
} else {
// add last part
prev.push(key);
// and exit loop
break;
}
} else {
break;
}
index++;
}
}
}
}
}
}
return prev
}, []);
if (select.length === 0) {
attributes = jsonAttributes.map((x) => x.name);
}
if (attributes.length === 0) {
return callback();
}
// define json converter
const parseJson = (item) => {
attributes.forEach((name) => {
if (Object.prototype.hasOwnProperty.call(item, name)) {
const value = item[name];
if (typeof value === 'string') {
item[name] = isJSON(value) ? JSON.parse(value) : value;
}
}
});
};
// iterate over result
const {result} = event;
if (result == null) {
return callback();
}
if (Array.isArray(result)) {
result.forEach((item) => parseJson(item));
} else {
// or parse json for single item
parseJson(result)
}
return callback();
}
/**
* @param {import('./types').DataEventArgs} event
* @param {function(err?:Error): void} callback
* @returns void
*/
afterSave(event, callback) {
return OnJsonAttribute.afterSelect({
model: event.model,
result: event.target
}, callback);
}
/**
* @param {import('./types').DataEventArgs} event
* @param {function(err?:Error): void} callback
* @returns void
*/
afterExecute(event, callback) {
try {
if (event.emitter && event.emitter.query && event.emitter.query.$select && event.result) {
return OnJsonAttribute.afterSelect(event, callback);
}
return callback();
} catch (err) {
return callback(err);
}
}
}
module.exports = {
OnJsonAttribute
}