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.
This commit is contained in:
PhilRunninger 2024-03-22 18:33:04 -04:00
parent 2454393a1d
commit 22c2375f44
8 changed files with 89 additions and 49 deletions

Binary file not shown.

View File

@ -16,6 +16,12 @@ Function makeHttpRequest(method As String, route As String, doc As String, ByRef
Exit Function Exit Function
End If End If
' Inject the user's name and the current version of this file into the request body.
Set json = JsonConverter.ParseJson(doc)
json("version") = shConfig.Range("version").Value
json("username") = Application.UserName
doc = JsonConverter.ConvertToJson(json)
Dim server As String Dim server As String
server = shConfig.Range("server").Value server = shConfig.Range("server").Value
@ -32,6 +38,17 @@ Function makeHttpRequest(method As String, route As String, doc As String, ByRef
Debug.Print Timer - t; "sec): "; Left(wr, 200) Debug.Print Timer - t; "sec): "; Left(wr, 200)
End With End With
' This is a poor man's self-upgrade mechanism for this application.
If Mid(wr, 1, 24) = "Obsolete client workbook" Then
If MsgBox("Your workbook is an older version and needs to be upgraded. Download now? This workbook will be closed so your download can overwrite it.", vbYesNo + vbQuestion) = vbYes Then
ActiveWorkbook.FollowHyperlink server & "/template"
ActiveWorkbook.Close False
Else
errorMsg = "You won't be able to use this workbook until you upgrade it. Please download the new one the next time you're prompted."
Exit Function
End If
End If
If Mid(wr, 1, 1) <> "{" Or _ If Mid(wr, 1, 1) <> "{" Or _
Mid(wr, 2, 5) = "error" Or _ Mid(wr, 2, 5) = "error" Or _
Mid(wr, 1, 6) = "<body>" Or _ Mid(wr, 1, 6) = "<body>" Or _
@ -40,7 +57,7 @@ Function makeHttpRequest(method As String, route As String, doc As String, ByRef
errorMsg = "Unexpected Result from Server: " & wr errorMsg = "Unexpected Result from Server: " & wr
Exit Function Exit Function
End If End If
If Mid(wr, 1, 6) = "null" Then If Mid(wr, 1, 6) = "null" Then
errorMsg = "API route not implemented." errorMsg = "API route not implemented."
Exit Function Exit Function

119
index.js
View File

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const minSupportedVersion = '1.9.2';
require('dotenv').config(); require('dotenv').config();
const express = require('express'); const express = require('express');
var https = require('https'); var https = require('https');
@ -51,7 +53,16 @@ function error() {
return `\x1b[91mERROR\x1b[0m|` return `\x1b[91mERROR\x1b[0m|`
} }
function process_route(route, description, path, res, callback) { 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(`\x1b[96m${"▼".repeat(120)}\x1b[0m`)
console.log(`${timestamp()} ${route} (${description})`) console.log(`${timestamp()} ${route} (${description})`)
if (!path) { if (!path) {
@ -59,6 +70,12 @@ function process_route(route, description, path, res, callback) {
return; 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}`) console.log(`${timestamp()} SQL file: ${path}`)
fs.readFile(path, 'utf8', function(err, data) { fs.readFile(path, 'utf8', function(err, data) {
if (!err) { if (!err) {
@ -98,13 +115,19 @@ function build_where(req, res) {
return where; return where;
} }
server.get('/', function(_, res) { server.get('/', function(req, res) {
process_route('GET /', 'Test if server is up.', undefined, res, process_route('GET /', 'Test if server is up.', undefined, req, res,
function(_) { function(_) {
res.send('node.js express is up and running'); 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('/login', (_, res) => res.sendFile(process.env.wd + 'msauth.html'))
server.get('/logs', (_, res) => res.sendFile(process.env.wd + 'changes.log')) server.get('/logs', (_, res) => res.sendFile(process.env.wd + 'changes.log'))
@ -114,8 +137,8 @@ server.get('/pgbadger', (_, res) => res.sendFile(process.env.wd + 'logs.html'))
server.get('/totals', (_, res) => res.sendFile(process.env.wd + 'totals.log')) server.get('/totals', (_, res) => res.sendFile(process.env.wd + 'totals.log'))
server.get('/get_pool', bodyParser.json(), function(req, res) { 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', res, process_route('GET /get_pool', 'Get all data for one DSM.', './route_sql/get_pool.sql', req, res,
function(arg) { function(sql) {
if (req.body.quota_rep) { if (req.body.quota_rep) {
// ensure backward compatibility // ensure backward compatibility
console.log(`${timestamp()} Converting old format… ${JSON.stringify(req.body)}`); console.log(`${timestamp()} Converting old format… ${JSON.stringify(req.body)}`);
@ -125,41 +148,41 @@ server.get('/get_pool', bodyParser.json(), function(req, res) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where); sql = sql.replace(new RegExp("where_clause", 'g'), where);
Postgres.FirstRow(sql, res) Postgres.FirstRow(sql, res)
}); });
}) })
server.get('/scenario_package', bodyParser.json(), function(req, 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', res, process_route('GET /scenario_package', 'Get all data for a given scenario.', './route_sql/scenario_package.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where) sql = sql.replace(new RegExp("where_clause", 'g'), where)
Postgres.FirstRow(sql, res) Postgres.FirstRow(sql, res)
}); });
}) })
server.get('/swap_fit', bodyParser.json(), function(req, res) { server.get('/swap_fit', bodyParser.json(), function(req, res) {
process_route('GET /swap_fit', 'Obsolete.', './route_sql/swap_fit.sql', res, process_route('GET /swap_fit', 'Obsolete.', './route_sql/swap_fit.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where); sql = sql.replace(new RegExp("where_clause", 'g'), where);
sql = sql.replace(new RegExp("replace_new_mold", 'g'), req.body.new_mold); sql = sql.replace(new RegExp("replace_new_mold", 'g'), req.body.new_mold);
Postgres.FirstRow(sql, res) Postgres.FirstRow(sql, res)
}); });
}) })
server.post('/swap', bodyParser.json(), function(req, res) { server.post('/swap', bodyParser.json(), function(req, res) {
process_route('POST /swap', 'Obsolete.', './route_sql/swap_post.sql', res, process_route('POST /swap', 'Obsolete.', './route_sql/swap_post.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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("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_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source); sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
@ -169,12 +192,12 @@ server.post('/swap', bodyParser.json(), function(req, res) {
}) })
server.post('/cust_swap', bodyParser.json(), function(req, res) { server.post('/cust_swap', bodyParser.json(), function(req, res) {
process_route('POST /cust_swap', 'Obsolete.', './route_sql/swap_cust.sql', res, process_route('POST /cust_swap', 'Obsolete.', './route_sql/swap_cust.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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("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_version", 'g'), req.body.scenario.version);
sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source); sql = sql.replace(new RegExp("replace_source", 'g'), req.body.source);
@ -184,19 +207,19 @@ server.post('/cust_swap', bodyParser.json(), function(req, res) {
}) })
server.get('/list_changes', bodyParser.json(), function(req, 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', res, process_route('GET /list_changes', 'Get a list of adjustments made to DSM\'s pool.', './route_sql/list_changes.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
var sql = arg.replace(new RegExp("where_clause", 'g'), where); sql = sql.replace(new RegExp("where_clause", 'g'), where);
Postgres.FirstRow(sql, res) Postgres.FirstRow(sql, res)
}); });
}) })
server.get('/undo_change', bodyParser.json(), function(req, 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', res, process_route('GET /undo_change', 'Remove an adjustment from the DSM\'s pool.', './route_sql/undo.sql', req, res,
function(arg) { function(sql) {
var sql = arg.replace(new RegExp("replace_id", 'g'), JSON.stringify(req.body.logid)) sql = sql.replace(new RegExp("replace_id", 'g'), JSON.stringify(req.body.logid))
Postgres.FirstRow(sql, res) Postgres.FirstRow(sql, res)
}); });
}) })
@ -204,13 +227,13 @@ server.get('/undo_change', bodyParser.json(), function(req, res) {
//deprecating this route, just use _vp for volume and prive //deprecating this route, just use _vp for volume and prive
/* /*
server.post('/addmonth_v', bodyParser.json(), function(req, res) { server.post('/addmonth_v', bodyParser.json(), function(req, res) {
process_route('POST /add_month_v', 'Obsolete.', './route_sql/addmonth_vd.sql', res, process_route('POST /add_month_v', 'Obsolete.', './route_sql/addmonth_vd.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("scenario = target_scenario", 'g'), where); 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_increment", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_month", 'g'), req.body.month); 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_version", 'g'), req.body.scenario.version);
@ -222,13 +245,13 @@ server.post('/addmonth_v', bodyParser.json(), function(req, res) {
*/ */
server.post('/addmonth_vp', bodyParser.json(), function(req, 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', 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(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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_volume", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_price", 'g'), req.body.amount); 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("target_month", 'g'), req.body.month);
@ -240,13 +263,13 @@ server.post('/addmonth_vp', bodyParser.json(), function(req, res) {
}) })
server.post('/scale_v', bodyParser.json(), function(req, 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', res, process_route('POST /scale_v', 'Scale the volume for the given scenario.', './route_sql/scale_vd.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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("incr_qty", 'g'), req.body.qty);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version); 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_source", 'g'), req.body.source);
@ -256,13 +279,13 @@ server.post('/scale_v', bodyParser.json(), function(req, res) {
}) })
server.post('/scale_p', bodyParser.json(), function(req, 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', res, process_route('POST /scale_p', 'Scale price for the given scenario.', './route_sql/scale_pd.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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("target_increment", 'g'), req.body.amount);
sql = sql.replace(new RegExp("replace_version", 'g'), req.body.scenario.version); 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_source", 'g'), req.body.source);
@ -272,13 +295,13 @@ server.post('/scale_p', bodyParser.json(), function(req, res) {
}) })
server.post('/scale_vp', bodyParser.json(), function(req, 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', res, process_route('POST /scale_vp', 'Scale volume and price for the given scenario.', './route_sql/scale_vupd.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount); 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_version", 'g'), req.body.scenario.version);
@ -289,13 +312,13 @@ server.post('/scale_vp', bodyParser.json(), function(req, res) {
}) })
server.post('/scale_vp_sales', bodyParser.json(), function(req, res) { server.post('/scale_vp_sales', bodyParser.json(), function(req, res) {
process_route('POST /scale_vp_sales', 'Obsolete.', './route_sql/scale_vupd.sql', res, process_route('POST /scale_vp_sales', 'Obsolete.', './route_sql/scale_vupd.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount); 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_version", 'g'), req.body.scenario.version);
@ -306,13 +329,13 @@ server.post('/scale_vp_sales', bodyParser.json(), function(req, res) {
}) })
server.post('/new_part', bodyParser.json(), function(req, res) { server.post('/new_part', bodyParser.json(), function(req, res) {
process_route('POST /new_part', 'Obsolete.', './route_sql/new_part.sql', res, process_route('POST /new_part', 'Obsolete.', './route_sql/new_part.sql', req, res,
function(arg) { function(sql) {
var where = build_where(req, res); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount); 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_request", 'g'), JSON.stringify(req.body));
@ -324,15 +347,15 @@ server.post('/new_part', bodyParser.json(), function(req, res) {
}) })
server.post('/new_basket', bodyParser.json(), function(req, 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', res, process_route('POST /new_basket', 'Add new part and/or customer.', './route_sql/new_basket.sql', req, res,
function(arg) { 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 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); var where = build_where(req, res);
if (!where) return; if (!where) return;
req.body.stamp = new Date().toISOString() req.body.stamp = new Date().toISOString()
var sql = arg.replace(new RegExp("where_clause", 'g'), where); 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_vol", 'g'), req.body.qty);
sql = sql.replace(new RegExp("target_prc", 'g'), req.body.amount); 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_request", 'g'), JSON.stringify(req.body));