UNPKG

@sap/xsodata

Version:

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

1,568 lines (1,389 loc) 60.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; const isSpecialCharacter = ( createBy, createAt, modifyBy, modifyAt, property ) => { return ( (createBy && property === 'CREATED_BY') || (createAt && property === 'CREATED_AT') || (modifyBy && property === 'MODIFIED_BY') || (modifyAt && property === 'MODIFIED_AT') ); }; /** * 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 (const keyValue of this._KeyValues) { if (keyValue.name === key) { return keyValue; } } return undefined; }; DbSegment.prototype.hasKeyValues = function () { return this._KeyValues.length > 0; }; DbSegment.prototype._isParamViaKey = function () { return ( this.entityType._entityType.parameters && this.entityType._entityType.parameters.viaKey === true ); }; DbSegment.prototype._getDBValue = function ( value, key, prop, context, supportNullable, converterArray, entityTypes ) { 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) { const createBy = entityTypes[0], createAt = entityTypes[1], modifyBy = entityTypes[2], modifyAt = entityTypes[3]; if ( !isSpecialCharacter(createBy, createAt, modifyBy, modifyAt, key) ) { throw new BadRequest( 'The serialized resource has a missing value for member ' + key ); } } dbValue = null; } else { if (this._isParamViaKey()) { 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); } return dbValue; }; /** * 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 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. for (let [key, prop] of Object.entries(this.entityType.propertiesMap)) { //Load property from record const value = record[key]; let dbValue = this._getDBValue( value, key, prop, context, supportNullable, converterArray, [createBy, createAt, modifyBy, modifyAt] ); 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 ( isSpecialCharacter( createBy, createAt, modifyBy, modifyAt, property ) || // 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 (const element of this.entityType.keys.names) { ret.push(new sqlStatement.TableColumn(this._Alias, element)); } 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 (const element of this._InputParams) { let keyName = element.key; if (!this.entityType.inputParameters[keyName]) { throw new InternalError('Inputparameter ' + keyName + ' unknown'); } } }; DbSegment.prototype.validateKeyParameters = function () { const keyNames = this.entityType.keyNamesOrdered; for (const element of keyNames) { const property = this.entityType.propertiesMap[element]; if (property.KIND !== EntityType.entityKind.inputParameters) { if (!this._InputParamsMap.has(element)) { throw new InternalError( 'Missing value for input parameter ' + element + '.' ); } } } }; /** * 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 (const prop of this._aliasedKeyPropertiesOnCalcView) { if (prop.property === propertyName) { return prop; } } 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 property; let j = -1; let keyNames = this.entityType.keyNamesOrdered; for (const keyName of keyNames) { 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 (const keyName of keyNames) { 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._getSortedSelectProperties(); for (const prop of properties) { let property = this.entityType.propertiesMap[prop]; 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 (const property of properties) { ret.push(createSqlCreateProperty(property, property._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' || typeString === 'NVARCHAR' || typeString === 'ALPHANUM' || typeString === 'CHAR' || typeString === 'NCHAR' || typeString === 'VARBINARY' ) { checkLength(property); typeString += '(' + property.LENGTH.toString() + ')'; } 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 (const prop of properties) { property = this.entityType.propertiesMap[prop]; selectProperty = null; if (property.KIND === EntityType.entityKind.inputParameters) { let paramValue = this.getParameterValue(property.COLUMN_NAME); selectProperty = new sqlStatement.ParameterSelectProperty( noTable ? null : this._Alias, prop, 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, prop, 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 (const prop of properties) { property = this.entityType.propertiesMap[prop]; selectProperty = null; if (property.KIND === EntityType.entityKind.inputParameters) { let paramValue = this.getParameterValue(property.COLUMN_NAME); selectProperty = new sqlStatement.ParameterSelectProperty( noTable ? null : this._Alias, prop, null, paramValue ); } else { propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; selectProperty = new sqlStatement.SelectProperty( noTable ? null : this._Alias, prop, 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 (const prop of properties) { const property = this.entityType.propertiesMap[prop]; // this include property KIND === inputParameters const propertyType = property.aggregate ? null : property.DATA_TYPE_NAME; ret.push( new sqlStatement.SelectProperty( noTable ? null : this._Alias, prop, 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 (const property of over.principal) { properties.push(property); } for (const property of over.dependent) { properties.push(property); } for (const property of properties) { selectProperty = new sqlStatement.SelectProperty(rId, property, 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._SelectedNavigations; for (const navPropertyName of properties) { let navDbSeg = this._relevantNavigationSegments[navPropertyName]; if (!navDbSeg) { continue; } let propertyNames = navDbSeg.getFrom().joinproperties; for (const prop of propertyNames) { let property = this.entityType.propertiesMap[prop]; 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._SelectedNavigations; for (const navPropertyName of properties) { if (this._getSortedSelectProperties().indexOf(navPropertyName) > -1) { continue; } let navDbSeg = this._relevantNavigationSegments[navPropertyName]; if (!navDbSeg) { continue; } let propertyNames = navDbSeg.getFrom().joinproperties; for (const prop of propertyNames) { let property = this.entityType.propertiesMap[prop]; let propertyName = property.COLUMN_NAME; if (this._getSortedSelectProperties().indexOf(propertyName) > -1) { continue; } ret.push( new sqlStatement.SelectProperty( noTable ? null : this._Alias, prop, 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 (const expandedNavigation of this._ExpandedNavigations) { let exp = this._relevantNavigationSegments[expandedNavigation]; let from = exp._end.from; for (const joinProperty of from.joinproperties) { //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 = []; for (const propertyName of this._getSortedSelectProperties()) { 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 property; let propertyType; for (const propertyName of this._getSortedSelectProperties()) { 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 (const keyValue of this._KeyValues) { let etProperty = this.entityType.propertiesMap[keyValue.name]; if (etProperty.KIND !== EntityType.entityKind.inputParameters) { ret.push({ key: new typedObjects.Property( keyValue.name, alias ? alias : this._Alias, etProperty.DATA_TYPE_NAME ), value: keyValue.value, }); } } return ret; }; DbSegment.prototype.getQKeyWithValuesDB = function (alias) { let ret = []; for (const keyValue of this._KeyValues) { ret.push({ key: new typedObjects.Property( keyValue.name, alias ? alias : this._Alias, keyValue.dbType ), //OK value: new typ