All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.http.v4.rs-data.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
 */
/**
 * Deep merge for JS objects (typeof o === 'object' is true).
 * Array members are copied as is without changes. That means that array in target object will overwrite a coresponding memeber with the same name in source
 */
/*var merge = function(target, source) {
    Object.keys(source).forEach(function(key) {
        if (source[key]){
			if(typeof source[key] === 'object' && !Array.isArray(source[key])) {
            	return merge(target[key] = target[key] || {}, source[key]);
        	}
        }
        target[key] = source[key];
    });
};
*/

var DataProtocolDefinition = function(){
	const rs = require('http/v4/rs');
	const mappings = this.mappings = new rs.ResourceMappings();

	mappings.collectionResource = mappings.resource("");
	mappings.entityResource = mappings.resource("{id}");

	//entity collection functions
	const _query = mappings.collectionResource.get().produces(['application/json']);
	mappings.query = function(){
		return _query;
	};
	const _create = mappings.collectionResource.post().consumes(['*/json']);
	mappings.create = function(){
		return _create;
	};
	const _count = mappings.resource("count").get().produces(['application/json']);
	mappings.count = function(){
		return _count;
	};
	//entity functions
	const _get = mappings.entityResource.get().produces(['application/json']);
	mappings.get = function(){
		return _get;
	};
	const _update = mappings.entityResource.put().consumes(['*/json']);
	mappings.update = function(){
		return _update;
	};
	const _remove = mappings.entityResource.remove();
	mappings.remove = function(){
		return _remove;
	};
	//association functions
	const _associationList = mappings.resource("{id}/{associationName}").get().produces(['application/json']);
	mappings.associationList = function(){
		return _associationList;
	};
	const _associationCreate = mappings.resource("{id}/{associationName}").post().consumes(['*/json']);
	mappings.associationCreate = function(){
		return _associationCreate;
	};
	//api functions
	const _metadata = mappings.resource("metadata").get().produces(['application/json']);
	mappings.metadata = function(){
		return _metadata;
	};

	//TODO: automate finding resource config by name and make it applicable beyond the well known mehtod names
	mappings.disableByName = function(){
		for(let i=0; i< arguments.length; i++){
			if(arguments[i] === "query"){
				this.disable("", "get", undefined, ['application/json']);
			}
			if(arguments[i] === "get"){
				this.disable("{id}", "get", undefined, ['application/json']);
			}
			if(arguments[i] === "count"){
				this.disable("count", "get", undefined, ['application/json']);
			}
			if(arguments[i] === "metadata"){
				this.disable("metadata", "get", undefined, ['application/json']);
			}
			if(arguments[i] === "create"){
				this.disable("", "post", ['application/json']);
			}
			if(arguments[i] === "update"){
				this.disable("{id}", "post", ['application/json']);
			}
			if(arguments[i] === "delete" || arguments[i] === "remove"){
				this.disable("{id}", "delete");
			}
		}
		return this;
	}.bind(this);

	return this;
};

