"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 });
exports.TableSaveFunctions = exports.hasTypeChanged = exports.areSwitchableTypes = exports.foreignKeyStructure = exports.generateJunctionTableName = exports.generateForeignKey = exports.checkForViewUpdates = exports.getTable = exports.getExternalTable = exports.getAllExternalTables = exports.getAllInternalTables = exports.checkStaticTables = exports.handleSearchIndexes = exports.handleDataImport = exports.makeSureTableUpToDate = exports.checkForColumnUpdates = exports.clearColumns = void 0;
const csvParser_1 = require("../../../utilities/csvParser");
const utils_1 = require("../../../db/utils");
const lodash_1 = require("lodash");
const constants_1 = require("../../../constants");
const rowProcessor_1 = require("../../../utilities/rowProcessor");
const constants_2 = require("../../../constants");
const utils_2 = require("../../../integrations/utils");
const utils_3 = require("../view/utils");
const viewBuilder_1 = __importDefault(require("../view/viewBuilder"));
const { getAppDB } = require("@devlego/backend-core/context");
const fp_1 = require("lodash/fp");
const pro_1 = require("@devlego/pro");
const backend_core_1 = require("@devlego/backend-core");
function clearColumns(table, columnNames) {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        const rows = yield db.allDocs((0, utils_1.getRowParams)(table._id, null, {
            include_docs: true,
        }));
        return db.bulkDocs(rows.rows.map(({ doc }) => {
            columnNames.forEach((colName) => delete doc[colName]);
            return doc;
        }));
    });
}
exports.clearColumns = clearColumns;
function checkForColumnUpdates(oldTable, updatedTable) {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        let updatedRows = [];
        const rename = updatedTable._rename;
        let deletedColumns = [];
        if (oldTable && oldTable.schema && updatedTable.schema) {
            deletedColumns = Object.keys(oldTable.schema).filter(colName => updatedTable.schema[colName] == null);
        }
        // check for renaming of columns or deleted columns
        if (rename || deletedColumns.length !== 0) {
            // Update all rows
            const rows = yield db.allDocs((0, utils_1.getRowParams)(updatedTable._id, null, {
                include_docs: true,
            }));
            const rawRows = rows.rows.map(({ doc }) => doc);
            updatedRows = rawRows.map((row) => {
                row = (0, fp_1.cloneDeep)(row);
                if (rename) {
                    row[rename.updated] = row[rename.old];
                    delete row[rename.old];
                }
                else if (deletedColumns.length !== 0) {
                    deletedColumns.forEach((colName) => delete row[colName]);
                }
                return row;
            });
            // cleanup any attachments from object storage for deleted attachment columns
            yield (0, rowProcessor_1.cleanupAttachments)(updatedTable, { oldTable, rows: rawRows });
            // Update views
            yield checkForViewUpdates(updatedTable, rename, deletedColumns);
            delete updatedTable._rename;
        }
        return { rows: updatedRows, table: updatedTable };
    });
}
exports.checkForColumnUpdates = checkForColumnUpdates;
// makes sure the passed in table isn't going to reset the auto ID
function makeSureTableUpToDate(table, tableToSave) {
    if (!table) {
        return tableToSave;
    }
    // sure sure rev is up to date
    tableToSave._rev = table._rev;
    // make sure auto IDs are always updated - these are internal
    // so the client may not know they have changed
    let field;
    let column;
    for ([field, column] of Object.entries(table.schema)) {
        if (column.autocolumn &&
            column.subtype === constants_1.AutoFieldSubTypes.AUTO_ID &&
            tableToSave.schema[field]) {
            tableToSave.schema[field].lastID = column.lastID;
        }
    }
    return tableToSave;
}
exports.makeSureTableUpToDate = makeSureTableUpToDate;
function handleDataImport(user, table, dataImport) {
    return __awaiter(this, void 0, void 0, function* () {
        if (!dataImport || !dataImport.csvString) {
            return table;
        }
        const db = getAppDB();
        // Populate the table with rows imported from CSV in a bulk update
        const data = yield (0, csvParser_1.transform)(Object.assign(Object.assign({}, dataImport), { existingTable: table }));
        let finalData = [];
        for (let i = 0; i < data.length; i++) {
            let row = data[i];
            row._id = (0, utils_1.generateRowID)(table._id);
            row.tableId = table._id;
            const processed = (0, rowProcessor_1.inputProcessing)(user, table, row, {
                noAutoRelationships: true,
            });
            table = processed.table;
            row = processed.row;
            let fieldName;
            let schema;
            for ([fieldName, schema] of Object.entries(table.schema)) {
                // check whether the options need to be updated for inclusion as part of the data import
                if (schema.type === constants_1.FieldTypes.OPTIONS &&
                    (!schema.constraints.inclusion ||
                        schema.constraints.inclusion.indexOf(row[fieldName]) === -1)) {
                    schema.constraints.inclusion = [
                        ...schema.constraints.inclusion,
                        row[fieldName],
                    ];
                }
            }
            finalData.push(row);
        }
        yield pro_1.quotas.addRows(finalData.length, () => db.bulkDocs(finalData));
        yield backend_core_1.events.rows.imported(table, "csv", finalData.length);
        return table;
    });
}
exports.handleDataImport = handleDataImport;
function handleSearchIndexes(table) {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        // create relevant search indexes
        if (table.indexes && table.indexes.length > 0) {
            const currentIndexes = yield db.getIndexes();
            const indexName = `search:${table._id}`;
            const existingIndex = currentIndexes.indexes.find((existing) => existing.name === indexName);
            if (existingIndex) {
                const currentFields = existingIndex.def.fields.map((field) => Object.keys(field)[0]);
                // if index fields have changed, delete the original index
                if (!(0, lodash_1.isEqual)(currentFields, table.indexes)) {
                    yield db.deleteIndex(existingIndex);
                    // create/recreate the index with fields
                    yield db.createIndex({
                        index: {
                            fields: table.indexes,
                            name: indexName,
                            ddoc: "search_ddoc",
                            type: "json",
                        },
                    });
                }
            }
            else {
                // create/recreate the index with fields
                yield db.createIndex({
                    index: {
                        fields: table.indexes,
                        name: indexName,
                        ddoc: "search_ddoc",
                        type: "json",
                    },
                });
            }
        }
        return table;
    });
}
exports.handleSearchIndexes = handleSearchIndexes;
function checkStaticTables(table) {
    // check user schema has all required elements
    if (table._id === utils_1.InternalTables.USER_METADATA) {
        for (let [key, schema] of Object.entries(constants_2.USERS_TABLE_SCHEMA.schema)) {
            // check if the schema exists on the table to be created/updated
            if (table.schema[key] == null) {
                table.schema[key] = schema;
            }
        }
    }
    return table;
}
exports.checkStaticTables = checkStaticTables;
class TableSaveFunctions {
    constructor({ user, oldTable, dataImport }) {
        this.db = getAppDB();
        this.user = user;
        this.oldTable = oldTable;
        this.dataImport = dataImport;
        // any rows that need updated
        this.rows = [];
    }
    // before anything is done
    before(table) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.oldTable) {
                table = makeSureTableUpToDate(this.oldTable, table);
            }
            table = checkStaticTables(table);
            return table;
        });
    }
    // when confirmed valid
    mid(table) {
        return __awaiter(this, void 0, void 0, function* () {
            let response = yield checkForColumnUpdates(this.oldTable, table);
            this.rows = this.rows.concat(response.rows);
            return table;
        });
    }
    // after saving
    after(table) {
        return __awaiter(this, void 0, void 0, function* () {
            table = yield handleSearchIndexes(table);
            yield handleDataImport(this.user, table, this.dataImport);
            return table;
        });
    }
    getUpdatedRows() {
        return this.rows;
    }
}
function getAllInternalTables() {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        const internalTables = yield db.allDocs((0, utils_1.getTableParams)(null, {
            include_docs: true,
        }));
        return internalTables.rows.map((tableDoc) => (Object.assign(Object.assign({}, tableDoc.doc), { type: "internal", sourceId: utils_1.BudibaseInternalDB._id })));
    });
}
exports.getAllInternalTables = getAllInternalTables;
function getAllExternalTables(datasourceId) {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        const datasource = yield db.get(datasourceId);
        if (!datasource || !datasource.entities) {
            throw "Datasource is not configured fully.";
        }
        return datasource.entities;
    });
}
exports.getAllExternalTables = getAllExternalTables;
function getExternalTable(datasourceId, tableName) {
    return __awaiter(this, void 0, void 0, function* () {
        const entities = yield getAllExternalTables(datasourceId);
        return entities[tableName];
    });
}
exports.getExternalTable = getExternalTable;
function getTable(tableId) {
    return __awaiter(this, void 0, void 0, function* () {
        const db = getAppDB();
        if ((0, utils_2.isExternalTable)(tableId)) {
            let { datasourceId, tableName } = (0, utils_2.breakExternalTableId)(tableId);
            const datasource = yield db.get(datasourceId);
            const table = yield getExternalTable(datasourceId, tableName);
            return Object.assign(Object.assign({}, table), { sql: (0, utils_2.isSQL)(datasource) });
        }
        else {
            return db.get(tableId);
        }
    });
}
exports.getTable = getTable;
function checkForViewUpdates(table, rename, deletedColumns) {
    return __awaiter(this, void 0, void 0, function* () {
        const views = yield (0, utils_3.getViews)();
        const tableViews = views.filter(view => view.meta.tableId === table._id);
        // Check each table view to see if impacted by this table action
        for (let view of tableViews) {
            let needsUpdated = false;
            // First check for renames, otherwise check for deletions
            if (rename) {
                // Update calculation field if required
                if (view.meta.field === rename.old) {
                    view.meta.field = rename.updated;
                    needsUpdated = true;
                }
                // Update group by field if required
                if (view.meta.groupBy === rename.old) {
                    view.meta.groupBy = rename.updated;
                    needsUpdated = true;
                }
                // Update filters if required
                if (view.meta.filters) {
                    view.meta.filters.forEach((filter) => {
                        if (filter.key === rename.old) {
                            filter.key = rename.updated;
                            needsUpdated = true;
                        }
                    });
                }
            }
            else if (deletedColumns) {
                deletedColumns.forEach((column) => {
                    // Remove calculation statement if required
                    if (view.meta.field === column) {
                        delete view.meta.field;
                        delete view.meta.calculation;
                        delete view.meta.groupBy;
                        needsUpdated = true;
                    }
                    // Remove group by field if required
                    if (view.meta.groupBy === column) {
                        delete view.meta.groupBy;
                        needsUpdated = true;
                    }
                    // Remove filters referencing deleted field if required
                    if (view.meta.filters && view.meta.filters.length) {
                        const initialLength = view.meta.filters.length;
                        view.meta.filters = view.meta.filters.filter((filter) => {
                            return filter.key !== column;
                        });
                        if (initialLength !== view.meta.filters.length) {
                            needsUpdated = true;
                        }
                    }
                });
            }
            // Update view if required
            if (needsUpdated) {
                const newViewTemplate = (0, viewBuilder_1.default)(view.meta);
                yield (0, utils_3.saveView)(null, view.name, newViewTemplate);
                if (!newViewTemplate.meta.schema) {
                    newViewTemplate.meta.schema = table.schema;
                }
                table.views[view.name] = newViewTemplate.meta;
            }
        }
    });
}
exports.checkForViewUpdates = checkForViewUpdates;
function generateForeignKey(column, relatedTable) {
    return `fk_${relatedTable.name}_${column.fieldName}`;
}
exports.generateForeignKey = generateForeignKey;
function generateJunctionTableName(column, table, relatedTable) {
    return `jt_${table.name}_${relatedTable.name}_${column.name}_${column.fieldName}`;
}
exports.generateJunctionTableName = generateJunctionTableName;
function foreignKeyStructure(keyName, meta = null) {
    const structure = {
        type: constants_1.FieldTypes.NUMBER,
        constraints: {},
        name: keyName,
    };
    if (meta) {
        structure.meta = meta;
    }
    return structure;
}
exports.foreignKeyStructure = foreignKeyStructure;
function areSwitchableTypes(type1, type2) {
    if (constants_2.SwitchableTypes.indexOf(type1) === -1 &&
        constants_2.SwitchableTypes.indexOf(type2) === -1) {
        return false;
    }
    for (let option of constants_2.CanSwitchTypes) {
        const index1 = option.indexOf(type1), index2 = option.indexOf(type2);
        if (index1 !== -1 && index2 !== -1 && index1 !== index2) {
            return true;
        }
    }
    return false;
}
exports.areSwitchableTypes = areSwitchableTypes;
function hasTypeChanged(table, oldTable) {
    if (!oldTable) {
        return false;
    }
    let key;
    let field;
    for ([key, field] of Object.entries(oldTable.schema)) {
        const oldType = field.type;
        if (!table.schema[key]) {
            continue;
        }
        const newType = table.schema[key].type;
        if (oldType !== newType && !areSwitchableTypes(oldType, newType)) {
            return true;
        }
    }
    return false;
}
exports.hasTypeChanged = hasTypeChanged;
const _TableSaveFunctions = TableSaveFunctions;
exports.TableSaveFunctions = _TableSaveFunctions;
