"use strict";
// -*- mode: js; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of Genie
//
// Copyright 2020 The Board of Trustees of the Leland Stanford Junior University
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Giovanni Campagna <gcampagn@cs.stanford.edu>
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.drop = exports.create = exports.query = exports.insertOne = exports.selectAll = exports.selectOne = exports.withTransaction = exports.withClient = exports.db = exports.ensureSchema = void 0;
const sqlite3_1 = __importDefault(require("sqlite3"));
const fs = __importStar(require("fs"));
const q_1 = __importDefault(require("q"));
let connectionPool = {};
function connectNow(filename, key) {
    let db = new sqlite3_1.default.Database(filename, sqlite3_1.default.OPEN_READWRITE);
    db.serialize(() => {
        db.run('PRAGMA busy_timeout = 1000');
        if (key)
            db.run(makeKeyPragma(key));
    });
    return db;
}
function acquireConnection(filename, key) {
    if (connectionPool[filename])
        return connectionPool[filename];
    return connectionPool[filename] = connectNow(filename, key);
}
function query(client, string, args) {
    return q_1.default.ninvoke(client, 'run', string, args);
}
exports.query = query;
exports.create = query;
exports.drop = query;
function selectAll(client, string, args) {
    return q_1.default.ninvoke(client, 'all', string, args);
}
exports.selectAll = selectAll;
function selectOne(client, string, args) {
    return q_1.default.ninvoke(client, 'get', string, args);
}
exports.selectOne = selectOne;
// PRAGMA key = ? does not work, so we have to use string pasting
// this is ok because the key should be trusted
// (and if the key can be manipulated by an attacker, either it will
// be invalid or we have already lost)
function makeKeyPragma(key) {
    return 'PRAGMA key = "x\'' + key.toUpperCase() + '\'"';
}
function withClient(filename, key, callback) {
    let client = acquireConnection(filename, key);
    return callback(client);
}
exports.withClient = withClient;
const _transactionQueue = new WeakMap;
function withTransaction(filename, key, transaction) {
    return withClient(filename, key, (client) => {
        let queue = _transactionQueue.get(client);
        if (!queue)
            queue = Promise.resolve();
        return new Promise((callback, errback) => {
            queue = queue.then(() => query(client, 'begin transaction', [])).then(() => {
                return transaction(client);
            }).then((result) => {
                return q_1.default.ninvoke(client, 'run', 'commit', []).then(() => {
                    callback(result);
                });
            }).catch((err) => {
                return q_1.default.ninvoke(client, 'run', 'rollback', []).then(() => {
                    errback(err);
                    // continue with the queue
                }, (rollerr) => {
                    console.error('Ignored error from ROLLBACK', rollerr);
                    errback(err);
                    // continue with the queue
                });
            });
            _transactionQueue.set(client, queue);
        });
    });
}
exports.withTransaction = withTransaction;
const schema_json_1 = __importDefault(require("./schema.json"));
function initializeDB(platform) {
    let filename = platform.getSqliteDB();
    let key = platform.getSqliteKey();
    let prefs = platform.getSharedPreferences();
    prefs.set('sqlite-schema-version', currentVersion);
    let db = new sqlite3_1.default.Database(filename, sqlite3_1.default.OPEN_READWRITE | sqlite3_1.default.OPEN_CREATE);
    let defer = q_1.default.defer();
    db.on('error', (err) => {
        console.error('Failed to initialize DB schema', err);
        defer.reject(err);
    });
    db.serialize(() => {
        if (key) {
            console.log('Initializing key in Sqlite DB');
            db.run(makeKeyPragma(key));
        }
        db.run('PRAGMA journal_mode=WAL');
        db.exec(schema_json_1.default);
    });
    db.close((err) => {
        if (!err)
            defer.resolve();
    });
    return defer.promise;
}
const MIGRATIONS = [
    function migrateTo1(db) {
        db.serialize(() => {
            db.run('drop table if exists keyword');
            db.run('create table keyword (uniqueId varchar(255) primary key, value text default null)');
            db.run('drop table if exists channel');
            db.run('create table channel (uniqueId varchar(255) primary key, value text default null)');
        });
    },
    function migrateTo2(db) {
        db.serialize(() => {
            db.run('drop table if exists permissions');
            db.run('create table permissions (uniqueId varchar(255) primary key, compat_key text, code text, extra text)');
            db.run('create index permissions_compat_key on permissions(compat_key)');
        });
    },
    function migrateTo3(db) {
        db.serialize(() => {
            db.run('create table if not exists permissions (uniqueId varchar(255) primary key, compat_key text, code text, extra text)');
            db.run('create index if not exists permissions_compat_key on permissions(compat_key)');
        });
    },
    function migrateTo4(db) {
        db.serialize(() => {
            db.run('alter table permissions add extra text default null');
        });
    },
    function migrateTo5(db) {
        db.serialize(() => {
            db.run(`create table matrix_sync (
            owner_id text,
            object_key text,
            object_value text,
            primary key(owner_id, object_key)
        )`);
            db.run(`create table matrix_accountData (
            owner_id text,
            object_key text,
            object_value text,
            primary key(owner_id, object_key)
        )`);
            db.run(`create table matrix_users (
            owner_id text,
            object_key text,
            object_value text,
            primary key(owner_id, object_key)
        )`);
        });
    },
    function migrateTo6(db) {
        db.serialize(() => {
            db.run(`create table matrix_outgoingRoomKeyRequests (
            owner_id text,
            request_id text,
            room_id text,
            session_id text,
            state int,
            object text,
            primary key(owner_id, request_id)
        )`);
            db.run(`create index matrix_outgoingRoomKeyRequests_session on matrix_outgoingRoomKeyRequests(owner_id, room_id, session_id)`);
            db.run(`create index matrix_outgoingRoomKeyRequests_state on matrix_outgoingRoomKeyRequests(owner_id, state)`);
        });
    },
    function migrateTo7(db) {
        // nothing to do, just bump the version number...
    },
    function migrateTo8(db) {
        db.serialize(() => {
            db.run(`create table if not exists memory_table_meta (
    name text primary key,
    args text,
    types text
)`);
        });
    }
];
const currentVersion = MIGRATIONS.length;
function migrateDB(platform) {
    // cloud migrations are handled out of band
    let filename = platform.getSqliteDB();
    let key = platform.getSqliteKey();
    let prefs = platform.getSharedPreferences();
    let version = prefs.get('sqlite-schema-version');
    if (version === undefined)
        version = 1;
    if (version === currentVersion)
        return q_1.default();
    prefs.set('sqlite-schema-version', currentVersion);
    console.log('Database needs migration...');
    let db = new sqlite3_1.default.Database(filename, sqlite3_1.default.OPEN_READWRITE);
    return new Promise((callback, errback) => {
        db.on('error', errback);
        if (key)
            db.run(makeKeyPragma(key));
        MIGRATIONS.forEach((migration, migrateTo) => {
            // migrations start at 1 (see above)
            migrateTo = migrateTo + 1;
            if (version < migrateTo)
                migration(db);
        });
        db.close((err) => {
            if (!err)
                console.log('Successfully migrated database to version ' + currentVersion);
            if (err)
                errback(err);
            else
                callback();
        });
    });
}
function ensureSchema(platform) {
    if (!fs.existsSync(platform.getSqliteDB()))
        return initializeDB(platform);
    else
        return migrateDB(platform);
}
exports.ensureSchema = ensureSchema;
function db(filename, key) {
    return {
        connect: function () {
            return acquireConnection(filename, key);
        },
        withClient: function (callback) {
            return withClient(filename, key, callback);
        },
        withTransaction: function (callback) {
            return withTransaction(filename, key, callback);
        }
    };
}
exports.db = db;
function insertOne(client, string, args) {
    return new Promise((callback, errback) => {
        client.run(string, args, function (err) {
            /* eslint-disable no-invalid-this */
            if (err) {
                errback(err);
                return;
            }
            if (this.lastID === undefined)
                errback(new Error("Row does not have ID"));
            else
                callback(this.lastID);
        });
    });
}
exports.insertOne = insertOne;
//# sourceMappingURL=sqlite.js.map