const ProtocolHandlerAdapter = function (oDataProtocolMappings) {

	this.logger = require('log/logging').getLogger('rs.data.dao.provider.default');

	const protocolDef = oDataProtocolMappings || new DataProtocolDefinition().mappings;
	const _self = this;

	const parseIntStrict = function (value) {
		if (/^(\-|\+)?([0-9]+|Infinity)$/.test(value))
			return Number(value);
		return NaN;
	};

	this.adapt = function () {
		const protocolFunctionNames = ["query", "create", "update", "remove", "get", "count", "metadata", "associationList", "associationCreate"];
		for (let i = 0; i < protocolFunctionNames.length; i++) {
			const functionName = protocolFunctionNames[i];
			if (protocolDef[functionName]) {
				var resourceVerbHandlerDef;
				if (typeof protocolDef[functionName] === 'function')
					resourceVerbHandlerDef = protocolDef[functionName]();
				else
					resourceVerbHandlerDef = protocolDef[functionName];
				_self[functionName].call(_self, resourceVerbHandlerDef, this);
			}
		}
		return protocolDef;
	};

	const daos = require('db/v4/dao');
	//functions deifned on the api prototype will be weaved in the using class
	this.api = function () {
		this.dao = function (orm, loggerName) {
			//check if accessor requested
			if (arguments.length < 1) {
				return this._dao;
			}
			if (arguments[0] instanceof daos.DAO)
				this._dao = arguments[0];
			else
				this._dao = daos.create(orm, loggerName);
			return this;
		};
	};

	const notify = function (event) {
		const func = this[event];
		if (!this[event])
			return;
		if (typeof func !== 'function')
			throw Error('Illegal argument. Not a function: ' + func);
		var args = [].slice.call(arguments);
		return func.apply(this, args.slice(1));
	};

	const throwBadRequestError = function (context, errorName, errorCode, errorMessage, error) {
		context.suppressStack = true;
		context.httpErrorCode = 400;
		context.errorMessage = errorMessage;
		context.errorName = errorName;
		context.errorCode = errorCode;
		//re-throw or construct new
		throw (error || Error(errorMessage));
	};

	const installCallbackInVerbHandlerConfig = function (oResourceVerbHandler, sCbName) {
		if (!oResourceVerbHandler[sCbName])
			oResourceVerbHandler[sCbName] = function (fCb) {
				oResourceVerbHandler.configuration()[sCbName] = fCb;
				return this;
			};
	};

	this.create = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			let entity;
			try {
				entity = request.getJSON();
			} catch (err) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid JSON in create entity request payload", err);
			}
			notify.call(this, 'onEntityInsert', entity, context);
			if (typeof handlerDef["onEntityInsert"] === 'function')
				handlerDef["onEntityInsert"].call(_this, entity, context);

			const ids = this._dao.insert(entity, context.queryParameters.$cascaded || true);
			notify.call(this, 'onAfterEntityInsert', entity, ids, context);
			if (typeof handlerDef["onAfterEntityInsert"] === 'function')
				handlerDef["onAfterEntityInsert"].call(_this, entity, ids, context);

			if (ids && ids.constructor !== Array) {
				response.setHeader('Location', request.getRequestURL().toString() + '/' + ids);
				response.setStatus(response.NO_CONTENT);
			} else {
				const responseBodyJson = JSON.stringify(ids, null, 2);
				response.println(responseBodyJson);
				response.setContentType(handlerDef.produces[0]);
				response.setStatus(response.OK);
			}
		}.bind(_this));
		//expose specific callback setup methods
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onEntityInsert");
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onAfterEntityInsert");
	};

	this.remove = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			const id = context.pathParameters.id;
			notify.call(this, 'onBeforeRemove', id, context);
			if (typeof handlerDef["onBeforeRemove"] === 'function')
				handlerDef["onBeforeRemove"].call(_this, id, context);
			this._dao.remove(id);
			notify.call(this, 'onAfterRemove', id, context);
			if (typeof handlerDef["onAfterRemove"] === 'function')
				handlerDef["onAfterRemove"].call(_this, id, context);
			response.setStatus(response.NO_CONTENT);
		}.bind(_this));
		//expose specific callback setup methods
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onBeforeRemove");
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onAfterRemove");
	};

	this.update = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			const id = context.pathParameters.id;
			let entity;
			try {
				entity = request.getJSON();
			} catch (err) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid JSON in update request payload", err);
			}
			//check for potential mismatch in path id and id in input
			const entityIdName = this._dao.orm.getPrimaryKey().name;
			if (entity[entityIdName] === null || entity[entityIdName] === undefined)
				throwBadRequestError(context, "Invalid Client Input", undefined, "The JSON entity payload in the request does include a primary key property");
			if (id !== entity[entityIdName])
				throwBadRequestError(context, "Invalid Client Input", undefined, "The id parameter in the request path[" + id + "] and the id in the payload[" + entity[entityIdName] + "] do not match.");
			entity[entityIdName] = id;
			//prevent implicit type convertion
			if (this._dao.orm.getPrimaryKey().type !== 'string')
				entity[entityIdName] = parseInt(entity[entityIdName], 10);

			notify.call(this, 'onEntityUpdate', entity, id);
			if (typeof handlerDef["onEntityUpdate"] === 'function')
				handlerDef["onEntityUpdate"].call(_this, entity, id);
			entity[this._dao.orm.getPrimaryKey()] = this._dao.update(entity);
			response.setStatus(response.NO_CONTENT);
		}.bind(_this));
		//expose specific callback setup methods
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onEntityUpdate");
	};

	this.get = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			const id = context.pathParameters.id;
			//id is mandatory parameter and an integer
			if (id === undefined || isNaN(parseIntStrict(id))) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid id parameter: " + id);
			}
			let $expand = context.queryParameters['$expand'];
			if ($expand) {
				if ($expand === true || $expand.toLowerCase() === '$all') {
					$expand = this._dao.orm.getAssociationNames().join(',');
				} else {
					$expand = String($expand);
					$expand = $expand.split(',').map(function (exp) {
						return exp.trim();
					});
				}
			}
			let $select = context.queryParameters['$select'];
			if ($select) {
				if ($select === true || $select.toLowerCase() === '$all') {
					$select = this._dao.orm.getAssociationNames().join(',');
				} else {
					$select = String($select);
					$select = $select.split(',').map(function (sel) {
						return sel.trim();
					});
				}
			}

			const entity = this._dao.find.apply(this._dao, [id, $expand, $select]);
			notify.call(this, 'onAfterFind', entity, context);
			if (typeof handlerDef["onAfterFind"] === 'function')
				handlerDef["onAfterFind"].call(_this, entity, context);
			if (!entity) {
				_this.logger.error("Record with id: " + id + " does not exist.");
				context.httpErrorCode = response.NOT_FOUND;
				context.suppressStack = true;
				context.errorCode = context.httpErrorCode;
				context.errorName = response.HttpCodesReasons.getReason(context.errorCode);
				throw Error("Record with id: " + id + " does not exist.");
			}
			const jsonResponse = JSON.stringify(entity, null, 2);
			response.setContentType(handlerDef.produces[0]);
			response.println(jsonResponse);
		}.bind(_this));
		//expose specific callback setup methods
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "onAfterFind");
	};

	const validateQueryInputs = this.validateQueryInputs = function (context) {

		let i;
		const limit = context.queryParameters.$limit || context.queryParameters.limit;
		if (limit === undefined || limit === null) {
			//context.queryParameters.limit = 10000;//default constraint
		} else if (isNaN(parseIntStrict(limit)) || limit < 0) {
			throwBadRequestError(context, "Invalid Client Input", undefined, "Invallid limit parameter: " + limit + ". Must be a positive integer.");
			return false;
		}

		const offset = context.queryParameters.$offset || context.queryParameters.offset;
		if (offset === undefined || offset === null) {
			context.queryParameters.offset = 0;
		} else if (isNaN(parseIntStrict(offset)) || offset < 0) {
			throwBadRequestError(context, "Invalid Client Input", undefined, "Invallid offset parameter: " + offset + ". Must be a positive integer.");
			return false;
		}

		let sort = context.queryParameters.$sort || context.queryParameters.sort || null;
		if (sort !== undefined && sort !== null) {
			sort = String(sort);
			const sortPropertyNames = sort.split(',').map(function (srt) {
				return srt.trim();
			});
			for (i = 0; i < sortPropertyNames.length; i++) {
				const prop = this._dao.orm.getProperty(sortPropertyNames[i]);
				if (!prop) {
					throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid $sort by property name: " + sortPropertyNames[i]);
					return false;
				}
			}
			context.queryParameters.$sort = sortPropertyNames;
		}

		const order = context.queryParameters.order || context.queryParameters.$order || null;
		if (order !== null) {
			if (sort === null) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid Client Input", undefined, "Parameter $order is invalid without paramter sort to order by.");
				return false;
			} else if (['asc', 'desc'].indexOf(order.trim().toLowerCase()) < 0) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invallid $order parameter: " + order + ". Must be either ASC or DESC.");
				return false;
			}
		} else if (sort !== null) {
			context.queryParameters.order = 'asc';
		}

		let $expand = context.queryParameters['$expand'];
		if ($expand !== undefined) {
			const associationNames = this._dao.orm.getAssociationNames();
			if ($expand === true || $expand.toLowerCase() === '$all') {
				$expand = associationNames.join(',');
			} else {
				$expand = String($expand);
				$expand = $expand.split(',').map(function (sel) {
					return sel.trim();
				});
				for (i = 0; i < $expand.length; i++) {
					if (associationNames.indexOf($expand[i]) < 0) {
						throwBadRequestError(context, "Invalid Client Input", undefined, 'Invalid expand association name - ' + $expand[i]);
						return false;
					}
				}
			}
			context.queryParameters['$expand'] = $expand;
		}

		let select = context.queryParameters['$select'];
		if (select !== undefined) {
			select = String(select);
			const selectedFieldNames = select.split(',').map(function (sel) {
				return sel.trim();
			});
			for (i = 0; i < selectedFieldNames.length; i++) {
				if (this._dao.orm.getProperty(selectedFieldNames[i]) === undefined) {
					throwBadRequestError(context, "Invalid Client Input", undefined, 'Invalid select property name - ' + selectedFieldNames[i]);
					return false;
				}
			}
			context.queryParameters['$select'] = selectedFieldNames;
		}
		return true;
	};

	this.query = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			if (typeof handlerDef["beforeQuery"] === 'function')
				handlerDef["beforeQuery"].call(_this, context);
			if (!validateQueryInputs.call(this, context, request, response))
				return;
			const args = [context.queryParameters];
			for (const propName in context.queryParameters) {
				const val = context.queryParameters[propName];
				if (val === '$null')
					context.queryParameters[propName] = null;
			}

			const $count = this._dao.count.apply(this._dao) || 0;
			response.addHeader('X-dservice-list-count', String($count));

			let entities;
			if ($count > 0) {
				entities = this._dao.list.apply(this._dao, args) || [];
				if (typeof handlerDef["afterQuery"] === 'function')
					handlerDef["afterQuery"].call(_this, entities, context);
				notify.call(this, 'postQuery', entities, context);
			} else {
				entities = [];
			}

			const jsonResponse = JSON.stringify(entities, null, 2);
			response.setContentType(handlerDef.produces[0]);
			response.println(jsonResponse);
			response.setStatus(response.OK);
		}.bind(_this));
		//expose specific callback setup methods
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "beforeQuery");
		installCallbackInVerbHandlerConfig(oResourceVerbHandler, "afterQuery");
	};

	this.count = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			const entitiesCount = this._dao.count() || 0;
			response.setHeader("Content-Type", "application/json");
			const payload = {
				"count": entitiesCount
			};
			response.println(JSON.stringify(payload, null, 2));
			response.setContentType(handlerDef.produces[0]);
			response.setStatus(response.OK);
		}.bind(_this));
	};

	this.metadata = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response, handlerDef) {
			//TODO: in process
			var entityMetadata = this._dao.orm.orm;
			response.setContentType(handlerDef.produces[0]);
			response.println(JSON.stringify(entityMetadata, null, 2));
			response.setStatus(response.OK)
		}.bind(_this));
	};

	//Associations handlers
	this.associationList = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response) {
			const associationName = context.pathParameters.associationName;
			if (!this._dao.orm.getAssociation(associationName))
				throwBadRequestError(context, undefined, 'Invalid association set name requested: ' + associationName);
			const args = context.queryParameters;
			args[this._dao.orm.getPrimaryKey().name] = context.pathParameters.id;
			const expansionPath = [associationName];//Tmp solution with array of one component until handler can be installed for paths with multiple segments
			const associationSetEntities = this._dao.expand.apply(this._dao, [expansionPath, context.pathParameters.id]) || [];
			response.setStatus(response.OK);
			response.println(JSON.stringify(associationSetEntities, null, 2));
		}.bind(_this));
	};

	this.associationCreate = function (oResourceVerbHandler, _this) {
		oResourceVerbHandler.serve(function (context, request, response) {
			const associationName = context.pathParameters.associationName;
			const associationDef = this._dao.orm.getAssociation(associationName);
			if (!associationDef)
				throwBadRequestError(context, undefined, 'Invalid association set name requested: ' + associationName);
			const associationType = associationDef.type;
			//create works only for one-to-many
			if (this._dao.orm.ASSOCIATION_TYPES['ONE-TO-MANY'] !== associationType) {
				_this.logger.error('Invalid operation \'create\' requested for association set \'' + associationName + '\' with association type ' + associationType + '. Association type must be one-to-many.');
				throwBadRequestError(context, undefined, 'Invalid operation \'create\' requested for association set \'' + associationName + '\' with association type ' + associationType + '. Association type must be one-to-many.');
			}

			const joinKey = associationDef.joinKey;
			if (joinKey === undefined) {
				_this.logger.error('Invalid configuration: missing join key in configuration for association \'' + associationName + '\'.');
				context.suppressStack = true;
				context.httpErrorCode = response.INTERNAL_SERVER_ERROR;
				throw Error('Invalid configuration: missing join key in configuration for association \'' + associationName + '\'.');
			}

			const dependendDao = associationDef.targetDao;
			if (dependendDao === undefined) {
				_this.logger.error('Invalid configuration: missing dao factory in configuraiton for association \'' + associationName + '\'.');
				context.suppressStack = true;
				context.httpErrorCode = response.INTERNAL_SERVER_ERROR;
				throw Error('Invalid configuration: missing dao factory in configuration for association \'' + associationName + '\'.');
			}

			let dependendEntity;
			try {
				dependendEntity = request.getJSON();
			} catch (err) {
				throwBadRequestError(context, "Invalid Client Input", undefined, "Invalid JSON in create association request payload", err);
			}
			if (this.onEntityInsert) {
				this.onEntityInsert(dependendEntity);
			}
			dependendEntity[dependendDao.orm.getPrimaryKey()] = dependendDao.insert(dependendEntity, context.queryParameters.cascaded);
			response.setStatus(response.OK);
			response.setHeader('Location', request.getRequestURL().toString() + '/' + dependendEntity[this._dao.orm.getPrimaryKey()]);
		}.bind(_this));
	};
};

