"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GrafanaProxy = void 0;
var Config_1 = require("./Config");
var Crypt_1 = require("./Crypt");
var openflow_api_1 = require("@openiap/openflow-api");
var DBHelper_1 = require("./DBHelper");
var OAuthProvider_1 = require("./OAuthProvider");
var OAuthServer = require("oauth2-server");
var Request = OAuthServer.Request;
var Response = OAuthServer.Response;
var GrafanaProxy = /** @class */ (function () {
    function GrafanaProxy() {
    }
    GrafanaProxy.parseJson = function (txt, reviver, context) {
        context = context || 20;
        try {
            return JSON.parse(txt, reviver);
        }
        catch (e) {
            if (typeof txt !== 'string') {
                var isEmptyArray = Array.isArray(txt) && txt.length === 0;
                var errorMessage = 'Cannot parse ' +
                    (isEmptyArray ? 'an empty array' : String(txt));
                throw new TypeError(errorMessage);
            }
            var syntaxErr = e.message.match(/^Unexpected token.*position\s+(\d+)/i);
            var errIdx = syntaxErr
                ? +syntaxErr[1]
                : e.message.match(/^Unexpected end of JSON.*/i)
                    ? txt.length - 1
                    : null;
            if (errIdx != null) {
                var start = errIdx <= context
                    ? 0
                    : errIdx - context;
                var end = errIdx + context >= txt.length
                    ? txt.length
                    : errIdx + context;
                e.message += " while parsing near '" + (start === 0 ? '' : '...') + txt.slice(start, end) + (end === txt.length ? '' : '...') + "'";
            }
            else {
                e.message += " while parsing '" + txt.slice(0, context * 2) + "'";
            }
            throw e;
        }
    };
    GrafanaProxy.configure = function (logger, app) {
        return __awaiter(this, void 0, void 0, function () {
            var _this = this;
            return __generator(this, function (_a) {
                this._logger = logger;
                this.app = app;
                app.all('/grafana', function (req, res, next) {
                    _this.logRequest(req.body, "/");
                    _this.setCORSHeaders(req, res);
                    if (!Config_1.Config.db.isConnected) {
                        res.send({
                            status: "error",
                            display_status: "Error",
                            message: 'OpenFlow not connected to DB'
                        });
                    }
                    else {
                        res.send({
                            status: "success",
                            display_status: "Success",
                            message: 'MongoDB Connection test OK'
                        });
                    }
                    // next()
                });
                app.all('/grafana/search', function (req, res, next) {
                    _this.logRequest(req.body, "/search");
                    _this.setCORSHeaders(req, res);
                    // Generate an id to track requests
                    var requestId = ++_this.requestIdCounter;
                    // Add state for the queries in this request
                    var queryStates = [];
                    _this.requestsPending[requestId] = queryStates;
                    try {
                        // Parse query string in target
                        var queryArgs = _this.parseQuery(req.body.target, {});
                        if (queryArgs.err != null) {
                            _this.queryError(requestId, queryArgs.err, res, next);
                            return;
                        }
                        else {
                            _this.doTemplateQuery(requestId, queryArgs, req.body.db, req, res, next);
                        }
                    }
                    catch (error) {
                        _this.queryError(requestId, error, res, next);
                    }
                });
                // Called to get graph points
                app.all('/grafana/query', function (req, res, next) {
                    _this.logRequest(req.body, "/query");
                    _this.setCORSHeaders(req, res);
                    if (openflow_api_1.NoderedUtil.IsNullUndefinded(req.body.range)) {
                        res.send({
                            status: "success",
                            display_status: "Success",
                            message: 'MongoDB Connection test OK'
                        });
                        // next();
                        return;
                    }
                    // Parse query string in target
                    var substitutions = {
                        "$from": new Date(req.body.range.from),
                        "$to": new Date(req.body.range.to),
                        "$dateBucketCount": _this.getBucketCount(req.body.range.from, req.body.range.to, req.body.intervalMs)
                    };
                    // Generate an id to track requests
                    var requestId = ++_this.requestIdCounter;
                    // Add state for the queries in this request
                    var queryStates = [];
                    _this.requestsPending[requestId] = queryStates;
                    var error = false;
                    for (var queryId = 0; queryId < req.body.targets.length && !error; queryId++) {
                        var tg = req.body.targets[queryId];
                        var queryArgs = _this.parseQuery(tg.target, substitutions);
                        queryArgs.type = tg.type;
                        if (queryArgs.err != null) {
                            _this.queryError(requestId, queryArgs.err, res, next);
                            error = true;
                            return;
                        }
                        else {
                            // Add to the state
                            queryStates.push({ pending: true });
                            // Run the query
                            _this.runAggregateQuery(requestId, queryId, req.body, queryArgs, req, res, next);
                        }
                    }
                });
                return [2 /*return*/];
            });
        });
    };
    GrafanaProxy.setCORSHeaders = function (req, res) {
        // res.setHeader("Access-Control-Allow-Origin", "*");
        // let port = 3000;
        // res.setHeader("Access-Control-Allow-Origin", req.protocol + '://' + req.get('host') + (port == 80 || port == 443 ? '' : ':' + port) + req.originalUrl);
        // res.setHeader("Access-Control-Allow-Origin", req.headers["referer"]);
        if (req.headers["origin"] == null)
            res.setHeader("Access-Control-Allow-Origin", "*");
        if (req.headers["origin"] != null)
            res.setHeader("Access-Control-Allow-Origin", req.headers["origin"]);
        res.setHeader("Access-Control-Allow-Methods", "POST");
        res.setHeader("Access-Control-Allow-Headers", "accept, content-type");
    };
    GrafanaProxy.logRequest = function (body, type) {
        if (this.logRequests) {
            console.log("REQUEST: " + type + ":\n" + JSON.stringify(body, null, 2));
        }
    };
    GrafanaProxy.logQuery = function (query, options) {
        if (this.logQueries) {
            console.log("Query:");
            console.log(JSON.stringify(query, null, 2));
            if (options != null) {
                console.log("Query Options:");
                console.log(JSON.stringify(options, null, 2));
            }
        }
    };
    GrafanaProxy.queryError = function (requestId, err, res, next) {
        // We only 1 return error per query so it may have been removed from the list
        if (requestId in this.requestsPending) {
            // Remove request
            delete this.requestsPending[requestId];
            // Send back error
            // next(err);
            // res.status(500).json(err);
            // next(new Error(err));
            var result = { message: err.message, statusCode: 500 };
            console.error(err.message); // Log error message in our server's console
            if (!err.statusCode)
                err.statusCode = 500; // If err has no specified error code, set error code to 'Internal Server Error (500)'
            var json = JSON.stringify(result);
            res.status(err.statusCode).send(json); // All HTTP requests must have a response, so let's send back an error with its status code and message
        }
    };
    // Called when query finished
    GrafanaProxy.queryFinished = function (requestId, queryId, results, res, next) {
        // We only 1 return error per query so it may have been removed from the list
        if (requestId in this.requestsPending) {
            var queryStatus = this.requestsPending[requestId];
            // Mark this as finished
            queryStatus[queryId].pending = false;
            queryStatus[queryId].results = results;
            // See if we're all done
            var done = true;
            for (var i = 0; i < queryStatus.length; i++) {
                if (queryStatus[i].pending == true) {
                    done = false;
                    break;
                }
            }
            // If query done, send back results
            if (done) {
                // Concatenate results
                var output = [];
                for (var i = 0; i < queryStatus.length; i++) {
                    var queryResults = queryStatus[i].results;
                    var keys = Object.keys(queryResults);
                    for (var k = 0; k < keys.length; k++) {
                        var tg = keys[k];
                        output.push(queryResults[tg]);
                    }
                }
                res.json(output);
                // next()
                // Remove request
                delete this.requestsPending[requestId];
            }
        }
    };
    GrafanaProxy.parseQuery = function (query, substitutions) {
        var doc = {};
        var queryErrors = [];
        if (query == null) {
            doc.err = new Error("Query cannot be empty.");
            return doc;
        }
        query = query.trim();
        if (query.substring(0, 3) != "db.") {
            doc.err = new Error("Query must start with db.");
            return doc;
        }
        // Query is of the form db.<collection>.aggregate or db.<collection>.find
        // Split on the first ( after db.
        var openBracketIndex = query.indexOf('(', 3);
        if (openBracketIndex == -1) {
            queryErrors.push("Can't find opening bracket");
        }
        else {
            // Split the first bit - it's the collection name and operation ( must be aggregate )
            var parts = query.substring(3, openBracketIndex).split('.');
            // Collection names can have .s so last part is operation, rest is the collection name
            if (parts.length >= 2) {
                doc.operation = parts.pop().trim();
                doc.collection = parts.join('.');
            }
            else {
                queryErrors.push("Invalid collection and operation syntax");
            }
            // Args is the rest up to the last bracket
            var closeBracketIndex = query.indexOf(')', openBracketIndex);
            if (closeBracketIndex == -1) {
                queryErrors.push("Can't find last bracket");
            }
            else {
                var args = query.substring(openBracketIndex + 1, closeBracketIndex);
                if (doc.operation == 'aggregate') {
                    try {
                        // Wrap args in array syntax so we can check for optional options arg
                        args = '[' + args + ']';
                        // let docs = JSON.parse(args);
                        var docs = this.parseJson(args, null, null);
                        // First Arg is pipeline
                        doc.pipeline = docs[0];
                        // If we have 2 top level args, second is agg options
                        if (docs.length == 2) {
                            doc.options = docs[1];
                        }
                        // Replace with substitutions
                        for (var i = 0; i < doc.pipeline.length; i++) {
                            var stage = doc.pipeline[i];
                            this.forIn(stage, function (obj, key, value) {
                                if (typeof (value) == "string") {
                                    if (value in substitutions) {
                                        obj[key] = substitutions[value];
                                    }
                                }
                            });
                        }
                    }
                    catch (error) {
                        queryErrors.push(error.message);
                        console.log(args);
                    }
                }
                else if (doc.operation == 'find') {
                    try {
                        // Wrap args in array syntax so we can check for optional options arg
                        args = '[' + args + ']';
                        // let docs = JSON.parse(args);
                        var docs = this.parseJson(args, null, null);
                        // First Arg is pipeline
                        doc.query = docs[0];
                        // If we have 2 top level args, second is options
                        if (docs.length == 2) {
                            doc.options = docs[1];
                        }
                        // Replace with substitutions
                        var stage = doc.query;
                        this.forIn(stage, function (obj, key, value) {
                            if (typeof (value) == "string") {
                                if (value in substitutions) {
                                    obj[key] = substitutions[value];
                                }
                            }
                        });
                    }
                    catch (error) {
                        queryErrors.push(error.message);
                        console.log(args);
                    }
                }
                else {
                    queryErrors.push("Unknown operation " + doc.operation + ", only aggregate supported");
                }
            }
        }
        if (queryErrors.length > 0) {
            doc.err = new Error('Failed to parse query - ' + queryErrors.join(':'));
        }
        return doc;
    };
    GrafanaProxy.forIn = function (obj, processFunc) {
        var key;
        for (key in obj) {
            var value = obj[key];
            processFunc(obj, key, value);
            if (value != null && typeof (value) == "object") {
                this.forIn(value, processFunc);
            }
        }
    };
    GrafanaProxy.getBucketCount = function (from, to, intervalMs) {
        var boundaries = [];
        var current = new Date(from).getTime();
        var toMs = new Date(to).getTime();
        var count = 0;
        while (current < toMs) {
            current += intervalMs;
            count++;
        }
        return count;
    };
    GrafanaProxy.getTableResults = function (docs) {
        var columns = {};
        // Build superset of columns
        for (var i = 0; i < docs.length; i++) {
            var doc = docs[i];
            // Go through all properties
            for (var propName in doc) {
                // See if we need to add a new column
                if (!(propName in columns)) {
                    columns[propName] =
                        {
                            text: propName,
                            type: "text"
                        };
                }
            }
        }
        // Build return rows
        var rows = [];
        for (var i = 0; i < docs.length; i++) {
            var doc = docs[i];
            var row = [];
            // All cols
            for (var colName in columns) {
                var col = columns[colName];
                if (col.text in doc) {
                    row.push(doc[col.text]);
                }
                else {
                    row.push(null);
                }
            }
            rows.push(row);
        }
        var results = {};
        results["table"] = {
            columns: Object.values(columns),
            rows: rows,
            type: "table"
        };
        return results;
    };
    GrafanaProxy.getTimeseriesResults = function (docs) {
        var results = {};
        for (var i = 0; i < docs.length; i++) {
            var doc = docs[i];
            var tg = doc.name;
            var dp = null;
            if (tg in results) {
                dp = results[tg];
            }
            else {
                dp = { 'target': tg, 'datapoints': [] };
                results[tg] = dp;
            }
            results[tg].datapoints.push([doc['value'], doc['ts'].getTime()]);
        }
        return results;
    };
    // Runs a query to support templates. Must returns documents of the form
    // { _id : <id> }
    GrafanaProxy.doTemplateQuery = function (requestId, queryArgs, db, req, res, next) {
        return __awaiter(this, void 0, void 0, function () {
            var collectionname, jwt, result, output, i, doc, error_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!(queryArgs.err == null)) return [3 /*break*/, 10];
                        if (!Config_1.Config.db.isConnected) {
                            this.queryError(requestId, new Error("OpenFlow not connected to DB"), res, next);
                            return [2 /*return*/];
                        }
                        // Remove request from list
                        if (requestId in this.requestsPending) {
                            delete this.requestsPending[requestId];
                        }
                        collectionname = queryArgs.collection;
                        return [4 /*yield*/, this.GetJWT(req, res)];
                    case 1:
                        jwt = _a.sent();
                        if (jwt == null) {
                            this.queryError(requestId, new Error("Go away!"), res, next);
                            return [2 /*return*/];
                        }
                        _a.label = 2;
                    case 2:
                        _a.trys.push([2, 8, , 9]);
                        result = void 0;
                        if (!(queryArgs.operation == 'aggregate')) return [3 /*break*/, 4];
                        return [4 /*yield*/, Config_1.Config.db.aggregate(queryArgs.pipeline, collectionname, jwt)];
                    case 3:
                        result = _a.sent();
                        return [3 /*break*/, 7];
                    case 4:
                        if (!(queryArgs.operation == 'find')) return [3 /*break*/, 6];
                        return [4 /*yield*/, Config_1.Config.db.query(queryArgs.query, null, 6000, 0, null, collectionname, jwt)];
                    case 5:
                        result = _a.sent();
                        return [3 /*break*/, 7];
                    case 6:
                        this.queryError(requestId, "unknown operation" + queryArgs.operation, res, next);
                        return [2 /*return*/];
                    case 7:
                        output = [];
                        for (i = 0; i < result.length; i++) {
                            doc = result[i];
                            if (doc["_id"] != null) {
                                output.push(doc["_id"]);
                            }
                            else {
                                output.push(doc);
                            }
                        }
                        res.json(output);
                        return [3 /*break*/, 9];
                    case 8:
                        error_1 = _a.sent();
                        this.queryError(requestId, error_1, res, next);
                        return [2 /*return*/];
                    case 9: return [3 /*break*/, 11];
                    case 10:
                        // next(queryArgs.err)
                        this.queryError(requestId, queryArgs.err, res, next);
                        _a.label = 11;
                    case 11: return [2 /*return*/];
                }
            });
        });
    };
    GrafanaProxy.replaceinstring = function (pipeline) {
        var keys = Object.keys(pipeline);
        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            var value = pipeline[key];
            if (key == "$in" && typeof value === 'string') {
                var array = value.split(",");
                value = array;
            }
        }
    };
    // Run an aggregate query. Must return documents of the form
    // { value : 0.34334, ts : <epoch time in seconds> }
    GrafanaProxy.runAggregateQuery = function (requestId, queryId, body, queryArgs, req, res, next) {
        return __awaiter(this, void 0, void 0, function () {
            var jwt, collectionname, docs, results, err_1;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!Config_1.Config.db.isConnected) {
                            this.queryError(requestId, new Error("OpenFlow not connected to DB"), res, next);
                            return [2 /*return*/];
                        }
                        return [4 /*yield*/, this.GetJWT(req, res)];
                    case 1:
                        jwt = _a.sent();
                        if (jwt == null) {
                            this.queryError(requestId, new Error("Go away!"), res, next);
                            return [2 /*return*/];
                        }
                        collectionname = queryArgs.collection;
                        _a.label = 2;
                    case 2:
                        _a.trys.push([2, 8, , 9]);
                        docs = void 0;
                        if (!(queryArgs.operation == 'aggregate')) return [3 /*break*/, 4];
                        this.logQuery(queryArgs.pipeline, queryArgs.options);
                        this.replaceinstring(queryArgs.pipeline);
                        return [4 /*yield*/, Config_1.Config.db.aggregate(queryArgs.pipeline, collectionname, jwt)];
                    case 3:
                        docs = _a.sent();
                        return [3 /*break*/, 7];
                    case 4:
                        if (!(queryArgs.operation == 'find')) return [3 /*break*/, 6];
                        this.logQuery(queryArgs.query, queryArgs.options);
                        this.replaceinstring(queryArgs.query);
                        return [4 /*yield*/, Config_1.Config.db.query(queryArgs.query, null, 6000, 0, null, collectionname, jwt)];
                    case 5:
                        docs = _a.sent();
                        return [3 /*break*/, 7];
                    case 6:
                        this.queryError(requestId, "unknown operation" + queryArgs.operation, res, next);
                        return [2 /*return*/];
                    case 7:
                        results = {};
                        if (queryArgs.type == 'timeserie') {
                            results = this.getTimeseriesResults(docs);
                        }
                        else {
                            results = this.getTableResults(docs);
                        }
                        // Mark query as finished - will send back results when all queries finished
                        this.queryFinished(requestId, queryId, results, res, next);
                        return [3 /*break*/, 9];
                    case 8:
                        err_1 = _a.sent();
                        this.queryError(requestId, err_1, res, next);
                        return [3 /*break*/, 9];
                    case 9: return [2 /*return*/];
                }
            });
        });
    };
    GrafanaProxy.GetJWT = function (req, res) {
        return __awaiter(this, void 0, void 0, function () {
            var user_1, request, response, token, user, jwt, error_2, username, user, jwt;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        if (!req.user) return [3 /*break*/, 1];
                        user_1 = openflow_api_1.TokenUser.From(req.user);
                        return [2 /*return*/, Crypt_1.Crypt.createToken(user_1, Config_1.Config.shorttoken_expires_in)];
                    case 1:
                        request = new Request(req);
                        response = new Response(res);
                        _a.label = 2;
                    case 2:
                        _a.trys.push([2, 6, , 7]);
                        return [4 /*yield*/, OAuthProvider_1.OAuthProvider.instance.oauthServer.authenticate(request, response)];
                    case 3:
                        token = _a.sent();
                        if (!(!openflow_api_1.NoderedUtil.IsNullUndefinded(token) && !openflow_api_1.NoderedUtil.IsNullUndefinded(token.user) && !openflow_api_1.NoderedUtil.IsNullEmpty(token.user._id))) return [3 /*break*/, 5];
                        return [4 /*yield*/, DBHelper_1.DBHelper.FindById(token.user._id)];
                    case 4:
                        user = _a.sent();
                        if (!openflow_api_1.NoderedUtil.IsNullUndefinded(user)) {
                            jwt = Crypt_1.Crypt.createToken(user, "1d");
                            return [2 /*return*/, jwt];
                        }
                        else {
                            console.error("Failed locating user with id " + token.user._id + "????");
                        }
                        _a.label = 5;
                    case 5: return [3 /*break*/, 7];
                    case 6:
                        error_2 = _a.sent();
                        if (error_2.message) {
                            console.error(error_2.message);
                        }
                        else {
                            console.error(error_2);
                        }
                        return [3 /*break*/, 7];
                    case 7:
                        username = req.headers["X-Grafana-User"];
                        if (openflow_api_1.NoderedUtil.IsNullEmpty(username))
                            username = req.headers["x-grafana-user"];
                        if (!!openflow_api_1.NoderedUtil.IsNullEmpty(username)) return [3 /*break*/, 9];
                        return [4 /*yield*/, DBHelper_1.DBHelper.FindByUsername(username)];
                    case 8:
                        user = _a.sent();
                        jwt = Crypt_1.Crypt.createToken(user, "1d");
                        return [2 /*return*/, jwt];
                    case 9: return [2 /*return*/, null];
                }
            });
        });
    };
    GrafanaProxy.logRequests = false;
    GrafanaProxy.logQueries = false;
    GrafanaProxy.requestIdCounter = 0;
    GrafanaProxy.requestsPending = {};
    return GrafanaProxy;
}());
exports.GrafanaProxy = GrafanaProxy;
//# sourceMappingURL=grafana-proxy%20copy.js.map