Skip to content

Commit

Permalink
use json attributes (#181)
Browse files Browse the repository at this point in the history
* use json attributes

* validate attribute

* 2.17.0
  • Loading branch information
kbarbounakis authored Dec 22, 2024
1 parent e7ecb74 commit c634b76
Show file tree
Hide file tree
Showing 17 changed files with 1,265 additions and 387 deletions.
313 changes: 313 additions & 0 deletions OnJsonAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
const {DataObjectState} = require('./types');
const {eachSeries} = require('async');
const {DataConfigurationStrategy} = require('./data-configuration');
const {DataError} = require('@themost/common');


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
void targetModel.emit('before.save', {
target: value,
state: event.state,
model: targetModel
}, (err) => {
if (err) {
return cb(err);
}
// get object properties
const properties = Object.getOwnPropertyNames(value);
// 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 cb(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 cb();
});

} 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) {
const jsonAttributes = event.model.attributes.filter((attr) => {
return attr.type === 'Json' && attr.additionalType != null;
}).map((attr) => {
return attr.name
});
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 last part
if (jsonAttributes.indexOf(matches[1]) >= 0) {
prev.push(matches.pop());
}
}
} 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 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') {
// get next model
nextModel = event.model.context.model(attribute.additionalType)
// if this is the last match
if (index + 1 === matches.length) {
// add attribute
prev.push(matches[index]);
// and exit
break;
}
} else {
break;
}
index++;
}
}
}
}
}
}
return prev
}, []);
if (select.length === 0) {
attributes = jsonAttributes;
}
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] = JSON.parse(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
}
Loading

0 comments on commit c634b76

Please sign in to comment.