const HttpController = require('http/v4/rs').HttpController;

/**
 * Utility method to setup the prototipical inheritance chain.
 * credits: https://stackoverflow.com/a/4389429/2134990
 */
function extend(base, sub) {
  // Avoid instantiating the base class just to setup inheritance
  // Also, do a recursive merge of two prototypes, so we don't overwrite
  // the existing prototype, but still maintain the inheritance chain
  // Thanks to @ccnokes
	const origProto = sub.prototype;
	sub.prototype = Object.create(base.prototype);
  for (const key in origProto)  {
     sub.prototype[key] = origProto[key];
  }
  // The constructor property was set wrong, let's fix it
  Object.defineProperty(sub.prototype, 'constructor', {
    enumerable: false,
    value: sub
  });
}

/**
 * Constructs new DataService instances.
 *
 * @constructs DataService
 * @param {Object} [oConfig] initial configuration that will be manipulated for building the protocol API. Defaults to an empty object {}.
 * @param {Object} [oProtocolHandlersAdapter] Defaults to a new ProtocolHandlerAdapter instance
 * @param {Object} [oDataProtocolDefinition]  oDataProtocolDefinition supplies the callback functions for each protocol method (e.g. query). Defaults to a new DataProtocolDefinition instance
 * @param {Object} [sLoggerName] An optional logger name to use with this instance. Defaults to 'http.rs.data.service'
 */
