const DBList = require("./jsDbList");
const DataAccess = require("./jsDataAccess");
const Deferred = require("JQDeferred");
const JsConnectionPool = require('./JsConnectionPool').JsConnectionPool;
const JsPooledConnection = require('./JsConnectionPool').JsPooledConnection;
const Context = require('./jsDbList').Context;
const Environment = require('./jsEnvironment');
const PostData = require('./jsPostData').PostData;
const Express = require('express');
const Path = require("path");
const fs = require("fs");
const checkToken = require("./jsToken").checkToken;
let Identity = require("./jsToken").Identity;
const getIdentityFromRequest= require("./jsToken").getIdentityFromRequest;
let createServicesRoutes = require('./jsExpressApplication').createServicesRoutes;
let createExpressApplication = require('./jsExpressApplication').createExpressApplication;
const tokenConfig = require("./../config/tokenConfig");
const Token = require("./jsToken").Token;
const securityProvider= require("./jsSecurity");
const LocalResource= require("./../client/components/metadata/LocalResource");
const GetMeta= require("./../client/components/metadata/GetMeta");
const GetDataInvoke= require("./../client/components/metadata/GetDataInvoke");
const dsNameSpace = require("./../client/components/metadata/jsDataSet");
const OptimisticLocking = dsNameSpace.OptimisticLocking;
const commonGetDataSet =require("./../client/components/metadata/GetDataSet");
const jsBusinessLogic = require("../src/jsBusinessLogic");
let BusinessPostData =jsBusinessLogic.BusinessPostData;
/**
* Main Application
* @constructor
*/
function JsApplication() {
this.expressApplication = createExpressApplication();
this.expressApplication.locals.JsApplication= this; //every expressApplication is attached to a JsApplication
this.router = Express.Router();
this.pool=null;
/**
* Collection of all user environments of the application, the key is the sessionID
* @type {Object.<string, Environment>}
*/
this.environments={};
/* Security */
this.security=null;
}
JsApplication.prototype = {
constructor: JsApplication,
/**
* Creates the connection pool, it is supposed to be overridden in derived classes
* @param {string} dbCode
* @returns {JsConnectionPool}
*/
createConnectionPool: function(dbCode){
return new JsConnectionPool(dbCode);
},
getApp: function (){
return this.expressApplication;
},
getRouter: function (){
return this.router;
},
//this must return the list of all folders containing routers that do not need authentication
getNoTokenFolders: function(){
return {
"auth":true
};
},
error: function (err,req,res,next){
res.status(401).json({
error: err.stack
});
},
/**
* Attaches a release event on close/finish of the request (the one which fires first)
* releases the pool connection after a request has been processed
* @param req
* @param ctx
*/
releaseConnection: function(req, res, ctx) {
let released=false;
res.on('close', () => {
if (released){
return;
}
released=true;
//let ctx = req.app.locals.context;
ctx.pooledConn.release();
});
res.on('finish', () => {
if (released){
return;
}
released=true;
//let ctx = req.app.locals.context;
ctx.pooledConn.release();
});
//next();
},
createTestSession: function(req,res,next){
let token = req[tokenConfig.options.requestProperty]; //default is auth
if (token) {
return next();
}
let identity= new Identity({
name:"AZZURRO",
title:"AZZURRO"
});
this.getDataAccess()
.then(pooledConn=>{
let conn = pooledConn.getDataAccess();
return this.createSession(identity, conn);
})
.then(environment=>{
let token = new Token(req,identity);
token.setInRequest(req);
next();
});
/// creare il token che simuli l'invio da parte di quella identity
/// valorizzare l'header con il token
},
/**
*
* @param {string} dbCode
* @return {Promise}
*/
init: function (dbCode){
this.dbCode= dbCode;
this.pool = this.createConnectionPool(dbCode);
let noTokenFolders = this.getNoTokenFolders();
let dbInfo = DBList.getDbInfo(dbCode);
if (dbInfo.test){
this.expressApplication.use(this.createTestSession.bind(this));
}
//adds all routers of directory routes
const routes = "routes";
fs.readdirSync(routes)
.filter(fileName => fs.lstatSync(`${routes}/${fileName}`).isDirectory()) //takes all folders
.forEach(folderName => {
if (noTokenFolders[folderName] === undefined){
//check token for any path, with the only exception for those in noTokenFolders
this.router.use("/"+folderName+"/", checkToken.bind(this));
}
//At this point the token exists if it is required. But not necessarily is bound to a valid session
this.router.use("/"+folderName+"/", this.getOrCreateContext.bind(this));
createServicesRoutes(this.router, Path.join("routes",folderName),folderName);
});
this.expressApplication.use(this.router);
this.expressApplication.use(this.error.bind(this));
let connPool;
let def = Deferred();
this.getDataAccess()
.then(_connPool=>{
connPool=_connPool;
return connPool.getDataAccess();
})
.then(conn=>{
this.security= conn.security;
def.resolve();
})
.fail(err=>{
def.reject(err);
});
return def.promise();
},
/**
* returns an open connection to db
* @return {Promise<JsPooledConnection>}
*/
getDataAccess: function (){
return this.pool.getDataAccess().promise();
},
/**
* Meant to be redefined in subclasses, creates an Environment class
* @param {Identity} identity
* @param {DataAccess} conn
* @return {Environment}
*/
createEnvironment: function (identity, conn){
let e = new Environment(identity);
//Sets field for optimistic locking
e.field("lu",identity.name);
e.field("cu",identity.name);
return e;
},
/**
* Create a Session for the user represented by the token.
* @param {Identity} identity
* @param {DataAccess} conn
* @return {Promise<Environment>}
*/
createSession: function (identity, conn){
let def = Deferred();
let that=this;
let env= this.createEnvironment(identity);
env.load(conn)//evaluate environments from database
.then(()=>{
that.environments[identity.sessionID()]=env;
def.resolve(env);
})
.fail(err=> {
def.reject(err);
});
return def.promise();
},
/**
* Function that must create a PostData class, this is meant to be overridden in derived classes
* @param {Context} ctx
* @return {BusinessPostData}
*/
createPostData: function (ctx){
let p = new BusinessPostData(ctx);
p.setOptimisticLocking( new OptimisticLocking(['lt', 'lu'], ['ct', 'cu', 'lt', 'lu']));
return p;
},
/**
*
* @param tableName
* @param editType
* @return {DataSet}
*/
getDataSet: function(tableName,editType){
return commonGetDataSet.getDataSet(tableName,editType);
},
getAnonymousEnvironment:function(identity) {
// TODO create an anonymous environment
let e= new Environment(identity);
return e;
},
/**
* Creates a context object in req.app.local.context. If the token is not provided in the header, creates
* an anonymous connection.
* @param {Request} req
* @param {Response} res
* @param {Middleware} next
*/
getOrCreateContext: function (req, res, next) {
let token = req[tokenConfig.options.requestProperty]; //default is req[auth]
//Creates an Identity basing on the request token. If no token,
// an anonymous identity is created
let identity = getIdentityFromRequest(req);
let sessionID = identity.sessionID();
if (token) {
let env;
if (this.environments[sessionID]){
env = this.environments[sessionID];
}
else {
if (identity.isAnonymous) {
env = this.getAnonymousEnvironment(identity);
}
}
return this.getContext(req,res,next, env);
}
// return res.status(401).json({
// error: 'No token'
// });
// identity is an annonymous identity cause there is no token at all
// it is the same as if anonymous token was found in the header
//Here a new environment is created
let env = this.getAnonymousEnvironment(identity); //creates an anonymous environment
this.environments[sessionID] = env; //why?? it should not be done
return this.getContext(req,res,next, env);
},
/**
* Creates a context and attach it to req.app.local
* Also adds the
* @param pooledConn
* @param env
* @param {Request} req
* @param {Response} res
* @param {Middleware} next
*/
createContext: function(pooledConn,env,req,res, next){
let ctx = new Context();
ctx.dbCode = this.dbCode;
ctx.pooledConn = pooledConn;
ctx.dataAccess = pooledConn.getDataAccess();
ctx.security = ctx.dataAccess.security;
ctx.sqlConn = ctx.dataAccess.sqlConn;
ctx.environment = env;
ctx.dataAccess.externalUser = env.usr("externalUser");
ctx.formatter = ctx.sqlConn.formatter;
ctx.dbDescriptor = DBList.getDescriptor(ctx.dbCode);
ctx.securityProvider = this.getSecurityProvider;
ctx.identity = getIdentityFromRequest(req);
securityProvider(ctx.dataAccess,ctx.formatter)
.then((security)=>{
ctx.security = security;
ctx.createPostData = this.createPostData.bind(this, ctx); //to override
ctx.getDataSet = this.getDataSet.bind(this); //to override
ctx.localResource = LocalResource.prototype.getLocalResource(this.getLanguageFromRequest(req));
ctx.getMeta= function (tableName){
return GetMeta.getMeta(tableName,req);
};
ctx.getDataInvoke = new GetDataInvoke(ctx);
req.app.locals.context = ctx;
this.releaseConnection(req, res, ctx);
next();
});
},
getSecurityProvider: function(){
return require("./jsSecurity");
},
getLanguageFromRequest: function(req){
return req.language || "It";
},
/**
* Creates a context object in req.app.local.context, when environment already exists
* @param {Request} req
* @param {Response} res
* @param {Middleware} next
* @param {Environment} env
*/
getContext: function (req, res, next, env) {
try {
let token = req[tokenConfig.options.requestProperty]; //default is auth
if (!token) {
res.status(401).json({
error: 'No token'
});
return;
}
//If a token is not present, evaluates an anonymous identity
let identity = getIdentityFromRequest(req);
//let sessionID = identity.sessionID();
//let env = this.environments[sessionID];
if (!env) {
res.status(401).json({
error: 'Session not found'
});
return;
}
//Creates a context for the request execution
this.getDataAccess()
.then((pooledConn) => {
this.createContext(pooledConn, env, req, res, next);
})
.fail(err=>{
res.status(401).json({
error: 'Db not connected'
});
});
}
catch (err) {
res.status(401).json({
error: 'Invalid request!'
});
}
},
};
module.exports = JsApplication;