"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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const datasource_1 = require("../definitions/datasource");
const utils_1 = require("./utils");
var PostgresModule;
(function (PostgresModule) {
    const { Client, types } = require("pg");
    const Sql = require("./base/sql");
    const { escapeDangerousCharacters } = require("../utilities");
    // Return "date" and "timestamp" types as plain strings.
    // This lets us reference the original stored timezone.
    // types is undefined when running in a test env for some reason.
    if (types) {
        types.setTypeParser(1114, (val) => val); // timestamp
        types.setTypeParser(1082, (val) => val); // date
        types.setTypeParser(1184, (val) => val); // timestampz
    }
    const JSON_REGEX = /'{.*}'::json/s;
    const SCHEMA = {
        docs: "https://node-postgres.com",
        plus: true,
        friendlyName: "PostgreSQL",
        type: "Relational",
        description: "PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
        datasource: {
            host: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "localhost",
                required: true,
            },
            port: {
                type: datasource_1.DatasourceFieldTypes.NUMBER,
                required: true,
                default: 5432,
            },
            database: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "postgres",
                required: true,
            },
            user: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "root",
                required: true,
            },
            password: {
                type: datasource_1.DatasourceFieldTypes.PASSWORD,
                default: "root",
                required: true,
            },
            schema: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "public",
                required: true,
            },
            ssl: {
                type: datasource_1.DatasourceFieldTypes.BOOLEAN,
                default: false,
                required: false,
            },
            rejectUnauthorized: {
                type: datasource_1.DatasourceFieldTypes.BOOLEAN,
                default: false,
                required: false,
            },
            ca: {
                type: datasource_1.DatasourceFieldTypes.LONGFORM,
                default: false,
                required: false,
            },
        },
        query: {
            create: {
                type: datasource_1.QueryTypes.SQL,
            },
            read: {
                type: datasource_1.QueryTypes.SQL,
            },
            update: {
                type: datasource_1.QueryTypes.SQL,
            },
            delete: {
                type: datasource_1.QueryTypes.SQL,
            },
        },
    };
    class PostgresIntegration extends Sql {
        constructor(config) {
            super(utils_1.SqlClients.POSTGRES);
            this.index = 1;
            this.tables = {};
            this.schemaErrors = {};
            this.PRIMARY_KEYS_SQL = `
    select tc.table_schema, tc.table_name, kc.column_name as primary_key 
    from information_schema.table_constraints tc
    join 
      information_schema.key_column_usage kc on kc.table_name = tc.table_name 
      and kc.table_schema = tc.table_schema 
      and kc.constraint_name = tc.constraint_name
    where tc.constraint_type = 'PRIMARY KEY';
    `;
            this.config = config;
            let newConfig = Object.assign(Object.assign({}, this.config), { ssl: this.config.ssl
                    ? {
                        rejectUnauthorized: this.config.rejectUnauthorized,
                        ca: this.config.ca,
                    }
                    : undefined });
            this.client = new Client(newConfig);
            this.open = false;
        }
        getBindingIdentifier() {
            return `$${this.index++}`;
        }
        getStringConcat(parts) {
            return parts.join(" || ");
        }
        openConnection() {
            return __awaiter(this, void 0, void 0, function* () {
                yield this.client.connect();
                if (!this.config.schema) {
                    this.config.schema = "public";
                }
                this.client.query(`SET search_path TO ${this.config.schema}`);
                this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`;
                this.open = true;
            });
        }
        closeConnection() {
            const pg = this;
            return new Promise((resolve, reject) => {
                this.client.end((err) => {
                    pg.open = false;
                    if (err) {
                        reject(err);
                    }
                    else {
                        resolve();
                    }
                });
            });
        }
        internalQuery(query, close = true) {
            return __awaiter(this, void 0, void 0, function* () {
                if (!this.open) {
                    yield this.openConnection();
                }
                const client = this.client;
                this.index = 1;
                // need to handle a specific issue with json data types in postgres,
                // new lines inside the JSON data will break it
                if (query && query.sql) {
                    const matches = query.sql.match(JSON_REGEX);
                    if (matches && matches.length > 0) {
                        for (let match of matches) {
                            const escaped = escapeDangerousCharacters(match);
                            query.sql = query.sql.replace(match, escaped);
                        }
                    }
                }
                try {
                    return yield client.query(query.sql, query.bindings || []);
                }
                catch (err) {
                    yield this.closeConnection();
                    // @ts-ignore
                    throw new Error(err);
                }
                finally {
                    if (close) {
                        yield this.closeConnection();
                    }
                }
            });
        }
        /**
         * Fetches the tables from the postgres table and assigns them to the datasource.
         * @param {*} datasourceId - datasourceId to fetch
         * @param entities - the tables that are to be built
         */
        buildSchema(datasourceId, entities) {
            return __awaiter(this, void 0, void 0, function* () {
                let tableKeys = {};
                yield this.openConnection();
                try {
                    const primaryKeysResponse = yield this.client.query(this.PRIMARY_KEYS_SQL);
                    for (let table of primaryKeysResponse.rows) {
                        const tableName = table.table_name;
                        if (!tableKeys[tableName]) {
                            tableKeys[tableName] = [];
                        }
                        const key = table.column_name || table.primary_key;
                        // only add the unique keys
                        if (key && tableKeys[tableName].indexOf(key) === -1) {
                            tableKeys[tableName].push(key);
                        }
                    }
                }
                catch (err) {
                    tableKeys = {};
                }
                try {
                    const columnsResponse = yield this.client.query(this.COLUMNS_SQL);
                    const tables = {};
                    for (let column of columnsResponse.rows) {
                        const tableName = column.table_name;
                        const columnName = column.column_name;
                        // table key doesn't exist yet
                        if (!tables[tableName] || !tables[tableName].schema) {
                            tables[tableName] = {
                                _id: (0, utils_1.buildExternalTableId)(datasourceId, tableName),
                                primary: tableKeys[tableName] || [],
                                name: tableName,
                                schema: {},
                            };
                        }
                        const identity = !!(column.identity_generation ||
                            column.identity_start ||
                            column.identity_increment);
                        const hasDefault = typeof column.column_default === "string" &&
                            column.column_default.startsWith("nextval");
                        const isGenerated = column.is_generated && column.is_generated !== "NEVER";
                        const isAuto = hasDefault || identity || isGenerated;
                        tables[tableName].schema[columnName] = Object.assign(Object.assign({ autocolumn: isAuto, name: columnName }, (0, utils_1.convertSqlType)(column.data_type)), { externalType: column.data_type });
                    }
                    const final = (0, utils_1.finaliseExternalTables)(tables, entities);
                    this.tables = final.tables;
                    this.schemaErrors = final.errors;
                }
                catch (err) {
                    // @ts-ignore
                    throw new Error(err);
                }
                finally {
                    yield this.closeConnection();
                }
            });
        }
        create(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const response = yield this.internalQuery((0, utils_1.getSqlQuery)(query));
                return response.rows.length ? response.rows : [{ created: true }];
            });
        }
        read(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const response = yield this.internalQuery((0, utils_1.getSqlQuery)(query));
                return response.rows;
            });
        }
        update(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const response = yield this.internalQuery((0, utils_1.getSqlQuery)(query));
                return response.rows.length ? response.rows : [{ updated: true }];
            });
        }
        delete(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const response = yield this.internalQuery((0, utils_1.getSqlQuery)(query));
                return response.rows.length ? response.rows : [{ deleted: true }];
            });
        }
        query(json) {
            return __awaiter(this, void 0, void 0, function* () {
                const operation = this._operation(json).toLowerCase();
                const input = this._query(json);
                if (Array.isArray(input)) {
                    const responses = [];
                    for (let query of input) {
                        responses.push(yield this.internalQuery(query, false));
                    }
                    yield this.closeConnection();
                    return responses;
                }
                else {
                    const response = yield this.internalQuery(input);
                    return response.rows.length ? response.rows : [{ [operation]: true }];
                }
            });
        }
    }
    module.exports = {
        schema: SCHEMA,
        integration: PostgresIntegration,
    };
})(PostgresModule || (PostgresModule = {}));
