UNPKG

@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
'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); } };