forecast_api/index.js
PhilRunninger 22c2375f44 Add a mechanism to self-upgrade the workbook.
It's not completely seamless, but it should work adequately well. The
workbook (aka client) inserts the workbook version into the http request
body. The server code compares that version number against its minimum
supported client version.

If the client is too old, an error message is sent back to the client.

When the client receives the "Obsolete" error message, it launches the
https://<server>:<port>/template URL in the default browser, which
enables the user to save the downloaded new workbook file.
2024-03-22 18:33:04 -04:00

368 lines
15 KiB
JavaScript

#!/usr/bin/env node
const minSupportedVersion = '1.9.2';
require('dotenv').config();
const express = require('express');
var https = require('https');
var bodyParser = require('body-parser');
const server = express();
const pg = require('pg');
var fs = require('fs');
var options = {
key: fs.readFileSync(process.env.wd + 'key.pem'),
cert: fs.readFileSync(process.env.wd + 'cert.pem'),
passprase: []
};
https.createServer(options, server).listen(process.env.nodeport, () => {
console.log(`${timestamp()} Web service started on port ${process.env.nodeport}`)
});
var Postgres = new pg.Client({
user: process.env.user,
password: process.env.password,
host: process.env.host,
port: process.env.port,
database: process.env.database,
ssl: false,
application_name: "osm_api"
});
Postgres.connect();
Postgres.FirstRow = function(sql, response) {
console.log(`${timestamp()} Running query:\n${sql}`);
Postgres.query(sql, [], (err, res) => {
if (err === null) {
console.log(`${timestamp()} Reponse: ${JSON.stringify(res.rows[0]).slice(0,5000)}${JSON.stringify(res.rows[0]).length<5000?'':'… (truncated)'}`);
response.json(res.rows[0]);
return;
}
console.log(`${timestamp()} ${error()}${JSON.stringify(err.stack)}`);
response.json(err.stack);
});
};
function timestamp() {
return `\x1b[33m${new Date().toLocaleString()}:\x1b[0m`
}
function error() {
return `\x1b[91mERROR\x1b[0m|`
}
function supported_version(v) {
if (v === undefined) return false;
const formatVersion = function(v) {
return v.split('.').map((x) => ('00000'+x).slice(-5)).join('.');
}
return formatVersion(v) >= formatVersion(minSupportedVersion);
}
function process_route(route, description, path, req, res, callback) {
console.log(`\x1b[96m${"▼".repeat(120)}\x1b[0m`)
console.log(`${timestamp()} ${route} (${description})`)
if (!path) {
callback(undefined);
return;
}
if (!supported_version(req.body.version)) {
console.log(`${timestamp()} ${error()}${req.body.username ?? 'Anonymous'} is using a too-old version of the client - ${req.body.version}`);
res.send(`Obsolete client workbook, version ${req.body.version}`);
return;
}
console.log(`${timestamp()} SQL file: ${path}`)
fs.readFile(path, 'utf8', function(err, data) {
if (!err) {
callback(data);
} else {
console.log(`${timestamp()} ${error()}Could not read sql file: ${JSON.stringify(err)}`);
res.send('Failed to read needed SQL file.');
}
});
}
function build_where(req, res) {
console.log(`${timestamp()} Building WHERE from ${JSON.stringify(req.body)}`);
var where = '';
for (var column in req.body.scenario) {
if (where) {
where = `${where}\n AND `;
}
if (Array.isArray(req.body.scenario[column])) {
where = `${where} ${column} IN ('${req.body.scenario[column].join("','")}')`;
} else {
where = `${where} ${column} = '${req.body.scenario[column]}'`;
}
};
//if the request has a stamp, then it's an adjustment
//prevent making adjustment to actual history by limiting to current stamp and beyond
if (req.body.stamp) {
where = `${where}\n AND order_date >= least('${req.body.stamp}'::date,'2021-06-01')`;
}
console.log(`${timestamp()} Returning ${where}`);
if (where == '') {
console.log(`${timestamp()} ${error()}Unable to create a WHERE clause.`);
res.send('No body was sent.');
}
return where;
}
server.get('/', function(req, res) {
process_route('GET /', 'Test if server is up.', undefined, req, res,
function(_) {
res.send('node.js express is up and running');
});
})
server.get('/template', function(_, res) {
console.log('Downloading a template for someone.');
const file = `${__dirname}/Master Template.xlsm`;
res.download(file);
})
server.get('/login', (_, res) => res.sendFile(process.env.wd + 'msauth.html'))
server.get('/logs', (_, res) => res.sendFile(process.env.wd + 'changes.log'))
server.get('/pgbadger', (_, res) => res.sendFile(process.env.wd + 'logs.html'))
server.get('/totals', (_, res) => res.sendFile(process.env.wd + 'totals.log'))
server.get('/get_pool', bodyParser.json(), function(req, res) {
process_route('GET /get_pool', 'Get all data for one DSM.', './route_sql/get_pool.sql', req, res,
function(sql) {
if (req.body.quota_rep) {
// ensure backward compatibility
console.log(`${timestamp()} Converting old format… ${JSON.stringify(req.body)}`);
req.body = {'scenario':{'quota_rep_descr':req.body.quota_rep}};
}
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where);
Postgres.FirstRow(sql, res)
});
})
server.get('/scenario_package', bodyParser.json(), function(req, res) {
process_route('GET /scenario_package', 'Get all data for a given scenario.', './route_sql/scenario_package.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where)
Postgres.FirstRow(sql, res)
});
})
server.get('/swap_fit', bodyParser.json(), function(req, res) {
process_route('GET /swap_fit', 'Obsolete.', './route_sql/swap_fit.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("replace_new_mold", 'g'), req.body.new_mold);
Postgres.FirstRow(sql, res)
});
})
server.post('/swap', bodyParser.json(), function(req, res) {
process_route('POST /swap', 'Obsolete.', './route_sql/swap_post.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("swap_doc", 'g'), JSON.stringify(req.body.swap));
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/cust_swap', bodyParser.json(), function(req, res) {
process_route('POST /cust_swap', 'Obsolete.', './route_sql/swap_cust.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("swap_doc", 'g'), JSON.stringify(req.body.swap));
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.get('/list_changes', bodyParser.json(), function(req, res) {
process_route('GET /list_changes', 'Get a list of adjustments made to DSM\'s pool.', './route_sql/list_changes.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
sql = sql.replace(new RegExp("where_clause", 'g'), where);
Postgres.FirstRow(sql, res)
});
})
server.get('/undo_change', bodyParser.json(), function(req, res) {
process_route('GET /undo_change', 'Remove an adjustment from the DSM\'s pool.', './route_sql/undo.sql', req, res,
function(sql) {
sql = sql.replace(new RegExp("replace_id", 'g'), JSON.stringify(req.body.logid))
Postgres.FirstRow(sql, res)
});
})
//deprecating this route, just use _vp for volume and prive
/*
server.post('/addmonth_v', bodyParser.json(), function(req, res) {
process_route('POST /add_month_v', 'Obsolete.', './route_sql/addmonth_vd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("scenario = target_scenario", 'g'), where);
sql = sql.replace(new RegExp("target_increment", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_month", 'g'), req.body.month);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
*/
server.post('/addmonth_vp', bodyParser.json(), function(req, res) {
process_route('POST /add_month_vp', 'Add volume and pricing for a new month in the forecast.', './route_sql/addmonth_vupd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_volume", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_price", 'g'), req.body.amount);
sql = sql.replace(new RegExp("target_month", 'g'), req.body.month);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/scale_v', bodyParser.json(), function(req, res) {
process_route('POST /scale_v', 'Scale the volume for the given scenario.', './route_sql/scale_vd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("incr_qty", 'g'), req.body.qty);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/scale_p', bodyParser.json(), function(req, res) {
process_route('POST /scale_p', 'Scale price for the given scenario.', './route_sql/scale_pd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_increment", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/scale_vp', bodyParser.json(), function(req, res) {
process_route('POST /scale_vp', 'Scale volume and price for the given scenario.', './route_sql/scale_vupd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/scale_vp_sales', bodyParser.json(), function(req, res) {
process_route('POST /scale_vp_sales', 'Obsolete.', './route_sql/scale_vupd.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/new_part', bodyParser.json(), function(req, res) {
process_route('POST /new_part', 'Obsolete.', './route_sql/new_part.sql', req, res,
function(sql) {
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_request", 'g'), JSON.stringify(req.body));
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})
server.post('/new_basket', bodyParser.json(), function(req, res) {
process_route('POST /new_basket', 'Add new part and/or customer.', './route_sql/new_basket.sql', req, res,
function(sql) {
req.body.scenario.iter.push("adj volume"); //intercept the request body and force in a "adj volume" at position 1, only a "copy" iteration is being used
var where = build_where(req, res);
if (!where) return;
req.body.stamp = new Date().toISOString()
sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("target_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_request", 'g'), JSON.stringify(req.body));
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
sql = sql.replace(new RegExp("replace_iterdef", 'g'), JSON.stringify(req.body));
Postgres.FirstRow(sql, res)
});
})