const DataService = function (oConfig, oProtocolHandlersAdapter, oDataProtocolDefinition) {
	let _oProtocolHandlersAdapter = oProtocolHandlersAdapter;
	if (_oProtocolHandlersAdapter === undefined) {
		_oProtocolHandlersAdapter = new ProtocolHandlerAdapter(oDataProtocolDefinition);
	}

	const _mappings = _oProtocolHandlersAdapter.adapt.call(this);

	if (oConfig !== undefined) {
		Object.keys(oConfig).forEach(function (sPath) {
			_mappings.resource(sPath, oConfig[sPath]);
		});
	}

	HttpController.call(this, _mappings);

	/*this.mappings = function(){
		return _mappings;
	};
*/
	//weave in methods from the oProtocolHandlersAdapter that it requires.
	_oProtocolHandlersAdapter.api.call(this);

	let loggerName;
	//use supplied loggername if any or use own
	for (let i = 0; i < arguments.length; i++) {
		if (typeof arguments[i] === 'string') {
			loggerName = arguments[i];
			break;
		}
	}
	loggerName = loggerName || 'http.rs.data.service';
	this.logger = require('log/v4/logging').getLogger(loggerName);

	return this;
};

extend(HttpController, DataService);


/*DataService.prototype.execute = function(oRequest, oResponse) {
	var cfg = this.mappings();
	var rs = require('http/v4/rs');
	var httpSvc = rs.service(cfg);
	return httpSvc.execute(oRequest, oResponse);
};
*/
/**
 * Creates new DataService instances.
 *
 * @param {Object} [oConfig] ] initial REST API configuration. Defaults to an empty object {}.
 * @param oProtocolHandlersAdapter
 * @param {Object} [oDataProtocolDefinition]  oDataProtocolDefinition supplies the callback functions for each protocol method (e.g. query). Defaults to a new DataProtocolDefinition instance
 * @param sLoggerName
 * @returns {DataService}
 */
exports.service = function(oConfig, oProtocolHandlersAdapter, oDataProtocolDefinition, sLoggerName){
	const ds = new DataService(oConfig, oProtocolHandlersAdapter, oDataProtocolDefinition, sLoggerName);
	return ds;
};




© 2015 - 2025 Weber Informatics LLC | Privacy Policy