"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.finaliseExternalTables = exports.isIsoDateString = exports.isSQL = exports.getSqlQuery = exports.convertSqlType = exports.breakRowIdField = exports.convertRowId = exports.isRowId = exports.generateRowIdField = exports.breakExternalTableId = exports.buildExternalTableId = exports.isExternalTable = exports.SqlClients = void 0;
const datasource_1 = require("../definitions/datasource");
const utils_1 = require("../db/utils");
const constants_1 = require("../constants");
const DOUBLE_SEPARATOR = `${utils_1.SEPARATOR}${utils_1.SEPARATOR}`;
const ROW_ID_REGEX = /^\[.*]$/g;
const SQL_NUMBER_TYPE_MAP = {
    integer: constants_1.FieldTypes.NUMBER,
    int: constants_1.FieldTypes.NUMBER,
    bigint: constants_1.FieldTypes.NUMBER,
    decimal: constants_1.FieldTypes.NUMBER,
    smallint: constants_1.FieldTypes.NUMBER,
    real: constants_1.FieldTypes.NUMBER,
    float: constants_1.FieldTypes.NUMBER,
    numeric: constants_1.FieldTypes.NUMBER,
    mediumint: constants_1.FieldTypes.NUMBER,
    dec: constants_1.FieldTypes.NUMBER,
    double: constants_1.FieldTypes.NUMBER,
    fixed: constants_1.FieldTypes.NUMBER,
    "double precision": constants_1.FieldTypes.NUMBER,
    number: constants_1.FieldTypes.NUMBER,
    binary_float: constants_1.FieldTypes.NUMBER,
    binary_double: constants_1.FieldTypes.NUMBER,
    money: constants_1.FieldTypes.NUMBER,
    smallmoney: constants_1.FieldTypes.NUMBER,
};
const SQL_DATE_TYPE_MAP = {
    timestamp: constants_1.FieldTypes.DATETIME,
    time: constants_1.FieldTypes.DATETIME,
    datetime: constants_1.FieldTypes.DATETIME,
    smalldatetime: constants_1.FieldTypes.DATETIME,
    date: constants_1.FieldTypes.DATETIME,
};
const SQL_DATE_ONLY_TYPES = ["date"];
const SQL_TIME_ONLY_TYPES = ["time"];
const SQL_STRING_TYPE_MAP = {
    varchar: constants_1.FieldTypes.STRING,
    char: constants_1.FieldTypes.STRING,
    nchar: constants_1.FieldTypes.STRING,
    nvarchar: constants_1.FieldTypes.STRING,
    ntext: constants_1.FieldTypes.STRING,
    enum: constants_1.FieldTypes.STRING,
    blob: constants_1.FieldTypes.STRING,
    long: constants_1.FieldTypes.STRING,
    text: constants_1.FieldTypes.STRING,
};
const SQL_BOOLEAN_TYPE_MAP = {
    boolean: constants_1.FieldTypes.BOOLEAN,
    bit: constants_1.FieldTypes.BOOLEAN,
    tinyint: constants_1.FieldTypes.BOOLEAN,
};
const SQL_MISC_TYPE_MAP = {
    json: constants_1.FieldTypes.JSON,
};
const SQL_TYPE_MAP = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, SQL_NUMBER_TYPE_MAP), SQL_DATE_TYPE_MAP), SQL_STRING_TYPE_MAP), SQL_BOOLEAN_TYPE_MAP), SQL_MISC_TYPE_MAP);
var SqlClients;
(function (SqlClients) {
    SqlClients["MS_SQL"] = "mssql";
    SqlClients["POSTGRES"] = "pg";
    SqlClients["MY_SQL"] = "mysql2";
    SqlClients["ORACLE"] = "oracledb";
})(SqlClients = exports.SqlClients || (exports.SqlClients = {}));
function isExternalTable(tableId) {
    return tableId.includes(utils_1.DocumentTypes.DATASOURCE);
}
exports.isExternalTable = isExternalTable;
function buildExternalTableId(datasourceId, tableName) {
    return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`;
}
exports.buildExternalTableId = buildExternalTableId;
function breakExternalTableId(tableId) {
    if (!tableId) {
        return {};
    }
    const parts = tableId.split(DOUBLE_SEPARATOR);
    let datasourceId = parts.shift();
    // if they need joined
    let tableName = parts.join(DOUBLE_SEPARATOR);
    return { datasourceId, tableName };
}
exports.breakExternalTableId = breakExternalTableId;
function generateRowIdField(keyProps = []) {
    if (!Array.isArray(keyProps)) {
        keyProps = [keyProps];
    }
    // this conserves order and types
    // we have to swap the double quotes to single quotes for use in HBS statements
    // when using the literal helper the double quotes can break things
    return encodeURIComponent(JSON.stringify(keyProps).replace(/"/g, "'"));
}
exports.generateRowIdField = generateRowIdField;
function isRowId(field) {
    return (Array.isArray(field) ||
        (typeof field === "string" && field.match(ROW_ID_REGEX) != null));
}
exports.isRowId = isRowId;
function convertRowId(field) {
    if (Array.isArray(field)) {
        return field[0];
    }
    if (typeof field === "string" && field.match(ROW_ID_REGEX) != null) {
        return field.substring(1, field.length - 1);
    }
    return field;
}
exports.convertRowId = convertRowId;
// should always return an array
function breakRowIdField(_id) {
    if (!_id) {
        return [];
    }
    // have to replace on the way back as we swapped out the double quotes
    // when encoding, but JSON can't handle the single quotes
    const id = typeof _id === "string" ? _id : _id._id;
    const decoded = decodeURIComponent(id).replace(/'/g, '"');
    try {
        const parsed = JSON.parse(decoded);
        return Array.isArray(parsed) ? parsed : [parsed];
    }
    catch (err) {
        // wasn't json - likely was handlebars for a many to many
        return [_id];
    }
}
exports.breakRowIdField = breakRowIdField;
function convertSqlType(type) {
    let foundType = constants_1.FieldTypes.STRING;
    const lcType = type.toLowerCase();
    for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
        if (lcType.includes(external)) {
            foundType = internal;
            break;
        }
    }
    const schema = { type: foundType };
    if (foundType === constants_1.FieldTypes.DATETIME) {
        schema.dateOnly = SQL_DATE_ONLY_TYPES.includes(lcType);
        schema.timeOnly = SQL_TIME_ONLY_TYPES.includes(lcType);
    }
    return schema;
}
exports.convertSqlType = convertSqlType;
function getSqlQuery(query) {
    if (typeof query === "string") {
        return { sql: query };
    }
    else {
        return query;
    }
}
exports.getSqlQuery = getSqlQuery;
function isSQL(datasource) {
    if (!datasource || !datasource.source) {
        return false;
    }
    const SQL = [
        datasource_1.SourceNames.POSTGRES,
        datasource_1.SourceNames.SQL_SERVER,
        datasource_1.SourceNames.MYSQL,
        datasource_1.SourceNames.ORACLE,
    ];
    return SQL.indexOf(datasource.source) !== -1;
}
exports.isSQL = isSQL;
function isIsoDateString(str) {
    if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) {
        return false;
    }
    let d = new Date(str);
    return d.toISOString() === str;
}
exports.isIsoDateString = isIsoDateString;
/**
 * This function will determine whether a column is a relationship and whether it
 * is currently valid. The reason for the validity check is that tables can be deleted
 * outside of Budibase control and if this is the case it will break Budibase relationships.
 * The tableIds is a list passed down from the main finalise tables function, which is
 * based on the tables that have just been fetched. This will only really be used on subsequent
 * fetches to the first one - if the user is periodically refreshing Budibase knowledge of tables.
 * @param column The column to check, to see if it is a valid relationship.
 * @param tableIds The IDs of the tables which currently exist.
 */
function shouldCopyRelationship(column, tableIds) {
    return (column.type === constants_1.FieldTypes.LINK &&
        column.tableId &&
        tableIds.includes(column.tableId));
}
/**
 * Similar function to the shouldCopyRelationship function, but instead this looks for options and boolean
 * types. It is possible to switch a string -> options and a number -> boolean (and vice versus) need to make
 * sure that these get copied over when tables are fetched. Also checks whether they are still valid, if a
 * column has changed type in the external database then copying it over may not be possible.
 * @param column The column to check for options or boolean type.
 * @param fetchedColumn The fetched column to check for the type in the external database.
 */
function shouldCopySpecialColumn(column, fetchedColumn) {
    const specialTypes = [
        constants_1.FieldTypes.OPTIONS,
        constants_1.FieldTypes.LONGFORM,
        constants_1.FieldTypes.ARRAY,
        constants_1.FieldTypes.FORMULA,
    ];
    if (column && !fetchedColumn) {
        return true;
    }
    const fetchedIsNumber = !fetchedColumn || fetchedColumn.type === constants_1.FieldTypes.NUMBER;
    return (specialTypes.indexOf(column.type) !== -1 ||
        (fetchedIsNumber && column.type === constants_1.FieldTypes.BOOLEAN));
}
/**
 * Looks for columns which need to be copied over into the new table definitions, like relationships
 * and options types.
 * @param tableName The name of the table which is being checked.
 * @param table The specific table which is being checked.
 * @param entities All the tables that existed before - the old table definitions.
 * @param tableIds The IDs of the tables which exist now, to check if anything has been removed.
 */
function copyExistingPropsOver(tableName, table, entities, tableIds) {
    if (entities && entities[tableName]) {
        if (entities[tableName].primaryDisplay) {
            table.primaryDisplay = entities[tableName].primaryDisplay;
        }
        const existingTableSchema = entities[tableName].schema;
        for (let key in existingTableSchema) {
            if (!existingTableSchema.hasOwnProperty(key)) {
                continue;
            }
            const column = existingTableSchema[key];
            if (shouldCopyRelationship(column, tableIds) ||
                shouldCopySpecialColumn(column, table.schema[key])) {
                table.schema[key] = existingTableSchema[key];
            }
        }
    }
    return table;
}
/**
 * Look through the final table definitions to see if anything needs to be
 * copied over from the old and if any errors have occurred mark them so
 * that the user can be made aware.
 * @param tables The list of tables that have been retrieved from the external database.
 * @param entities The old list of tables, if there was any to look for definitions in.
 */
function finaliseExternalTables(tables, entities) {
    const invalidColumns = Object.values(constants_1.InvalidColumns);
    let finalTables = {};
    const errors = {};
    // @ts-ignore
    const tableIds = Object.values(tables).map(table => table._id);
    for (let [name, table] of Object.entries(tables)) {
        const schemaFields = Object.keys(table.schema);
        // make sure every table has a key
        if (table.primary == null || table.primary.length === 0) {
            errors[name] = constants_1.BuildSchemaErrors.NO_KEY;
            continue;
        }
        else if (schemaFields.find(field => invalidColumns.includes(field))) {
            errors[name] = constants_1.BuildSchemaErrors.INVALID_COLUMN;
            continue;
        }
        // make sure all previous props have been added back
        finalTables[name] = copyExistingPropsOver(name, table, entities, tableIds);
    }
    // sort the tables by name
    finalTables = Object.entries(finalTables)
        .sort(([a], [b]) => a.localeCompare(b))
        .reduce((r, [k, v]) => (Object.assign(Object.assign({}, r), { [k]: v })), {});
    return { tables: finalTables, errors };
}
exports.finaliseExternalTables = finaliseExternalTables;
