
META-INF.dirigible.db.v4.dao.js Maven / Gradle / Ivy
/*
* Copyright (c) 2022 SAP SE or an SAP affiliate company and Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: 2022 SAP SE or an SAP affiliate company and Eclipse Dirigible contributors
* SPDX-License-Identifier: EPL-2.0
*/
"use strict";
const database = require("db/v4/database");
var globals = require("core/v4/globals");
var DAO = exports.DAO = function(orm, logCtxName, dataSourceName, databaseType){
if(orm === undefined)
throw Error('Illegal argument: orm['+ orm + ']');
this.orm = require("db/v4/orm").get(orm);
const sequences = require("db/v4/sequence");
this.sequenceName = this.orm.table+'_'+this.orm.getPrimaryKey().name.toUpperCase();
this.dropIdGenerator = function(){
return sequences.drop(this.sequenceName, databaseType, dataSourceName);
};
this.generateId = function(){
return sequences.nextval(this.sequenceName, databaseType, dataSourceName);
};
const conn = database.getConnection(databaseType, dataSourceName);
try{
this.ormstatements = require('db/v4/ormstatements').create(this.orm, conn);
} finally {
conn.close();
}
//setup loggerName
let loggerName = logCtxName;
if(!loggerName){
loggerName = 'db.dao';
if(this.orm.table)
loggerName = 'db.dao.'+(this.orm.table.toLowerCase());
}
this.$log = require('log/v4/logging').getLogger(loggerName);
const execQuery = require('db/v4/query');
const execUpdate = require('db/v4/update');
this.execute = function(sqlBuilder, parameterBindings){
const sql = sqlBuilder.build();
if(sql === undefined || sql.length<1)
throw Error("Illegal argument: sql from statement builder is invalid["+sql+"]");
this.$log.trace('Executing SQL Statement: {}', sql);
const parameters = sqlBuilder.parameters && sqlBuilder.parameters();
let _parameterBindings;
if(parameters && parameters.length>0){
_parameterBindings = [];
for(var i = 0; i< parameters.length; i++){
var val;
if(parameterBindings){
if(Array.isArray(parameterBindings)){
val = parameterBindings[i];
} else {
val = parameterBindings[parameters[i].name];
}
}
if((val=== null || val===undefined) && sql.toLowerCase().startsWith('select')){
continue;
}
const index = i + 1;
this.$log.trace('Binding to parameter[{}]: {}', index, val);
_parameterBindings.push({
"type": parameters[i].type,
"value": parseValue(parameters[i].type, val)
});
}
}
let result;
if(sql.toLowerCase().startsWith('select')){
result = execQuery.execute(sql, _parameterBindings, databaseType, dataSourceName);
} else {
result = execUpdate.execute(sql, _parameterBindings, databaseType, dataSourceName);
}
return result !== null ? result : [];
};
function parseValue(type, value) {
switch (type.toUpperCase()) {
case 'INTEGER':
return parseInt(value);
case 'DOUBLE':
case 'FLOAT':
return parseFloat(value);
case 'BOOLEAN':
return value ? 'true' : 'false';
default:
return value;
}
}
};
DAO.prototype.notify = function(event){
const func = this[event];
if(!this[event])
return;
if(typeof func !== 'function')
throw Error('Illegal argument. Not a function: ' + func);
const args = [].slice.call(arguments);
func.apply(this, args.slice(1));
};
//Prepare a JSON object for insert into DB
DAO.prototype.createSQLEntity = function(entity) {
let i;
const persistentItem = {};
const mandatories = this.orm.getMandatoryProperties();
for(i = 0; i0){
properties = properties.filter(function(prop){
return entityPropertyNames.indexOf(prop.name)>-1;
});
}
for(let i=0; i-1) || mandatories[i].type.toUpperCase() === 'BOOLEAN')
continue;
const propValue = entity[propName];
if(propValue === undefined || propValue === null){
throw new Error('Illegal ' + propName + ' attribute value in '+this.orm.table+' entity: ' + propValue);
}
}
};
DAO.prototype.insert = function(_entity){
const ids = [];
let entities = _entity;
if(_entity.constructor !== Array){
entities = [_entity];
}
this.$log.trace('Inserting {} {}', this.orm.table, (entities.length===1?'entity':'entities'));
for(let i=0; i0)
throw Error('Unique constraint violation for ' + prop.name + '['+entity[prop.name]+']');
}
const dbEntity = this.createSQLEntity(entity);
try {
const parametericStatement = this.ormstatements.insert.apply(this.ormstatements);
if (this.orm.isAutoIncrementPrimaryKey()) {
const id = this.generateId();
dbEntity[this.orm.getPrimaryKey().name] = id;
}
const updatedRecordCount = this.execute(parametericStatement, dbEntity);
this.notify('afterInsert', dbEntity);
this.notify('beforeInsertAssociationSets', dbEntity);
if(updatedRecordCount > 0 && this.orm.associations && Object.keys(this.orm.associations).length){
//Insert dependencies if any are provided inline with this entity
this.$log.trace('Inserting association sets for {}[{}]', this.orm.table, dbEntity[this.orm.getPrimaryKey().name]);
for(const idx in Object.keys(this.orm.associations)){
const association = this.orm.associations[idx];
const associationName = association['name'];
if([this.orm.ASSOCIATION_TYPES['MANY-TO-MANY'], this.orm.ASSOCIATION_TYPES['MANY-TO-ONE']].indexOf(association.type)<0){
if(entity[associationName] && entity[associationName].length>0){
const associationDaoFactoryFunc = association.targetDao || this;
if(associationDaoFactoryFunc.constructor !== Function)
throw Error('Invalid ORM: Association ' + associationName + ' dao property is expected to be function. Instead, it is: ' + (typeof associationDaoFactoryFunc))
const associationDAO = associationDaoFactoryFunc.apply(this);
this.notify('beforeInsertAssociationSet', entity[associationName], entity);
this.$log.trace('Inserting {} inline entities into association set {}', entity[associationName].length, associationName);
for(let j=0; j0){
ids.push(dbEntity[this.orm.getPrimaryKey().name]);
this.$log.trace('{}[] entity inserted', this.orm.table, dbEntity[this.orm.getPrimaryKey().name]);
} else {
this.$log.trace('No changes incurred in {}', this.orm.table);
}
} catch(e) {
this.$log.error("Inserting {} {} failed", e, this.orm.table, (entities.length===1?'entity':'entities'));
this.$log.trace('Rolling back changes after failed {}[{}] insert. ', this.orm.table, dbEntity[this.orm.getPrimaryKey().name]);
if(dbEntity[this.orm.getPrimaryKey().name]){
try{
this.remove(dbEntity[this.orm.getPrimaryKey().name]);
} catch(err) {
this.$log.error('Could not rollback changes after failed {}[{}}] insert. ', err, this.orm.table, dbEntity[this.orm.getPrimaryKey().name]);
}
}
throw e;
}
}
if(_entity.constructor!== Array)
return ids[0];
else
return ids;
};
// update entity from a JSON object. Returns the id of the updated entity.
DAO.prototype.update = function(entity) {
this.$log.trace('Updating {}[{}] entity', this.orm.table, entity!==undefined?entity[this.orm.getPrimaryKey().name]:entity);
if(entity === undefined || entity === null){
throw new Error('Illegal argument: entity is ' + entity);
}
const ignoredProperties = this.orm.getMandatoryProperties()
.filter(function (property) {
return property.allowedOps && property.allowedOps.indexOf('update') < 0;
})
.map(function (property) {
return property.name;
});
this.validateEntity(entity, ignoredProperties);
const parametericStatement = this.ormstatements.update.apply(this.ormstatements, [entity]);
const dbEntity = this.createSQLEntity(entity);
try {
this.notify('beforeUpdateEntity', dbEntity);
const updatedRecordsCount = this.execute(parametericStatement, dbEntity);
if(updatedRecordsCount > 0)
this.$log.trace('{}[{}] entity updated', this.orm.table, dbEntity[this.orm.getPrimaryKey().name]);
else
this.$log.trace('No changes incurred in {}', this.orm.table);
return this;
} catch(e) {
this.$log.error('Updating {}[{}] failed', e, this.orm.table, entity!==undefined?entity[this.orm.getPrimaryKey().name]:entity);
throw e;
}
};
// delete entity by id, or array of ids, or delete all (if not argument is provided).
DAO.prototype.remove = function() {
let ids = [];
if(arguments.length===0){
ids = this.list({
"$select": [this.orm.getPrimaryKey().name]
}).map(function(ent){
return ent[this.orm.getPrimaryKey().name];
}.bind(this));
} else {
if(arguments[0].constructor !== Array){
ids = [arguments[0]];
} else {
ids = arguments[0];
}
}
this.$log.trace('Deleting '+this.orm.table+((ids!==undefined && ids.length===1)?'['+ids[0]+'] entity': ids.length+' entities'));
for(let i=0; i1)
this.$log.trace('Deleting {}[{}] entity', this.orm.table, id);
if(id === undefined || id === null){
throw new Error('Illegal argument for id parameter:' + id);
}
try {
this.notify('beforeRemoveEntity', id);
//first we attempt to remove depndents if any
if(this.orm.associations){
//Remove associated dependencies
for(const idx in Object.keys(this.orm.associations)){
const association = this.orm.associations[idx];
const associationName = association['name'];
if([this.orm.ASSOCIATION_TYPES['MANY-TO-MANY'], this.orm.ASSOCIATION_TYPES['MANY-TO-ONE']].indexOf(association.type)<0){
this.$log.trace("Inspecting {}[{}}] entity's dependency '{}' for entities to delete.", this.orm.table, id, associationName);
const associationDAO = association.targetDao ? association.targetDao() : this;
const settings = {};
let joinId = id;
//check if we are joining on field, other than pk
if(association.key!==undefined){
var ctxEntity = this.find(id);
joinId = ctxEntity[association.key];
}
settings[association.joinKey] = joinId;
let associatedEntities;
//associatedEntities = this.expand(associationName, id);
associatedEntities = associationDAO.list(settings);
if(associatedEntities && associatedEntities.length > 0){
this.$log.trace("Deleting {}[{}] entity's {} dependent {}", this.orm.table, id, associatedEntities.length, associationName);
this.notify('beforeRemoveAssociationSet', associatedEntities, id);
for(let j=0; j1?'entities':'entity');
}
}
}
}
//Delete by primary key value
const parametericStatement = this.ormstatements["delete"].apply(this.ormstatements, [this.orm.getPrimaryKey().name]);
let params = {};
params[this.orm.getPrimaryKey().name] = id;
const updatedRecordsCount = this.execute(parametericStatement, params);
if(updatedRecordsCount>0)
this.$log.trace('{}[{}] entity deleted', this.orm.table, id);
else
this.$log.trace('No changes incurred in {}', this.orm.table);
} catch(e) {
this.$log.error('Deleting {}[{}] entity failed', e, this.orm.table, id);
throw e;
}
}
};
DAO.prototype.expand = function(expansionPath, context){
let i;
let settings;
let key;
let joinId;
this.$log.trace('Expanding for association path {} and context entity {}', expansionPath, (typeof arguments[1] !== 'object' ? 'id ': '') + JSON.stringify(arguments[1]));
if(!expansionPath || !expansionPath.length){
throw new Error('Illegal argument: expansionPath['+expansionPath+']');
}
if(!context){
throw new Error('Illegal argument: context['+context+']');
}
const associationName = expansionPath.splice ? expansionPath.splice(0, 1)[0] : expansionPath;
const association = this.orm.getAssociation(associationName);
if(!associationName || !association)
throw new Error('Illegal argument: Unknown association for this DAO [' + associationName + ']');
const joinKey = association.joinKey;
let contextEntity;
if(context[this.orm.getPrimaryKey().name] !== undefined){
contextEntity = context;
} else {
contextEntity = this.find(context);
}
if(!contextEntity){
throw Error('No record found for context entity ['+context+']');
}
const associationTargetDAO = association.targetDao ? association.targetDao.apply(this) : this;
if(!associationTargetDAO)
throw Error('No target association DAO instance available for association '+associationName);
let expansion;
let associationEntities = [];
if(association.type===this.orm.ASSOCIATION_TYPES['ONE-TO-ONE'] || association.type===this.orm.ASSOCIATION_TYPES['MANY-TO-ONE']){
joinId = contextEntity[joinKey];
this.$log.trace('Expanding association type {} on {}[{}]', association.type, joinKey, joinId);
if(!association.key || association.key === associationTargetDAO.orm.getPrimaryKey().name)
expansion = associationTargetDAO.find.apply(associationTargetDAO, [joinId]);
else {
let listSettings = {};
listSettings["$filter"] = association.key;
listSettings[association.key] = joinId;
expansion = associationTargetDAO.list.apply(associationTargetDAO, [listSettings])[0];
}
if(expansionPath.length>0){
this.expand(expansionPath, expansion);
}
} else if(association.type===this.orm.ASSOCIATION_TYPES['ONE-TO-MANY']){
settings = {};
if(association.defaults)
settings = association.defaults;
key = association.key || this.orm.getPrimaryKey().name;
joinId = contextEntity[key];
this.$log.trace('Expanding association type {} on {}[{}]', association.type, joinKey, joinId);
settings[joinKey] = joinId;
associationEntities = associationEntities.concat(associationTargetDAO.list.apply(associationTargetDAO, [settings]));
if(expansionPath.length>0){
for(i = 0; i0){
for(i = 0; i-1){
entity[associationName] = this.expand([associationName], entity);
}
}
}
} else {
this.$log.trace('{}[{}] entity not found', this.orm.table, id);
}
}
return entity;
} catch(e) {
this.$log.error("Finding {}[{}] entitiy failed.", e, this.orm.table, id);
throw e;
}
};
DAO.prototype.count = function() {
const parametericStatement = this.ormstatements.count.apply(this.ormstatements);
this.$log.trace('Counting '+this.orm.table+' entities');
let count = 0;
try {
const rs = this.execute(parametericStatement);
if (rs.length>0) {
//expectaion is that there is a single object in the result set with a single porperty
const key = Object.keys(rs[0])[0];
count = parseInt(rs[0][key], 10);
}
} catch(e) {
this.$log.error('Counting {} entities failed', e, this.orm.table);
e.errContext = parametericStatement.toString();
throw e;
}
this.$log.trace('{} {} entities counted', String(count), this.orm.table);
return count;
};
/*
* list parameters:
* - $expand
* - $filter
* - $select
* - $sort
* - $order
* - $limit
* - $offset
*/
DAO.prototype.list = function(settings) {
let key;
settings = settings || {};
const expand = settings.$expand || settings.expand;
if(expand!==undefined){
if(expand.constructor !== Array){
if(expand.constructor === String){
if(expand.indexOf(',')>-1){
settings.$expand = expand.split(',').map(function(exp){
if(exp.constructor !== String)
throw Error('Illegal argument: expand array components are expected ot be strings but found ' + (typeof exp));
return exp.trim();
});
} else {
settings.$expand = [expand];
}
} else {
throw Error('Illegal argument: expand is expected to be string or array of strings but was ' + (typeof expand));
}
}
}
const select = settings.$select || settings.select;
if(select!==undefined){
if(select.constructor !== Array){
if(select.constructor === String){
if(select.indexOf(',')>-1){
settings.$select = select.split(',').map(function(exp){
if(exp.constructor !== String)
throw Error('Illegal argument: select array components are expected ot be strings but found ' + (typeof exp));
return exp.trim();
});
} else {
settings.$select = [select];
}
} else {
throw Error('Illegal argument: select is expected to be string or array of strings but was ' + (typeof expand));
}
}
}
const listArgs = [];
for(key in settings){
listArgs.push(' ' + key + '[' + settings[key] + ']');
}
this.$log.trace('Listing {} entity collection with list operators: {}', this.orm.table, listArgs.join(','));
if(settings.$select!==undefined && expand!==undefined){
settings.$select.push(this.orm.getPrimaryKey().name);
}
//simplistic filtering of (only) string properties with like
if(settings.$filter){
if(settings.$filter.indexOf(',')>-1){
settings.$filter = settings.$filter.split(',');
} else {
settings.$filter = [settings.$filter];
}
settings.$filter = settings.$filter.filter(function(filterField){
const prop = this.ormstatements.orm.getProperty(filterField);
if(prop===undefined || prop.type.toUpperCase()!=='VARCHAR' || settings[prop.name]===undefined)
return false;
settings[prop.name] = '%' + settings[prop.name] + '%';
return true;
}.bind(this));
}
var parametericStatement = this.ormstatements.list.apply(this.ormstatements, [settings]);
//cleanup filtering value expressions if any and convert to Number
for(let key in settings){
const s = settings[key];
if(String(s).startsWith('>') || String(s).startsWith('<'))//TODO: improve
settings[key] = s.substring(1,s.length).trim();
const p = this.orm.getProperty(key);
if(p && p.type!=='VARCHAR' && !isNaN(s)){
settings[key] = +s;
}
}
try {
let entities = [];
const resultSet = this.execute(parametericStatement, settings);
resultSet.forEach(function(rsEntry){
var entity = this.createEntity(rsEntry, settings.$select);
if(expand){
var associationNames = this.orm.getAssociationNames();
for(var idx = 0; idx < associationNames.length; idx++){
var associationName = associationNames[idx];
if(expand.indexOf(associationName)>-1){
entity[associationName] = this.expand([associationName], entity);
}
}
}
this.notify('afterFound', entity, settings);
entities.push(entity);
}.bind(this));
this.$log.trace('{} {} entities found', entities.length, this.orm.table);
return entities;
} catch(e) {
this.$log.error("Listing {} entities failed.", e, this.orm.table);
throw e;
}
};
DAO.prototype.existsTable = function() {
this.$log.trace('Check exists table ' + this.orm.table);
try {
const parametericStatement = this.ormstatements.count.apply(this.ormstatements);
const rs = this.execute(parametericStatement);
return rs.length > 0;
} catch(e) {
return false;
}
};
DAO.prototype.createTable = function() {
this.$log.trace('Creating table {}', this.orm.table);
const parametericStatement = this.ormstatements.createTable.apply(this.ormstatements);
try {
this.execute(parametericStatement);
this.$log.trace('{} table created', this.orm.table);
return this;
} catch(e) {
this.$log.error("Create table {} failed", e, this.orm.table);
throw e;
}
};
DAO.prototype.dropTable = function(dropIdSequence) {
this.$log.trace('Dropping table {}.', this.orm.table);
const parametericStatement = this.ormstatements.dropTable.apply(this.ormstatements);
try {
this.execute(parametericStatement);
this.$log.trace('Table {} dropped.', this.orm.table);
} catch(e) {
this.$log.error("Dropping table {} failed.", e, this.orm.table);
throw e;
}
if(dropIdSequence){
this.$log.trace('Dropping table {} sequence {}.', this.orm.table, this.sequenceName);
try{
this.dropIdGenerator();
this.$log.trace('Table {} sequence {} dropped.', this.orm.table, this.sequenceName);
} catch(e) {
this.$log.error("Dropping table {} sequence {} failed.", e, this.orm.table, this.sequenceName);
throw e;
}
}
return this;
};
//var toCamelCase = function(str){
// return str.toLowerCase().replace(/(?:_| |\b)(\w)/g, function(str, p1, offset) {
// return offset===0 ? p1 : p1.toUpperCase();
// });
//};
//var fromTableDef = exports.ormFromTable = function(tableDef){
// var orm = {};
// orm["table"] = tableDef["name"];
// if(tableDef["columns"]){
// orm["properties"] = tableDef["columns"].map(function(columnDef, idx, arr){
// var property = {
// "name": toCamelCase(columnDef["name"]),
// "column": columnDef["name"],
// "type": columnDef["type"],
// "size": columnDef["length"] !== undefined && columnDef["length"] !=="0"? parseInt(columnDef["length"], 10) : undefined,
// "id": columnDef["primaryKey"]==='true',
// "required": columnDef["nullable"] !== 'true'
// };
// if(tableDef.constraints && tableDef.constraints.uniqueIndices && tableDef.constraints.uniqueIndices.columns && tableDef.constraints.uniqueIndices.columns.indexOf(columnDef['name'])>-1){
// property["unique"] = true;
// }
// return property;
// });
// }
// return orm;
//};
/**
* oDefinition can be table definition or standard orm definition object. Or it can be a valid path to
* a .table file, or any other text file contianing a standard dao orm definition.
*/
exports.create = exports.dao = function(oDefinition, logCtxName, dataSourceName, databaseType){
let orm;
// if(typeof oDefinition === 'string'){
// var files = require('io/v4/files');
// if(files.isReadable(oDefinition)){
// var defText = files.readText(oDefinition);
// try{
// oDefinition = JSON.parse(defText);
// } catch (parseError){
// var logger = require('log/logging').getLogger("db.dao");
// logger.error("Invalid JSON in {}", parseError, oDefinition);
// throw parseError;
// }
// } else {
// throw Error('Cannot get dao definition from ' + oDefinition + '. Check path and read permissions.');
// }
// }
//
// if(oDefinition["name"] && oDefinition["type"] && ["TABLE","VIEW"].indexOf(oDefinition["type"])>-1){
// orm = fromTableDef(oDefinition);
// } else {
orm = oDefinition;
let productName = globals.get(databaseType + "_" + dataSourceName);
if (!productName) {
productName = database.getProductName(databaseType, dataSourceName);
globals.set(databaseType + "_" + dataSourceName, productName);
}
let isCaseSensitive = require("core/v4/configurations").get("DIRIGIBLE_DATABASE_NAMES_CASE_SENSITIVE");
if (!isCaseSensitive && productName === "PostgreSQL") {
orm["properties"].map(function(property) {
property.column = property.column.toLowerCase();
});
}
// }
return new DAO(orm, logCtxName, dataSourceName, databaseType);
};
//TODO: ability to defien easily associations on daos created from .table definitions.
//TODO: factory function for generating a set of related DAOs from an ER underlying model defined in .table models.
© 2015 - 2025 Weber Informatics LLC | Privacy Policy