"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const datasource_1 = require("../definitions/datasource");
const utils_1 = require("./utils");
const oracledb_1 = __importDefault(require("oracledb"));
const sql_1 = __importDefault(require("./base/sql"));
const constants_1 = require("../constants");
var OracleModule;
(function (OracleModule) {
    oracledb_1.default.outFormat = oracledb_1.default.OUT_FORMAT_OBJECT;
    const SCHEMA = {
        docs: "https://github.com/oracle/node-oracledb",
        plus: true,
        friendlyName: "Oracle",
        type: "Relational",
        description: "Oracle Database is an object-relational database management system developed by Oracle Corporation",
        datasource: {
            host: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                default: "localhost",
                required: true,
            },
            port: {
                type: datasource_1.DatasourceFieldTypes.NUMBER,
                required: true,
                default: 1521,
            },
            database: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                required: true,
            },
            user: {
                type: datasource_1.DatasourceFieldTypes.STRING,
                required: true,
            },
            password: {
                type: datasource_1.DatasourceFieldTypes.PASSWORD,
                required: true,
            },
        },
        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,
            },
        },
    };
    const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"];
    const OracleContraintTypes = {
        PRIMARY: "P",
        NOT_NULL_OR_CHECK: "C",
        FOREIGN_KEY: "R",
        UNIQUE: "U",
    };
    class OracleIntegration extends sql_1.default {
        constructor(config) {
            super(utils_1.SqlClients.ORACLE);
            this.index = 1;
            this.tables = {};
            this.schemaErrors = {};
            this.COLUMNS_SQL = `
      SELECT
        tabs.table_name,
        cols.column_name,
        cols.data_type,
        cols.data_default,
        cols.column_id,
        cons.constraint_name,
        cons.constraint_type,
        cons.r_constraint_name,
        cons.search_condition
      FROM
        user_tables tabs
      JOIN 
        user_tab_columns cols
        ON tabs.table_name = cols.table_name 
      LEFT JOIN 
        user_cons_columns col_cons
        ON cols.column_name = col_cons.column_name
        AND cols.table_name = col_cons.table_name
      LEFT JOIN 
        user_constraints cons
        ON col_cons.constraint_name = cons.constraint_name
        AND cons.table_name = cols.table_name
      WHERE
        (cons.status = 'ENABLED'
          OR cons.status IS NULL)
    `;
            this.getConnection = () => __awaiter(this, void 0, void 0, function* () {
                //connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
                const connectString = `${this.config.host}:${this.config.port || 1521}/${this.config.database}`;
                const attributes = {
                    user: this.config.user,
                    password: this.config.password,
                    connectString,
                };
                return oracledb_1.default.getConnection(attributes);
            });
            this.config = config;
        }
        getBindingIdentifier() {
            return `:${this.index++}`;
        }
        getStringConcat(parts) {
            return parts.join(" || ");
        }
        /**
         * Map the flat tabular columns and constraints data into a nested object
         */
        mapColumns(result) {
            const oracleTables = {};
            if (result.rows) {
                result.rows.forEach(row => {
                    const tableName = row.TABLE_NAME;
                    const columnName = row.COLUMN_NAME;
                    const dataType = row.DATA_TYPE;
                    const dataDefault = row.DATA_DEFAULT;
                    const columnId = row.COLUMN_ID;
                    const constraintName = row.CONSTRAINT_NAME;
                    const constraintType = row.CONSTRAINT_TYPE;
                    const relatedConstraintName = row.R_CONSTRAINT_NAME;
                    const searchCondition = row.SEARCH_CONDITION;
                    let table = oracleTables[tableName];
                    if (!table) {
                        table = {
                            name: tableName,
                            columns: {},
                        };
                        oracleTables[tableName] = table;
                    }
                    let column = table.columns[columnName];
                    if (!column) {
                        column = {
                            name: columnName,
                            type: dataType,
                            default: dataDefault,
                            id: columnId,
                            constraints: {},
                        };
                        table.columns[columnName] = column;
                    }
                    if (constraintName && constraintType) {
                        let constraint = column.constraints[constraintName];
                        if (!constraint) {
                            constraint = {
                                name: constraintName,
                                type: constraintType,
                                relatedConstraintName: relatedConstraintName,
                                searchCondition: searchCondition,
                            };
                        }
                        column.constraints[constraintName] = constraint;
                    }
                });
            }
            return oracleTables;
        }
        static isSupportedColumn(column) {
            return !UNSUPPORTED_TYPES.includes(column.type);
        }
        static isAutoColumn(column) {
            return !!(column.default && column.default.toLowerCase().includes("nextval"));
        }
        /**
         * No native boolean in oracle. Best we can do is to check if a manual 1 or 0 number constraint has been set up
         * This matches the default behaviour for generating DDL used in knex.
         */
        isBooleanType(column) {
            return (column.type.toLowerCase() === "number" &&
                Object.values(column.constraints).filter(c => {
                    if (c.type === OracleContraintTypes.NOT_NULL_OR_CHECK &&
                        c.searchCondition) {
                        const condition = c.searchCondition
                            .replace(/\s/g, "") // remove spaces
                            .replace(/[']+/g, ""); // remove quotes
                        if (condition.includes("in(0,1)") ||
                            condition.includes("in(1,0)")) {
                            return true;
                        }
                    }
                    return false;
                }).length > 0);
        }
        internalConvertType(column) {
            if (this.isBooleanType(column)) {
                return { type: constants_1.FieldTypes.BOOLEAN };
            }
            return (0, utils_1.convertSqlType)(column.type);
        }
        /**
         * Fetches the tables from the oracle 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* () {
                const columnsResponse = yield this.internalQuery({
                    sql: this.COLUMNS_SQL,
                });
                const oracleTables = this.mapColumns(columnsResponse);
                const tables = {};
                // iterate each table
                Object.values(oracleTables).forEach(oracleTable => {
                    let table = tables[oracleTable.name];
                    if (!table) {
                        table = {
                            _id: (0, utils_1.buildExternalTableId)(datasourceId, oracleTable.name),
                            primary: [],
                            name: oracleTable.name,
                            schema: {},
                        };
                        tables[oracleTable.name] = table;
                    }
                    // iterate each column on the table
                    Object.values(oracleTable.columns)
                        // remove columns that we can't read / save
                        .filter(oracleColumn => OracleIntegration.isSupportedColumn(oracleColumn))
                        // match the order of the columns in the db
                        .sort((c1, c2) => c1.id - c2.id)
                        .forEach(oracleColumn => {
                        const columnName = oracleColumn.name;
                        let fieldSchema = table.schema[columnName];
                        if (!fieldSchema) {
                            fieldSchema = Object.assign({ autocolumn: OracleIntegration.isAutoColumn(oracleColumn), name: columnName }, this.internalConvertType(oracleColumn));
                            table.schema[columnName] = fieldSchema;
                        }
                        // iterate each constraint on the column
                        Object.values(oracleColumn.constraints).forEach(oracleConstraint => {
                            if (oracleConstraint.type === OracleContraintTypes.PRIMARY) {
                                table.primary.push(columnName);
                            }
                        });
                    });
                });
                const final = (0, utils_1.finaliseExternalTables)(tables, entities);
                this.tables = final.tables;
                this.schemaErrors = final.errors;
            });
        }
        internalQuery(query) {
            return __awaiter(this, void 0, void 0, function* () {
                let connection;
                try {
                    this.index = 1;
                    connection = yield this.getConnection();
                    const options = { autoCommit: true };
                    const bindings = query.bindings || [];
                    return yield connection.execute(query.sql, bindings, options);
                }
                finally {
                    if (connection) {
                        try {
                            yield connection.close();
                        }
                        catch (err) {
                            console.error(err);
                        }
                    }
                }
            });
        }
        create(query) {
            return __awaiter(this, void 0, void 0, function* () {
                const response = yield this.internalQuery((0, utils_1.getSqlQuery)(query));
                return response.rows && 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 ? 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 && 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 && response.rows.length
                    ? response.rows
                    : [{ deleted: true }];
            });
        }
        query(json) {
            var _a, _b, _c;
            return __awaiter(this, void 0, void 0, function* () {
                const operation = this._operation(json);
                const input = this._query(json, { disableReturning: true });
                if (Array.isArray(input)) {
                    const responses = [];
                    for (let query of input) {
                        responses.push(yield this.internalQuery(query));
                    }
                    return responses;
                }
                else {
                    // read the row to be deleted up front for the return
                    let deletedRows;
                    if (operation === datasource_1.Operation.DELETE) {
                        const queryFn = (query) => this.internalQuery(query);
                        deletedRows = yield this.getReturningRow(queryFn, json);
                    }
                    // run the query
                    const response = yield this.internalQuery(input);
                    // get the results or return the created / updated / deleted row
                    if ((_a = deletedRows === null || deletedRows === void 0 ? void 0 : deletedRows.rows) === null || _a === void 0 ? void 0 : _a.length) {
                        return deletedRows.rows;
                    }
                    else if ((_b = response.rows) === null || _b === void 0 ? void 0 : _b.length) {
                        return response.rows;
                    }
                    else {
                        // get the last row that was updated
                        if (response.lastRowid &&
                            ((_c = json.endpoint) === null || _c === void 0 ? void 0 : _c.entityId) &&
                            operation !== datasource_1.Operation.DELETE) {
                            const lastRow = yield this.internalQuery({
                                sql: `SELECT * FROM \"${json.endpoint.entityId}\" WHERE ROWID = '${response.lastRowid}'`,
                            });
                            return lastRow.rows;
                        }
                        else {
                            return [{ [operation.toLowerCase()]: true }];
                        }
                    }
                }
            });
        }
    }
    module.exports = {
        schema: SCHEMA,
        integration: OracleIntegration,
    };
})(OracleModule || (OracleModule = {}));
