UNPKG

@sap/xsodata

Version:

Expose data from a HANA database as OData V2 service with help of .xsodata files.

1,432 lines (1,212 loc) 57.8 kB
'use strict'; let _ = require('lodash'); const getCollector = require('../sql/dataCollectorGet'); const sqlStatement = require('./../sql/sqlStatement'); const dbConnect = require('./../db/connect'); const nodeUtils = require('util'); const xsODataUtils = require('../utils/utils'); const typedObjects = require('./../utils/typedObjects'); const EntityType = require('../model/entityType.js'); const typeConverter = require('./../utils/typeConverter'); const keyGenerator = require('../utils/keyGenerator'); const BadRequest = require('../utils/errors/http/badRequest'); const InternalError = require('../utils/errors/internalError'); // Types of db segments exports.DBS_Entity = 2; exports.DBS_ResourceNavigation = 3; exports.DBS_Property = 4; exports.DBS_Navigation = 6; // expanded navigation || selected navigation exports.DbSegment = DbSegment; /** * Constructs a dbSegment. A dbSegment stores all information to load data from one actual database table * * @param kind Either Entity, Navigation in resource path, property or navigation in expand tree * @param entityType * @param nr Id used for creating temporary table with unique names * @constructor */ function DbSegment(kind, entityType, nr) { this.kind = kind; this.previousDBSegment = null; this.entityType = entityType; // restrictions used on last segment of resource path this.restriction = { onlyCount: false, // $count used in uri onlyRefs: false, // $ref used in uri onlyValue: false // $value used in uri }; this.isLinks = false; this.isCollection = undefined; this.singleProperty = null; this._end = { from: null, to: null }; //for SQL generation this._DB_Schema = entityType.schema; this._DB_TableName = entityType.tableName; this._AliasName = entityType.name; this._AliasId = nr; this._Alias = this._AliasName + this._AliasId; /** * List of key predicates * @type {Array<{key : string,value : string}>} * @private */ this._KeyValues = []; this._recordNV = []; /*use for post/put/delete*/ this._recordMapFromPayload = {}; /** * Input parameters if this dbSegment queries a calcview * @type {Array<{key : string,value : string}>} * @private */ this._InputParams = []; this._InputParamsMap = new Map(); /** * Selected property name (either by * all properties of the entity set or explicit via $select) * @type {Array<String>} */ this._SelectedProperties = []; /** * Store column names ordered * @type {Array<String>} */ this._SelectedPropertiesOrdered = null; /** * For CV: (real) key names with alias * @type {Array<String,string>} */ this._aliasedKeyPropertiesOnCalcView = []; /** * Store names existing of navigation properties * @type {Array<String>} */ this._SelectedNavigations = []; //Array of string /** * Store names of really expanded of navigation properties * @type {Array<String>} */ this._ExpandedNavigations = []; //Array of string /** * Store dbSegments of really expanded of navigation properties * @type {Map<String,DbSegment>} */ this._ExpandedNavigationsDBSeg = {}; //Array of DbSegments /** * Store dbSegments of used navigation properties. May contain more navigation properties than * attribute _ExpandedNavigationsDBSeg * @type {Map<String,DbSegment>} */ this._relevantNavigationSegments = {}; this.isGenKeySelected = false; this.systemQueryParameter = { filter: null, orderBy: null, top: null, skip: null }; this.sql = { stmContainer: null, rows: null, rowsInput: null, readPosition: 0 }; /** * @type {null} */ this._rowsWithGenKey = null; this.nextDBSegment = null; this.association = null; } /** * Return only the alias for the table * @returns {string} Alias used to reference the table */ DbSegment.prototype.getAlias = function () { return this._Alias; }; /** * Return only the alias for an association * @returns {string} Alias used to reference the association */ DbSegment.prototype.getAssocAlias = function () { return 'Assoc' + this._AliasId; }; /** * Returns the tableName with schema and alias * @returns {{schema: string, table: string, alias: string}} */ DbSegment.prototype.getAliasedTableName = function (alias) { return { schema: this._DB_Schema, table: this._DB_TableName, alias: alias || this._Alias }; }; /** * Check if the given dbSegment navigates the corresponding association from its principal end * @returns {boolean} * */ DbSegment.prototype.isPrincipal = function () { return this.entityType.name === this.association.principal.type; }; /** * Returns the key values of the DbSegment. The key values origin from the URL parameters (POST/PUT) * @param key * @returns {undefined|{key: string, value: string}} */ DbSegment.prototype.getKeyValue = function (key) { for (let i = 0; i < this._KeyValues.length; i++) { if (this._KeyValues[i].name === key) { return this._KeyValues[i]; } } return undefined; }; DbSegment.prototype.hasKeyValues = function () { return this._KeyValues.length > 0; }; /** * Move POST http payload into this._recordNv and this._recordMapFromPayload * Key/Inputparameter Values are also filled * If required created/modified by/at information is also added * this._recordNv is used to fill the SQL statements * @param context * @param record Data from http payload */ DbSegment.prototype.setRecordFromPostPayload = function (context, record) { const converterArray = typeConverter.converterFunctions.jsonPayloadToDbName; // const converterArray1 = typeConverter.converterFunctions.uriPayloadToDbName; const supportNullable = context.gModel.isNullSupported(); const createBy = this.entityType.getAddAdmindata('create', 'by'); const createAt = this.entityType.getAddAdmindata('create', 'at'); const modifyBy = this.entityType.getAddAdmindata('modify', 'by'); const modifyAt = this.entityType.getAddAdmindata('modify', 'at'); //Loop through the properties of the entityType. //Object.keys(this.entityType.propertiesMap).forEach(function (property) { for (let [key, prop] of Object.entries(this.entityType.propertiesMap)) { //Load property from record const value = record[key]; let dbValue; if (value === undefined) { // Use columns default value if (prop.KIND === EntityType.entityKind.inputParameters) { if (this._InputParamsMap.has(key)) { const dbType = this.entityType.propertiesMap[key]['DATA_TYPE_NAME']; let param = []; this._InputParamsMap.get(key).toSqlHana(context, param, { useDbType: dbType }); dbValue = param[0]; } else { if (context.request.method === 'GET') { //for create, update, delete on a calcview input parameters are optional throw new BadRequest('Payload for input parameter ' + key + ' missing'); } } } else { dbValue = this.entityType.propertiesMap[key]['DEFAULT_VALUE']; } } else if (value === null) { // Use null const isNullable = this.entityType.propertiesMap[key]['IS_NULLABLE'] === 'TRUE'; if (!supportNullable || !isNullable) { if (!((createBy && key === 'CREATED_BY') || (createAt && key === 'CREATED_AT') || (modifyBy && key === 'MODIFIED_BY') || (modifyAt && key === 'MODIFIED_AT')) ) { throw new BadRequest('The serialized resource has a missing value for member ' + key); } } dbValue = null; } else { if (this.entityType._entityType.parameters && this.entityType._entityType.parameters.viaKey === true) { if (this._InputParamsMap.get(key)) { throw new BadRequest('Inputparameter ' + key + ' must be provided only in uri (POST)'); } if (prop.KIND === EntityType.entityKind.inputParameters) { throw new BadRequest('Inputparameter ' + key + ' must be provided in uri (POST)'); } } //Use the converted value const dbType = this.entityType.propertiesMap[key]['DATA_TYPE_NAME']; const converter = converterArray[typeConverter.dbTypeNameToODataTypeName[dbType]]; dbValue = converter(value, dbType); } this._recordMapFromPayload[key] = dbValue; this._recordNV.push({ name: key, value: dbValue }); } //set KeyValues let max = this.entityType.keys.names.length; for (let i = 0; i < max; i++) { this._KeyValues.push( { name: this.entityType.keys.names[i], value: this._recordMapFromPayload[this.entityType.keys.names[i]], dbType: this.entityType.propertiesMap[this.entityType.keys.names[i]].DATA_TYPE_NAME // OK } ); } // Here a check that no additional properties (and also no inline content) are send to server might be added }; /** * Move PUT http payload into this._recordNv and this._recordMapFromPayload * Key/Inputparameter Values are also filled * If required created/modified by/at information is also added * this._recordNv is used to fill the SQL statements * @param context * @param record Data from http payload */ DbSegment.prototype.setRecordFromPutPayload = function (context, record) { const converterArray = typeConverter.converterFunctions.jsonPayloadToDbName; const supportNullable = context.gModel.isNullSupported(); const createBy = this.entityType.getAddAdmindata('create', 'by'); const createAt = this.entityType.getAddAdmindata('create', 'at'); const modifyBy = this.entityType.getAddAdmindata('modify', 'by'); const modifyAt = this.entityType.getAddAdmindata('modify', 'at'); //Check if all required properties are send to server Object.keys(this.entityType.propertiesMap).forEach(function (property) { let dbValue; let kv = this.getKeyValue(property); if (kv) { if (this.entityType._entityType.parameters && this.entityType._entityType.parameters.viaKey === true) { if (this._InputParamsMap.get(property) && record[property]) { throw new BadRequest('Inputparameter ' + property + ' must be provided only in uri (PUT)'); } } dbValue = kv.value; this._recordMapFromPayload[property] = dbValue; this._recordNV.push({ name: property, value: dbValue }); } else { const value = record[property]; const isNullable = this.entityType.propertiesMap[property]['IS_NULLABLE'] === 'TRUE'; dbValue = null; if (value === undefined) { if (supportNullable && isNullable) { dbValue = null; } else { if (!( // skip special properties (createBy && property === 'CREATED_BY') || (createAt && property === 'CREATED_AT') || (modifyBy && property === 'MODIFIED_BY') || (modifyAt && property === 'MODIFIED_AT') || // skip optional calc view parameters (this.entityType.propertiesMap[property] && this.entityType.propertiesMap[property].MANDATORY === 0) )) { throw new BadRequest('The serialized resource has a missing value for member ' + property); } } } else { const dbType = this.entityType.propertiesMap[property]['DATA_TYPE_NAME']; dbValue = converterArray[typeConverter.dbTypeNameToODataTypeName[dbType]](value, dbType); } this._recordMapFromPayload[property] = dbValue; this._recordNV.push({ name: property, value: dbValue }); } }.bind(this)); // Here a check that no additional properties (and also no inline content) are send to server might be added }; DbSegment.setRecordPutPostLinks = function (context) { let toBeUpdated = context.oData.links.toBeUpdated; let keySource = context.oData.links.keySource; // Add keys toBeUpdated._KeyValues.forEach(function (key) { toBeUpdated._recordMapFromPayload[key.name] = key.value; toBeUpdated._recordNV.push({ name: key.name, value: key.value }); }); // Add foreign keys, e.g. Employees.ManagerId let fkeys = toBeUpdated.getQForeignKeyProperties(); fkeys.forEach(function (key, i) { toBeUpdated._recordMapFromPayload[key.property] = keySource._KeyValues[i].value; toBeUpdated._recordNV.push({ name: key.property, value: keySource._KeyValues[i].value }); }); }; DbSegment.setRecordDeleteLinks = function (context) { let toBeUpdated = context.oData.links.toBeUpdated; // Add keys toBeUpdated._KeyValues.forEach(function (key) { toBeUpdated._recordMapFromPayload[key.name] = key.value; toBeUpdated._recordNV.push({ name: key.name, value: key.value }); }); // Skip adding foreign keys, so that later in movePayloadFromSegmentToSelectStm they are set to 'null' }; /** * Returns the tableName with schema and alias * @returns {{schema: string, table: string}} */ DbSegment.prototype.getQTableNameNoAlias = function () { return { schema: this._DB_Schema, table: this._DB_TableName }; }; /** * @returns {{type: string, joinProperties: [string], multiplicity: string}|null} */ DbSegment.prototype.getTo = function () { return this._end.to; }; /** * @param {{type: string, joinProperties: [string], multiplicity: string}} endTo */ DbSegment.prototype.setTo = function (endTo) { this._end.to = endTo; }; /** * @param {{type: string, joinpProperties: [string], multiplicity: string}} endFrom */ DbSegment.prototype.setFrom = function (endFrom) { this._end.from = endFrom; }; /** * @returns {{type: string, joinProperties: [string], multiplicity: string}} */ DbSegment.prototype.getFrom = function () { return this._end.from; }; DbSegment.prototype.setOver = function (endOver) { this._end.over = endOver; }; DbSegment.prototype.getOver = function () { return this._end.over; }; DbSegment.prototype.setLinks = function () { if (this.previousDBSegment) { this.previousDBSegment.setLinks(); } else { this.isLinks = true; } }; /** * Returns the key names as TableColumn array for usage in the INSERT, UPDATE or CREATE TABLE SQL statements. * @returns {sql.TableColumn[]} */ DbSegment.prototype.getQKeyProperties = function () { let ret = []; for (let i = 0; i < this.entityType.keys.names.length; i++) { ret.push(new sqlStatement.TableColumn(this._Alias, this.entityType.keys.names[i])); } return ret; }; /** * Returns the key names as SelectProperty array for usage in a SELECT SQL statement. * @returns {sqlStatement.SelectProperty[]} */ DbSegment.prototype.getQKeyPropertiesForSelect = function () { return this._createSelectProperties(this._Alias, this.entityType.keys.names); }; /** * Creates an array of SelectProperty instances for usage in a SELECT SQL statement. * @returns {sqlStatement.SelectProperty[]} */ DbSegment.prototype._createSelectProperties = function (tableAlias, propertyNames, with0123Alias) { let propertiesMap = this.entityType.propertiesMap; return propertyNames.map(function (propertyName, index) { let property = propertiesMap[propertyName]; let propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; return new sqlStatement.SelectProperty(tableAlias, propertyName, propertyType, with0123Alias ? index.toString() : undefined); }); }; DbSegment.prototype.getQKeyPropertiesWith0123AliasForSelect = function (noTable) { return this._createSelectProperties(noTable ? null : this._Alias, this.entityType.keys.names, true); }; DbSegment.prototype.getKeyProperties0123ForSelect = function (noTable) { let ret = []; let keyNames = this.entityType.keyNamesOrdered; for (let i = 0; i < keyNames.length; i++) { ret.push( new sqlStatement.SelectProperty(noTable ? null : this._Alias, i.toString(), null) ); } return ret; }; /** * Adds a table input parameter when accessing a calcview with localVariables. * E.g. OrderValues.hdbcalculationview with IP_LANG and IP_CLIENT * Call via: http://.../material.xsodata/OrderValueParameters(IP_CLIENT='100',IP_LANG='E')/Results?$format=json * * @param context * @param key * @param value */ DbSegment.prototype.addInputParameter = function (context, key, value) { context.logger.silly('dbsegment', 'addInputParameter'); this._InputParams.push({ key: key, value: value }); this._InputParamsMap.set(key, value); }; DbSegment.prototype.getInputParameters = function () { return this._InputParams; }; DbSegment.prototype.getParameterValue = function (parameterName) { let param = _.find(this._InputParams, function (kv) { return kv.key === parameterName; }); if (param) { return param.value; } }; DbSegment.prototype.validateInputParameters = function () { for (let [keyName, value] of Object.entries(this.entityType.inputParameters)) { if (value.MANDATORY === 1 && !this._InputParamsMap.has(keyName)) { throw new InternalError('Missing value for input parameter ' + keyName + '.'); } } for (let i = 0; i < this._InputParams.length; i++) { let keyName = this._InputParams[i].key; if (!this.entityType.inputParameters[keyName]) { throw new InternalError('Inputparameter ' + keyName + ' unknown'); } } }; DbSegment.prototype.validateKeyParameters = function () { const keyNames = this.entityType.keyNamesOrdered; for (let i = 0; i < keyNames.length; i++) { const keyName = keyNames[i]; const property = this.entityType.propertiesMap[keyName]; if (property.KIND !== EntityType.entityKind.inputParameters) { if (!this._InputParamsMap.has(keyName)) { throw new InternalError('Missing value for input parameter ' + keyName + '.'); } } } }; /** * Create a sql select fragments to select key properties with alias 0,1,2, ... * Those selects are used for $expand to join the sub queries * Sample: ... "FromEntity1"."KEY_1" "0" ... * @param noTable Provide true if the table should be added to the fragment * @returns {[]} * */ DbSegment.prototype.getKeyProperties0123ForSelectAs0123 = function (noTable) { let ret = []; let keyNames; let keyName; let property; let selectProperty; let propertyType; let paramValue; let i; keyNames = this.entityType.keyNamesOrdered; for (i = 0; i < keyNames.length; i++) { keyName = keyNames[i]; property = this.entityType.propertiesMap[keyName]; selectProperty = null; if (property.KIND === EntityType.entityKind.inputParameters) { paramValue = this.getParameterValue(property.COLUMN_NAME); selectProperty = new sqlStatement.ParameterSelectProperty(noTable ? null : this._Alias, keyName, i.toString(), paramValue); // Here the value is added. // E.g. for /CalcFrom(IN_INTEGER=123,IN_NVARCHAR='AAA',KEY_2=24) there is no way to do a // select IN_INTEGER, IN_NVARCHAR from CalcFrom(placeholder."$$IN_NVARCHAR$$" => ?,placeholder."$$IN_INTEGER$$" => ?) where ("CalcFrom1"."KEY_2" = ?) with ? = [123, 'AAA', 24] // that because the calcview at HANA object does not expose the inputparameter in the result set. // So the input parameters are only added as key in the OData layer. } else { propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; selectProperty = new sqlStatement.SelectProperty(noTable ? null : this._Alias, keyName, propertyType, i.toString()); } ret.push(selectProperty); } return ret; }; DbSegment.prototype.getKeyProperties0123ForSelectOnCalcViewAs0123 = function (noTable) { let ret = []; let keyNames; let keyName; let property; let selectProperty; let propertyType; let i; let j = -1; keyNames = this.entityType.keyNamesOrdered; for (i = 0; i < keyNames.length; i++) { keyName = keyNames[i]; property = this.entityType.propertiesMap[keyName]; selectProperty = null; if (property.KIND !== EntityType.entityKind.inputParameters) { j = j + 1; // for real key properties use numbering: 0,1,2, ... (skip CV input params) propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; selectProperty = new sqlStatement.SelectProperty(noTable ? null : this._Alias, keyName, propertyType, j.toString()); ret.push(selectProperty); } } return ret; }; DbSegment.prototype.setAliasedKeyPropertiesOnCalcView = function(selectProperties) { this._aliasedKeyPropertiesOnCalcView = selectProperties; }; DbSegment.prototype.hasAliasedKeyPropertiesOnCalcView = function() { return this._aliasedKeyPropertiesOnCalcView.length !== 0; }; DbSegment.prototype.getAliasedKeyPropertyOnCalcView = function(propertyName) { for (let i = 0; i < this._aliasedKeyPropertiesOnCalcView.length; i++) { if (this._aliasedKeyPropertiesOnCalcView[i].property === propertyName) { return this._aliasedKeyPropertiesOnCalcView[i]; } } return null; }; /** * Returns a selectProperty for usage in the select part of sql statements * The property are named "0","1","2",... and have as type the type of the corresponding key ( ordered by position * @returns {sqlStatement.SelectProperty[]} */ DbSegment.prototype.getKeyProperties0123ForCreate = function () { let ret = []; let keyNames = this.entityType.keyNamesOrdered; let propertiesMap = this.entityType.propertiesMap; for (let i = 0; i < keyNames.length; i++) { let keyName = keyNames[i]; let property = propertiesMap[keyName]; let typeString = property.DATA_TYPE_NAME; if (typeString === 'DECIMAL') { typeString += '(' + property.LENGTH.toString() + ',' + property.SCALE.toString() + ')'; } else if (typeString === 'VARCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NVARCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'ALPHANUM') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'CHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'VARBINARY') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'SHORTTEXT') { typeString = 'NVARCHAR(10)'; } ret.push( new sqlStatement.CreateProperty(i.toString(), typeString) ); } return ret; }; DbSegment.prototype.getKeyProperties0123ForOrderBy = function (noTable) { let ret = []; let keyNames = this.entityType.keyNamesOrdered; for (let i = 0; i < keyNames.length; i++) { ret.push(new typedObjects.SortOrder( /* no dbType required as property is used only for SortOrder */ new typedObjects.Property(i.toString(), noTable ? null : this._Alias), 'ASC' )); } return ret; }; DbSegment.prototype.getKeyProperties0123ForOrderByCalcView = function (noTable) { let ret = []; let keyName; let property; let j = -1; let keyNames = this.entityType.keyNamesOrdered; for (let i = 0; i < keyNames.length; i++) { keyName = keyNames[i]; property = this.entityType.propertiesMap[keyName]; if (property.KIND !== EntityType.entityKind.inputParameters) { j = j + 1; let newSortOrder = new typedObjects.SortOrder(new typedObjects.Property(j.toString(), noTable ? null : this._Alias), 'ASC'); newSortOrder.setPropertyName(property.COLUMN_NAME); ret.push(newSortOrder); } } return ret; }; DbSegment.prototype.getKeyPropertiesNotSelectedForSelect = function (noTable = undefined, inputParameters = null) { let ret = []; let keyNames; let i; let keyName; let key; let propertyType; keyNames = this.entityType.keyNamesOrdered; // remove input params from key list if (inputParameters !== null) { for (let inputParameterName in inputParameters) { if (inputParameters.hasOwnProperty(inputParameterName)) { let indexKeyName = keyNames.indexOf(inputParameterName); if (indexKeyName > -1) { keyNames.splice(indexKeyName, 1); } } } } for (i = 0; i < keyNames.length; i++) { keyName = keyNames[i]; key = this.entityType.propertiesMap[keyName]; if ((this._getSortedSelectProperties().indexOf(keyName) === -1) && (this._SelectedNavigations.indexOf(keyName) === -1)) { propertyType = key.aggregate ? null : key.DATA_TYPE_NAME; ret.push(new sqlStatement.SelectProperty(noTable ? null : this._Alias, keyName, propertyType, null)); } } return ret; }; DbSegment.prototype.getKeyPropertiesNotSelectedForCreate = function () { let ret = []; let keyNames = this.entityType.keyNamesOrdered; let propertiesMap = this.entityType.propertiesMap; for (let i = 0; i < keyNames.length; i++) { let keyName = keyNames[i]; if ((this._getSortedSelectProperties().indexOf(keyName) === -1) && (this._SelectedNavigations.indexOf(keyName) === -1)) { let property = propertiesMap[keyName]; let typeString = createPropertyTypeString(property, this.entityType); ret.push( new sqlStatement.CreateProperty(keyName, typeString) ); } } return ret; }; DbSegment.prototype.getPropertiesForCreate = function () { let ret = []; //let properties = this.entityType.properties; let properties = this._getSortedSelectProperties(); for (let i = 0; i < properties.length; i++) { let property = this.entityType.propertiesMap[properties[i]]; ret.push( createSqlCreateProperty(property, this.entityType) ); } return ret; }; // Get the properties (as full objects, e.g. with data type etc.) of the associative entity (3rd table) // via the association's 'over', for CREATE statements DbSegment.prototype.getOverPropertiesForCreate = function () { let ret = []; let over = this.getOver(); let properties = []; // fetch Key property from original table const from = this.getFrom(); for (let i = 0; i < from.joinproperties.length; i++) { const keyPropOrig = this.previousDBSegment.entityType.propertiesMap[from.joinproperties[i]]; const keyProp = xsODataUtils.clone(keyPropOrig); keyProp.COLUMN_NAME = from.overRefProp[i];// update the column and table name according to the associative table keyProp.TABLE_NAME = over.object; keyProp._entityType = this.previousDBSegment.entityType; properties.push(keyProp); } const to = this.getTo(); for (let i = 0; i < to.joinproperties.length; i++) { const keyPropOrig = this.entityType.propertiesMap[to.joinproperties[i]]; const keyProp = xsODataUtils.clone(keyPropOrig); keyProp.COLUMN_NAME = to.overRefProp[i]; keyProp.TABLE_NAME = over.object; keyProp._entityType = this.entityType; properties.push(keyProp); } for (let i = 0; i < properties.length; i++) { ret.push( createSqlCreateProperty(properties[i], properties[i]._entityType) ); } return ret; }; function createSqlCreateProperty(property, entityType) { return new sqlStatement.CreateProperty(property.COLUMN_NAME, createPropertyTypeString(property, entityType)); } /** * Create a sql fragment which declares a column * @param property * @param entityType * @returns {string} */ function createPropertyTypeString(property, entityType) { let typeString = property.DATA_TYPE_NAME; const checkLength = (property) => { // if (property.LENGTH === null || property.LENGTH === undefined) { const name = entityType ? entityType.name : ''; throw new InternalError('Property ' + name + '-' + property.COLUMN_NAME + ' with type ' + property.DATA_TYPE_NAME + ' requires length-setting.'); } }; if (typeString === 'DECIMAL') { checkLength(property); const scale = property.SCALE ? property.SCALE.toString() : 0; typeString += '(' + property.LENGTH.toString() + ',' + scale + ')'; } else if (typeString === 'VARCHAR') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NVARCHAR') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'ALPHANUM') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'CHAR') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NCHAR') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'VARBINARY') { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; // ON } else if (typeString === 'SHORTTEXT') { typeString = 'NVARCHAR(10)'; } return typeString; } /** * USE CASE accessing calc view with "via key" * Create sql fragments which select a column either directly with data from DB * or indirectly with "?" and data from sql parameters ( the later in case of query calculation views) * @param noTable Provide true if the table should be added to the fragment * @returns {{selects: Array<SelectProperty>, input: Array<ParameterSelectProperty>}} */ DbSegment.prototype.getPropertiesForSelectCollectInputParameters = function (noTable) { let ret = []; let retInput = []; let properties = this._getSortedSelectProperties(); let property; let selectProperty; let propertyType; for (let i = 0; i < properties.length; i++) { property = this.entityType.propertiesMap[properties[i]]; selectProperty = null; if (property.KIND === EntityType.entityKind.inputParameters) { let paramValue = this.getParameterValue(property.COLUMN_NAME); selectProperty = new sqlStatement.ParameterSelectProperty(noTable ? null : this._Alias, properties[i], null, paramValue); selectProperty._property = property; selectProperty._dbType = property.aggregate ? null : property.DATA_TYPE_NAME; retInput.push(selectProperty); } else { propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; selectProperty = new sqlStatement.SelectProperty(noTable ? null : this._Alias, properties[i], propertyType, null); ret.push(selectProperty); } } return { selects: ret, input: retInput }; }; /* * USE CASE accessing calc view without "via key" and normal queries * Create sql fragments which select a column either directly with data from DB * or indirectly with "?" and data from sql parameters ( the later in case of query calculation views) * @param noTable Provide true if the table should be added to the fragment * @returns Array<SelectProperty|ParameterSelectProperty>} */ DbSegment.prototype.getPropertiesForSelect = function (noTable) { let ret = [], properties = this._getSortedSelectProperties(), property, selectProperty, propertyType; for (let i = 0; i < properties.length; i++) { property = this.entityType.propertiesMap[properties[i]]; selectProperty = null; if (property.KIND === EntityType.entityKind.inputParameters) { let paramValue = this.getParameterValue(property.COLUMN_NAME); selectProperty = new sqlStatement.ParameterSelectProperty(noTable ? null : this._Alias, properties[i], null, paramValue); } else { propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; selectProperty = new sqlStatement.SelectProperty(noTable ? null : this._Alias, properties[i], propertyType, null); } ret.push(selectProperty); } return ret; }; /** * USE CASE accessing calc view with "via key" * Create sql select fragments to select a all properties (normal and input parameters) from this DbSegment. * Used in the POST/PUT case to create the select SQL commands for selecting rows from the tmp tables. * Sample: ... "IN_NVARCHAR" "IN_INTEGER "OUT_DATE"... * @param noTable Provide true if the table should be added to the fragment * @returns {Array<SelectProperty>} */ DbSegment.prototype.getSelectFragmentsFromAllProperties = function (noTable) { let ret = []; let properties = this._getSortedSelectProperties(); for (let i = 0; i < properties.length; i++) { const property = this.entityType.propertiesMap[properties[i]]; // this include property KIND === inputParameters const propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; ret.push(new sqlStatement.SelectProperty(noTable ? null : this._Alias, properties[i], propertyType, null)); } return ret; }; /** * Get the properties (as names only) of the associative entity (3rd table) via the association's 'over', for SELECT statements * @param rId * @returns {[]} */ DbSegment.prototype.getSelectFragmentsForOverProperties = function (rId) { let ret = [], selectProperty; let over = this.getOver(); let properties = []; for (let i = 0; i < over.principal.length; i++) { properties.push(over.principal[i]); } for (let i = 0; i < over.dependent.length; i++) { properties.push(over.dependent[i]); } for (let i = 0; i < properties.length; i++) { selectProperty = new sqlStatement.SelectProperty(rId, properties[i], null); ret.push( selectProperty ); } return ret; }; DbSegment.prototype.getAllSelectedPropertiesConsideringAggregates = function (noTable) { let table = noTable ? null : this._Alias; let withAlias = true; return this._getSortedSelectProperties().map(createSelectProperty.bind(this, table, withAlias)); }; DbSegment.prototype.getPropertyAsSelectProperty = function (propertyName) { let withAlias = false; return createSelectProperty.call(this, this._Alias, withAlias, propertyName); }; function createSelectProperty(table, withAlias, propertyName) { /* jshint -W040 */ let aggregate; let alias; let property; let propertyType; aggregate = this.entityType.getAggregates().filter(function (aggregate) { return propertyName === aggregate.column; }).shift(); if (aggregate) { alias = withAlias && propertyName; return new sqlStatement.AggregateSelectProperty(aggregate.function.toLowerCase(), table, propertyName, alias); } property = this.entityType.propertiesMap[propertyName]; if (property.KIND === EntityType.entityKind.inputParameters) { return new sqlStatement.ParameterSelectProperty(table, propertyName, null, this.getKeyValue(propertyName)); } propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; return new sqlStatement.SelectProperty(table, propertyName, propertyType, null); /* jshint +W040 */ } DbSegment.prototype.getNonKeyJoinProperties = function () { let keys = this.entityType.keyNamesOrdered; let joinProperties = this.getTo() && this.getTo().joinproperties || []; let joinProps = joinProperties.filter(function (joinProp) { return keys.indexOf(joinProp) < 0; }); return concatJoinProperties([], joinProps, this._Alias); }; DbSegment.prototype.getAllSelectedIncludingJoinPropertiesConsideringAggregates = function (noTable) { let table = noTable ? null : this._Alias; let selectedProperties = this.getAllSelectedPropertiesConsideringAggregates(noTable); let joinProps = []; if (this.getTo()) { joinProps = this.getTo().joinproperties; } else { let relevantNavigationSegments = this.getRelevantNavigationSegments(); joinProps = getJoinProperties(relevantNavigationSegments, this._SelectedNavigations); } return concatJoinProperties(selectedProperties, joinProps, table); }; function getJoinProperties(relevantNavigationSegments, selectedNavigations) { return selectedNavigations.reduce(toJoinProperties, []); function toJoinProperties(joinProps, selectedNavigation) { let selectedNavigationSegment = relevantNavigationSegments[selectedNavigation]; if (selectedNavigationSegment) { let from = selectedNavigationSegment.getFrom(); if (from) { return joinProps.concat(from.joinproperties); } } return joinProps; } } DbSegment.prototype.getAllSelectedNonAggregatePropertiesForOrderBy = function (noTable) { let table = noTable ? null : this._Alias; let entityType = this.entityType; let selectedProperties = this._getSortedSelectProperties(); if (entityType.hasAggregates()) { selectedProperties = selectedProperties.filter(aggregates); } return selectedProperties.map(toSqlSelectProperty); function aggregates(selectedProp) { return !entityType.getAggregates().some(withColumnName); function withColumnName(aggregate) { return aggregate.column === selectedProp; } } function toSqlSelectProperty(selectedProperty) { return new sqlStatement.SelectProperty(table, selectedProperty); } }; DbSegment.prototype.getAllSelectedNonAggregateIncludingJoinPropertiesForGroupBy = function (noTable) { let table = noTable ? null : this._Alias; let selectedProps = this.getAllSelectedNonAggregatePropertiesForOrderBy(noTable, true); let joinProps = []; if (this.getTo()) { joinProps = this.getTo().joinproperties; } else { let relevantNavigationSegments = this.getRelevantNavigationSegments(); joinProps = getJoinProperties(relevantNavigationSegments, this._SelectedNavigations); } return concatJoinProperties(selectedProps, joinProps, table); }; function concatJoinProperties(selectedProps, joinProps, table) { joinProps.forEach(function (joinProp) { let shouldAdd = !selectedProps.some(function (selectProp) { return selectProp.property === joinProp; }); if (shouldAdd) { selectedProps.push(new sqlStatement.SelectProperty(table, joinProp, null)); } }); return selectedProps; } DbSegment.prototype.getNavPropertiesForCreate = function () { let ret = []; //let properties = this.entityType.properties; let properties = this._SelectedNavigations; for (let i = 0; i < properties.length; i++) { let navPropertyName = properties[i]; let navDbSeg = this._relevantNavigationSegments[navPropertyName]; if (!navDbSeg) { continue; } let propertyNames = navDbSeg.getFrom().joinproperties; for (let ii = 0; ii < propertyNames.length; ii++) { let property = this.entityType.propertiesMap[propertyNames[ii]]; let propertyName = property.COLUMN_NAME; if (this._getSortedSelectProperties().indexOf(propertyName) > -1) { continue; } let typeString = property.DATA_TYPE_NAME; if (typeString === 'DECIMAL') { typeString += '(' + property.LENGTH.toString() + ',' + property.SCALE.toString() + ')'; } else if (typeString === 'VARCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NVARCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'ALPHANUM') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'CHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'NCHAR') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'VARBINARY') { typeString += '(' + property.LENGTH.toString() + ')'; } else if (typeString === 'SHORTTEXT') { typeString = 'NVARCHAR(10)'; } ret.push( new sqlStatement.CreateProperty(propertyName, typeString) ); } } return ret; }; DbSegment.prototype.getNavPropertiesForSelect = function (noTable) { let ret = []; //let properties = this.entityType.properties; let properties = this._SelectedNavigations; for (let i = 0; i < properties.length; i++) { let navPropertyName = properties[i]; if (this._getSortedSelectProperties().indexOf(navPropertyName) > -1) { continue; } let navDbSeg = this._relevantNavigationSegments[navPropertyName]; if (!navDbSeg) { continue; } let propertyNames = navDbSeg.getFrom().joinproperties; for (let ii = 0; ii < propertyNames.length; ii++) { let property = this.entityType.propertiesMap[propertyNames[ii]]; let propertyName = property.COLUMN_NAME; if (this._getSortedSelectProperties().indexOf(propertyName) > -1) { continue; } ret.push( new sqlStatement.SelectProperty(noTable ? null : this._Alias, propertyNames[ii], null) ); } } return ret; }; /** * Returns the key names of used/expanded navigation selectProperty list (containing table alias and key name) * For usage in the select part of sql statements * @returns {sqlStatement.SelectProperty[]} */ DbSegment.prototype.getNonKeyNonSelectedProperties4Expansion = function () { // let ret = []; for (let i = 0; i < this._ExpandedNavigations.length; i++) { let exp = this._relevantNavigationSegments[this._ExpandedNavigations[i]]; let from = exp._end.from; for (let ii = 0; ii < from.joinproperties.length; ii++) { let joinProperty = from.joinproperties[ii]; //Non Key if (this.entityType.keys.names.indexOf(joinProperty) === -1) { if (this._getSortedSelectProperties().indexOf(joinProperty) === -1) { ret.push( new sqlStatement.SelectProperty(this._Alias, joinProperty, null) ); } } } } return ret; }; /** * Returns non key properties as array for usage in the INSERT, UPDATE or CREATE TABLE SQL statements. * @returns {sqlStatement.TableColumn[]} */ DbSegment.prototype.getQNonKeyProperties = function () { let ret = []; let propertyName; for (let i = 0; i < this._getSortedSelectProperties().length; i++) { propertyName = this._getSortedSelectProperties()[i]; if (this.entityType.keys.names.indexOf(propertyName) === -1) { ret.push(new sqlStatement.TableColumn(this._Alias, propertyName)); } } return ret; }; /** * Returns non key properties as array for usage in a SELECT SQL statement. * @returns {sqlStatement.SelectProperty[]} */ DbSegment.prototype.getQNonKeyPropertiesForSelect = function () { let ret = []; let propertiesMap = this.entityType.propertiesMap; let propertyName; let property; let propertyType; for (let i = 0; i < this._getSortedSelectProperties().length; i++) { propertyName = this._getSortedSelectProperties()[i]; if (this.entityType.keys.names.indexOf(propertyName) === -1) { property = propertiesMap[propertyName]; propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; ret.push(new sqlStatement.SelectProperty(this._Alias, propertyName, propertyType, null)); } } return ret; }; /** * for this DBSeg, returns the foreign key for prev/next DBSeg * */ DbSegment.prototype.getQForeignKeyProperties = function () { let ret = []; let fKeys; if (this.previousDBSegment) { fKeys = this.getTo().joinproperties; } else if (this.nextDBSegment) { let refDBSeg = this.nextDBSegment; fKeys = refDBSeg.getFrom().joinproperties; } else { // error } let alias = this._Alias; fKeys.forEach(function (fkeyName) { ret.push( new sqlStatement.SelectProperty(alias, fkeyName, null) ); }); return ret; }; /** * Returns the key names and values * For usage in where clauses * @returns {{key : sqlStatement.Property, value : string}[]} */ DbSegment.prototype.getQKeyWithValues = function (alias) { let ret = []; for (let i = 0; i < this._KeyValues.length; i++) { let etProperty = this.entityType.propertiesMap[this._KeyValues[i].name]; if (etProperty.KIND !== EntityType.entityKind.inputParameters) { ret.push({ key: new typedObjects.Property(this._KeyValues[i].name, alias ? alias : this._Alias, etProperty.DATA_TYPE_NAME), value: this._KeyValues[i].value }); } } return ret; }; DbSegment.prototype.getQKeyWithValuesDB = function (alias) { let ret = []; for (let i = 0; i < this._KeyValues.length; i++) { ret.push({ key: new typedObjects.Property(this._KeyValues[i].name, alias ? alias : this._Alias, this._KeyValues[i].dbType), //OK value: new typedObjects.DbValue(this._KeyValues[i].value, this._KeyValues[i].dbType) // Ok }); } return ret; }; DbSegment.prototype.getQForeignKeyWithValues = function (alias) { let ret = [], refDBSeg, fkeys; if (this.previousDBSegment) { refDBSeg = this.previousDBSegment; fkeys = this.getTo().joinproperties; } else if (this.nextDBSegment) { refDBSeg = this.nextDBSegment; fkeys = refDBSeg.getFrom().joinproperties; } else { // error } let a = (alias ? alias : this._Alias); fkeys.forEach(function (fkeyName, i) { ret.push({ key: new typedObjects.Property(fkeyName, a, refDBSeg._KeyValues[i].dbType), // OK value: refDBSeg._KeyValues[i].value }); }); return ret; }; DbSegment.prototype.getOverPropertiesWithValues = function () { let ret = [], dbSeg, refDBSeg; //var a = (alias ? alias : this._Alias); if (this.previousDBSegment) { refDBSeg = this.previousDBSegment; dbSeg = this; } else if (this.nextDBSegment) { refDBSeg = this.nextDBSegment; dbSeg = this; } let over = this.getOver(); let keyProp1 = over.principal; keyProp1.forEach(function (fkeyName, i) { ret.push({ key: new typedObjects.Property(fkeyName, null, refDBSeg._KeyValues[i].dbType), // OK value: refDBSeg._KeyValues[i].value }); }); let keyProp2 = over.dependent; keyProp2.forEach(function (fKeyName, i) { ret.push({ key: new typedObjects.Property(fKeyName, null, dbSeg._KeyValues[i].dbType), // OK value: dbSeg._KeyValues[i].value }); }); return ret; }; /* DbSegment.prototype.dump = function (logger, addText) { let text = addText || 'DbSegment: '; let filter = function (key, value) { if (key === '__metadata' || key === 'previousDBSegment') { return undefined; } return value; }; logger.debug(text + JSON.stringify(this, filter, 4)); }; */ /** * Add a navigationDbSegment to the expand tree * * @param navPropName Name of the expanded navigation property * @param navigationDbSegment Corresponding dbSegment */ DbSegment.prototype.addExpandDbSegment = function (navPropName, navigationDbSegment) { if (this.entityType.navPropertiesMap[navPropName]) { navigationDbSegment.isExpand = true; this._ExpandedNavigations.push(navPropName); this._ExpandedNavigationsDBSeg[navPropName] = navigationDbSegment; this._relevantNavigationSegments[navPropName] = navigationDbSegment; } }; DbSegment.prototype.addRelevantNavigationSegment = function (navPropName, dbSeg) { if (this.entityType.navPropertiesMap[navPropName]) { this._relevantNavigationSegments[navPropName] = dbSeg; } }; DbSegment.prototype.getRelevantNavigationSegments = fu