@sap/xsodata
Version:
Expose data from a HANA database as OData V2 service with help of .xsodata files.
846 lines (708 loc) • 27.5 kB
JavaScript
'use strict';
/**
* Keep order
* The file ('./../utils/utils') requires other files which may again require this file. Hence the entityKind
* should be initialised before the first required
* @type {{table: number, view: number, attributeView: number, calculationView: number, inputParameters: number}}
*/
EntityType.entityKind = {
table: 0,
view: 1,
attributeView: 2,
calculationView: 3,
inputParameters: 4
};
module.exports = EntityType;
var utils = require('./../utils/utils');
var generatedKey = require('../utils/keyGenerator');
var typeConverter = require('./../utils/typeConverter');
var concurrencyTokenValidator = require('./../model/validator/xsoDataConcurrencyTokenValidator');
var annotationFactory = require('./annotationFactory');
var util = require('util');
// see https://help.sap.com/viewer/88fe5f56472e40cca6ef3c3dcab4855b/2.0.05/en-US/d336ea064c0948ffa3ef84dd516d69af.html
var CV_AGGREGATE_FUNCTIONS = {
1: "sum",
2: "count", // was avg before that was a bug
3: "min",
4: "max",
5: "avg", // supported since Q1 2020
};
function EntityType(entityType, settings) {
this._entityType = utils.clone(entityType);
this._settings = settings || {};
this.__metadata = null;
this.isInitialised = false;
this.name = this._entityType.name;
this.schema = this._entityType.schema;
this.tableName = this._entityType.table;
this.path = this._entityType.path;
this.keys = determineKeys(this._entityType);
// virtual table / virtual view data (includes synonyms + virtual tables)
this._isVirtual = false;
this._virtualObjectSchema = null;
this._virtualObjectName = null;
this._remoteSource = null;
// in .xsodata as "entityname" --> null
// in .xsodata as "entityname" concurrencytoken; --> {}
// in .xsodata as "entityname" concurrencytoken ("KEY"); --> { key : true }
this.concurrentProperties = this._entityType.concurrencytoken;
this._scopes = this._entityType.scopes;
this.originalConcurrentProperties = {};
if (this.concurrentProperties) {
Object.keys(this.concurrentProperties).forEach(function (key) {
this.originalConcurrentProperties[key] = true;
}.bind(this));
}
this.hasProperties = false;
this.modifications = this._entityType.modifications || [];
/**
* List of aggregates used in the entitytype
* @type {Array<{column: p.COLUMN_NAME, function: p.aggregate}>}
* @private
*/
this._aggregates = this._entityType.aggregates; // null/undefined/array/plain object
/**
* List of this entities property names (according to with and without filter)
* @type {Array}
*/
this.propertyNames = [];
/**
* List of this entities property objects (according to with and without filter)
* @type {Array}
*/
this.properties = [];
/**
* Map of this entities property names and property objects (according to with and without filter)
* @type {{}}
*/
this.propertiesMap = {};
/**
* List of this entities navigation property names
* @type {Array}
*/
this.navPropertyNames = [];
/**
* List of this entities navigation property objects
* @type {Array}
*/
this.navProperties = [];
/**
* Map of this entities navigation property names and property objects
* @type {{}}
*/
this.navPropertiesMap = {};
this.converterMapToJsonPayload = {};
this.converterMapToXMLPayload = {};
this._admindata = prepareAdmindata(this._settings.admindata, this._entityType.admindata);
}
EntityType.prototype.setVirtualInfo = function(objectSchema, objectName, remoteSource = null) {
this._isVirtual = true;
this._virtualObjectSchema = objectSchema;
this._virtualObjectName = objectName;
this._remoteSource = remoteSource;
};
function mergeAdmindataTuples(into, data) {
for (const tuple of data) {
if (!into[tuple.operation]) {
into[tuple.operation] = {};
}
if (!into[tuple.operation][tuple.property]) {
into[tuple.operation][tuple.property] = {};
}
into[tuple.operation][tuple.property] = tuple.value;
}
}
function prepareAdmindata(settingsAdmindata, entityTypeAdmindata) {
const ret = {};
mergeAdmindataTuples(ret, settingsAdmindata || []);
mergeAdmindataTuples(ret, entityTypeAdmindata || []); // entityTypeAdmindata overwrites global admindata
return ret;
}
/*
EntityType.prototype.getName = function () {
return this.name;
};*/
EntityType.prototype.getUriName = function () {
/*
if (this._entityType.parameters) {
if (this._entityType.parameters.entity) {
return this._entityType.parameters.entity;
} else {
return this.name + 'Parameters';
}
}*/
return this.name;
};
/**
* Returns an array of defined concurrent properties provided by xsodata configuration.
*
* @returns {array} Array with concurrent properties.
* If no concurrent properties provided in xsodata configuration but concurrencytoken
* exists the array contains all valid primitive entity properties.
* If no concurrencytoken was provided in xsodata configuration, returns null.
*/
EntityType.prototype.getConcurrentProperties = function getConcurrentProperties() {
if (this.hasConcurrencyToken()) {
return Object.keys(this.concurrentProperties);
}
return null;
};
/**
* Returns an object of defined concurrent properties provided by xsodata configuration.
*
* @returns {object} Object with concurrent properties.
* If no concurrent properties provided in xsodata configuration but concurrencytoken
* exists the object contains all valid primitive entity properties.
* If no concurrencytoken was provided in xsodata configuration, returns null.
*/
EntityType.prototype.getConcurrentPropertiesMap = function getConcurrentProperties() {
return this.concurrentProperties;
};
/**
* Returns true if the current entity has a defined concurrencytoken.
*
* @returns {boolean} True if concurrencytoken is defined, else false
*/
EntityType.prototype.hasConcurrencyToken = function hasConcurrencyToken() {
return !!this.concurrentProperties;
};
/**
* Returns true if the current entity has at least one concurrent property.
*
* @returns {boolean} True if at least one concurrent property is defined, else false
*/
EntityType.prototype.hasConcurrentProperties = function hasConcurrentProperties() {
return this.hasConcurrencyToken() && utils.hasProperties(this.concurrentProperties);
};
/**
* Returns true if the requested property is in concurrent mode.
* A property is in concurrent mode if the property was defined by concurrencytoken in the
* xsodata configuration or the concurrecytoken does't have any property. In this case
* all primitive properties are in concurrent mode by default.
*
* @param {string} property The name of the property
* @returns {boolean} True if the property is in concurrent mode, else false.
*/
EntityType.prototype.isConcurrentProperty = function isConcurrentProperty(property) {
return this.hasConcurrencyToken() && !!this.concurrentProperties[property];
};
EntityType.prototype.getNavigation = function (propertyName) {
if (this.navPropertiesMap) {
return this.navPropertiesMap[propertyName];
}
};
function determineKeys(entityType) {
var keys = {
names: entityType.keys || []
};
if (entityType.keys_generated) {
keys.generatedKey = entityType.keys_generated.local;
}
return keys;
}
EntityType.prototype.getScopes = function () {
return this._scopes;
};
EntityType.prototype.hasGeneratedKey = function () {
return !!this.keys.generatedKey;
};
EntityType.prototype.getAggregates = function () {
return this._aggregates || [];
};
EntityType.prototype.hasAggregates = function () {
return !!this._aggregates && this._aggregates.length;
};
EntityType.prototype.setKeyNames = function (keyNames) {
this.keys.names = [].concat(keyNames);
this.calcKeyNamesOrdered();
};
EntityType.prototype.calcKeyNamesOrdered = function () {
this.keyNamesOrdered = [];
if (!this.keys.names) {
return;
}
for (var i = 0; i < this.propertyNames.length; i++) {
var propertyName = this.propertyNames[i];
if (this.keys.names.indexOf(propertyName) > -1) {
this.keyNamesOrdered.push(propertyName);
}
}
};
EntityType.prototype.init = function (metadata, kind, tableStoreType, logger) {
logger.silly('model', 'EntityType.init');
this.__metadata = metadata;
this.kind = kind;
this.isInitialised = true;
var i;
/**
* List of this entities property names (according to with and without filter)
* @type {Array}
*/
this.propertyNames = [];
/**
* List of this entities property objects (according to with and without filter)
* @type {Array}
*/
this.properties = [];
/**
* Map of this entities property names and property objects (according to with and without filter)
* @type {{}}
*/
this.propertiesMap = {};
/**
* List of this entities navigation property names
* @type {Array}
*/
this.navPropertyNames = [];
/**
* List of this entities navigation property objects
* @type {Array}
*/
this.navProperties = [];
/**
* Map of this entities navigation property names and property objects
* @type {{}}
*/
this.navPropertiesMap = {};
this.converterMapToJsonPayload = {};
this.converterMapToXMLPayload = {};
var dbColumns;
//fill propertyNames
var propertyName;
var withh = this._entityType.properties.with;
if (withh) {
dbColumns = this.__metadata.getColumnsMap();
for (i = 0; i < withh.length; i++) {
propertyName = withh[i];
if (dbColumns[propertyName]) {
this.propertyNames.push(propertyName);
this.properties.push(dbColumns[propertyName]);
this.propertiesMap[propertyName] = dbColumns[propertyName];
} else {
logger.error('Unknown property: ' + propertyName);
throw('Unknown property');
}
}
} else {
dbColumns = this.__metadata.getColumns();
var without = this._entityType.properties.without;
//Loop over all existing columns
for (i = 0; i < dbColumns.length; i++) {
var column = dbColumns[i];
if (without && without.indexOf(column.COLUMN_NAME) > -1) {
continue;
}
propertyName = column.COLUMN_NAME;
this.propertyNames.push(propertyName);
this.properties.push(column);
this.propertiesMap[propertyName] = column;
}
}
//aggregates
this.resolveAggregates();
//
var navigates = this._entityType.navigates;
for (var navName in navigates) {
if (navigates.hasOwnProperty(navName)) {
this.navPropertyNames.push(navName);
this.navProperties.push(navigates[navName]);
this.navPropertiesMap[navName] = navigates[navName];
}
}
this.properties.forEach(this._addConverter.bind(this));
this._addGenKeyConverter();
if (this.properties.length) {
this.hasProperties = true;
}
this.calcKeyNamesOrdered();
// if tableStoreType is undefined then set "" value to the object property in order not to break the
// "CREATE LOCAL TEMPORARY <tableStoreType> TABLE" SQL statements
this.tableStoreType = tableStoreType || "";
};
EntityType.prototype.addProperty = function (colProperty, isKey) {
this.propertyNames.unshift(colProperty.COLUMN_NAME);
this.properties.unshift(colProperty);
this.propertiesMap[colProperty.COLUMN_NAME] = colProperty;
this._addConverter(colProperty);
if (isKey) {
var keys = this.keys || {};
keys.names.unshift(colProperty.COLUMN_NAME);
this.calcKeyNamesOrdered();
}
};
EntityType.prototype.resolveAggregates = function () {
this._aggregates = this._entityType.aggregates;
if (this._aggregates && !Array.isArray(this._aggregates)) {
var _aggr_props = this.properties.filter(function (p) {
return !!p.aggregate;
});
this._aggregates = _aggr_props.map(function (p) {
return { column: p.COLUMN_NAME, function: p.aggregate };
});
}
};
EntityType.prototype.makeCalcview = function () {
this.kind = EntityType.entityKind.calculationView;
};
EntityType.prototype.setCalculationViewDimensionData = function (rows, calcView, logger) {
this.calcView = calcView;
//var cv_properties = [];
var cv_propertiesMap = {};
var new_propertiesMap = {};
rows.forEach(function (row) {
cv_propertiesMap[row.COLUMN_NAME] = row;
});
this.properties.forEach(function (p) {
var cvp = cv_propertiesMap[p.COLUMN_NAME];
p.KIND = EntityType.entityKind.calculationView;
new_propertiesMap[p.COLUMN_NAME] = p;
if (cvp) {
p.aggregate = cvp.MEASURE_AGGREGATOR ? CV_AGGREGATE_FUNCTIONS[cvp.MEASURE_AGGREGATOR] : null;
p.SEMANTIC_TYPE = cvp.SEMANTIC_TYPE;
p.COLUMN_CAPTION = cvp.COLUMN_CAPTION;
p.DIMENSION_TYPE = cvp.DIMENSION_TYPE;
p.UNIT_COLUMN_NAME = cvp.UNIT_COLUMN_NAME;
p.DESC_NAME = cvp.DESC_NAME;
} else {
logger.silly('model', 'setCalculationView: no measure info for ' + p.COLUMN_NAME);
}
});
/*
rows.forEach(function (row) {
var property = self.propertiesMap[row.COLUMN_NAME];
if (!property) {
return;
}
property.aggregate = row.MEASURE_AGGREGATOR ? CV_AGGREGATE_FUNCTIONS[row.MEASURE_AGGREGATOR] : null;
property.KIND = EntityType.entityKind.calculationView;
cv_properties.push(property);
cv_propertiesMap[row.COLUMN_NAME] = property;
});
*/
this.propertiesMap = new_propertiesMap;
};
EntityType.prototype.getCalculationView = function () {
return this.calcView;
};
EntityType.prototype.getParameters = function () {
return this._entityType.parameters;
};
/*
EntityType.prototype.getPropertyNames = function () {
return this.propertyNames;
};*/
EntityType.prototype._addGenKeyConverter = function () {
var genKey = this.keys.generatedKey;
if (genKey) {
this._addConverter(generatedKey.createGenKeyProperty(genKey));
}
};
EntityType.prototype.getKeysOrGenKey = function () {
var genKey = this.keys.generatedKey;
if (genKey) {
return [genKey];
}
return this.keys.names;
};
EntityType.prototype.getPropertiesWithGenKey = function getPropertiesWithGenKey() {
var genKey = this.keys.generatedKey;
var properties = this.properties;
if (genKey) {
properties = [generatedKey.createGenKeyProperty(genKey)].concat(properties);
}
return properties;
};
EntityType.prototype._addConverter = function (property) {
this.converterMapToJsonPayload[property.COLUMN_NAME] = typeConverter.converterFunctions.dbNameToJsonPayload[property.DATA_TYPE_NAME];
this.converterMapToXMLPayload[property.COLUMN_NAME] = typeConverter.converterFunctions.dbNameToXmlPayload[property.DATA_TYPE_NAME];
};
/**
* Returns object containing EDM annotations for this entity type.
*
* @returns {Object} object with the entity type annotations, for example:
* {
* "sap:semantics" : "aggregate"
* }
*/
EntityType.prototype.getEntityTypeAnnotations = function getEntityTypeAnnotations() {
if (!this._annotations) {
this._annotations = annotationFactory.createEntityTypeAnnotations(this);
}
return this._annotations;
};
/**
* Returns object containing EDM annotations for the entity set, which is used for this entity type.
*
* @returns {Object} object with the entity set annotations, for example:
* {
* "sap:addressable" : "false",
* "sap:creatable" : "false"
* }
*/
EntityType.prototype.getEntitySetAnnotations = function getEntitySetAnnotations() {
if (!this._entitySetAnnotations) {
this._entitySetAnnotations = annotationFactory.createEntitySetAnnotations(this);
}
return this._entitySetAnnotations;
};
/**
* Returns object containing EDM annotations for the property with the specified propertyName.
*
* @param {string} propertyName - name of the entity type property, for which annotations should be returned
* @returns {Object} object with the property annotations, for example:
* {
* "sap:parameter" : "mandatory",
* "sap:label" : "Currency code variable",
* "sap:semantics" : "currency-code"
* }
*/
EntityType.prototype.getPropertyAnnotations = function getPropertyAnnotations(propertyName) {
var property = this.getProperty(propertyName);
if (!property._annotations) {
property._annotations = annotationFactory.createPropertyAnnotations(this, property);
}
return property._annotations;
};
/**
* Returns object containing EDM annotations for the navigation property with the specified navPropertyName.
*
* @param {string} navPropertyName - name of the entity type navigation property, for which the annotations
* should be returned
*
* @returns {Object} object with the navigation property annotations, for example:
* {
* "sap:creatable" : "false",
* "sap:filterable" : "false"
* }
*/
EntityType.prototype.getNavigationPropertyAnnotations = function getNavigationPropertyAnnotations(navPropertyName) {
var navProperty = this.getNavigation(navPropertyName);
if (!navProperty._annotations) {
navProperty._annotations = annotationFactory.createNavigationPropertyAnnotations();
}
return navProperty._annotations;
};
/**
* Returns property object for the entity type property with the specified <code>propertyName<code/>.
*
* @param {string} propertyName - name of the property
* @returns {Object} object containing the property metadata, for example:
* {
* KIND: 4,
* TABLE_NAME: null,
* COLUMN_NAME: 'IN_VARBINARY',
* POSITION: 20,
* DATA_TYPE_NAME: 'VARBINARY',
* IS_NULLABLE: true,
* LENGTH: null,
* SCALE: null,
* SEMANTIC_TYPE: '',
* MANDATORY: 1,
* DESCRIPTION: '',
* SELECTION_TYPE: 'SingleValue',
* MULTILINE: 0
* }
*
* @throws {Error} if propertyName is empty or there is no property with the specified name in the entity type
*/
EntityType.prototype.getProperty = function getProperty(propertyName) {
var property;
if (!propertyName) {
throw new Error("Property name must be specified");
}
property = this.propertiesMap[propertyName];
if (property) {
return property;
}
if (propertyName === this.keys.generatedKey) {
return generatedKey.createGenKeyProperty(propertyName);
}
throw new Error(util.format("Property with '%s' name does not exist", propertyName));
};
/**
* Return true if there is an entry in the BIMC_DIMENSION_VIEW table for that property. This is because
* XS Classic loops over the BIMC_DIMENSION_VIEW to set annotations like "sap:aggregation-role", so these annotation are
* not seen if there is no entry in BIMC_DIMENSION_VIEW.
* xsodata only loops over the properties, so we need to know if a property is in the BIMC_DIMENSION_VIEW.
*
* @param propertyName
* @returns {boolean} If the property has attr. DIMENSION_TYPE with is set if there is an entry in BIMC_DIMENSION_VIEW
*/
EntityType.prototype.isInBimcDimensionView = function isInBimcDimensionView(propertyName) {
var property = this.getProperty(propertyName);
if (!property) {
return false;
}
return property.DIMENSION_TYPE !== undefined;
};
/**
* Indicates, whether entity type property with the specified <code>propertyName<code/> is an aggregate property.
* A property is an aggregate property, if it is defined as a measure property of a calculation view or if it is used
* in the "aggregates" expression, which is defined for the entity set in the .xsodata file.
*
* @param {string} propertyName - name of the property
* @returns {boolean} <code>true<code/> if the property is an aggregate property; otherwise - <code>false<code/>.
*/
EntityType.prototype.isAggregateProperty = function isAggregateProperty(propertyName) {
var property = this.getProperty(propertyName),
aggregates,
aggregate,
i;
if (isMeasureProperty(property)) {
return true;
}
aggregates = this.getAggregates();
// check if the property is used in the "aggregates" expression
for (i = 0; i < aggregates.length; i++) {
aggregate = aggregates[i];
if (aggregate.column === propertyName) {
return true;
}
}
return false;
};
/**
* Indicates, whether the specified property object represents a measure attribute of the calculation view.
*
* @param {object} property - property object, obtained by calling getProperty() method.
* @returns <code>true<code/> if the property is a measure property; otherwise - <code>false<code/>.
*/
function isMeasureProperty(property) {
// value of DIMENSION_TYPE is retrieved from HANA during the loading of the entity type (CalcView) properties;
// 2 means "MEASURE", i.e. the property represents an aggregated value
return property.DIMENSION_TYPE === 2;
}
/**
* Indicates whether this entity type has measure properties.
*
* @returns {boolean} <code>true<code/> if the entity type has measure properties; otherwise - <code>false<code/>.
*/
EntityType.prototype.hasMeasureProperties = function hasMeasureProperties() {
var i,
property;
// only calculation views can have measure properties
if (this.kind !== EntityType.entityKind.calculationView) {
return false;
}
for (i = 0; i < this.propertyNames.length; i++) {
property = this.propertiesMap[this.propertyNames[i]];
if (isMeasureProperty(property)) {
return true;
}
}
return false;
};
EntityType.prototype.hasAdmindata = function hasAdmindata() {
return (Object.keys(this._admindata).length > 0);
};
EntityType.prototype.getAddAdmindata = function getAdminData(operation, property) {
if (this._admindata &&
this._admindata[operation] &&
this._admindata[operation][property]) {
return this._admindata[operation][property] === 'add';
}
return false;
};
/**
* Indicates whether instances of this entity type can be created via OData HTTP request.
*
* @returns {boolean} <code>true<code/> if the instances of the entity type can be created and <code>false<code/>
* otherwise.
*/
EntityType.prototype.isCreatable = function isCreatable() {
return !this._isActionForbidden("create") && this._isModifiable();
};
/**
* Indicates whether instances of this entity type can be updated via OData HTTP request.
*
* @returns {boolean} <code>true<code/> if the instances of the entity type can be updated and <code>false<code/>
* otherwise.
*/
EntityType.prototype.isUpdatable = function isUpdatable() {
return !this._isActionForbidden("update") && this._isModifiable() && !this.hasGeneratedKey();
};
/**
* Indicates whether instances of this entity type can be deleted via OData HTTP request.
*
* @returns {boolean} <code>true<code/> if the instances of the entity type can be deleted and <code>false<code/>
* otherwise.
*/
EntityType.prototype.isDeletable = function isDeletable() {
return !this._isActionForbidden("delete") && this._isModifiable() && !this.hasGeneratedKey();
};
/**
* Indicates whether instances of this entity type can be modified via OData HTTP request.
*
* @returns {boolean} <code>true<code/> if the instances of the entity type can be modified and <code>false<code/>
* otherwise.
*/
EntityType.prototype._isModifiable = function isModifiable() {
// entity types, representing DB views cannot be modified as well as the ones having aggregated properties
return this.kind !== EntityType.entityKind.view && this.kind !== EntityType.entityKind.attributeView &&
this.kind !== EntityType.entityKind.calculationView && this.kind !== EntityType.entityKind.inputParameters && !this.hasAggregates();
};
/**
* Indicates whether action with the specified actionName (i.e. create/update/delete) is forbidden for instances of this
* entity type. It can be done by adding the corresponding configuration to the .xsodata file (e.g. "create forbidden").
*
* @param {string} actionName - name of the action
*
* @returns {boolean} <code>true<code/> if the action is forbidden and <code>false<code/> otherwise.
*/
EntityType.prototype._isActionForbidden = function isActionForbidden(actionName) {
return !!(this.modifications && this.modifications[actionName] === "forbidden");
};
/**
* Validates the entity internal concurrent properties state regarding to database metadata after
* entity intialization. Checks if all properties defined by concurrencytoken are valid database
* fields.
*
* @private
* @param entityType {object} The entity to validate
* @throws {Error} Throws an error if the internal state of entity is not valid
*/
function validateConcurrentProperties(entityType) {
// if any concurrent property is not in db columns --> throw error
if (entityType.hasConcurrentProperties()) {
entityType.getConcurrentProperties().forEach(function (concurrentProp) {
if (!entityType.propertiesMap[concurrentProp]) {
throw new Error("XSODATA configuration has invalid concurrencytoken" +
" '" + concurrentProp + "' for entity " + entityType.name +
". The property does not exist");
}
});
}
}
/**
* Validates the entity internal concurrent properties state regarding to database metadata after
* entity intialization. Checks if all properties defined by concurrencytoken are valid database
* fields.
*
* @private
* @param entityType {object} The entity to validate
* @throws {Error} Throws an error if the internal state of entity is not valid
*/
function validateConcurrentPropertyIsNotKey(entityType) {
concurrencyTokenValidator.validateHasNotKeyPropertyToken([entityType]);
}
/**
* Updates the entity internal concurrent properties regarding to database metadata after
* entity initialization. Adds missing concurrent properties to the concurrent properties map.
* This function must be called AFTER all keys of the entity are known in order to exclude keys properly
* @private
*/
EntityType.prototype.buildConcurrentPropertyList = function () {
if (this.hasConcurrencyToken()) {
// Docu: If you specify concurrency token only, then all properties, except the key properties,
// are used to calculate the ETag value. If you provide specific properties, then only those properties are used for the calculation
if (!this.hasConcurrentProperties()) {
for (let propertyName of this.propertyNames) {
if (!this.keys.names.includes(propertyName)) {
this.concurrentProperties[propertyName] = true;
}
}
}
validateConcurrentProperties(this);
validateConcurrentPropertyIsNotKey(this);
}
};