"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __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.ResourceBridge = void 0;
const searchValidator_js_1 = __importDefault(require("../../validation/searchValidator.js"));
const hdbError_js_1 = require("../../utility/errors/hdbError.js");
const databases_ts_1 = require("../../resources/databases.js");
const insertUpdateValidate_js_1 = __importDefault(require("./bridgeUtility/insertUpdateValidate.js"));
const hdbTerms_ts_1 = require("../../utility/hdbTerms.js");
const signalling = __importStar(require("../../utility/signalling.js"));
const itc_js_1 = require("../../server/threads/itc.js");
const common_utils_js_1 = require("../../utility/common_utils.js");
const transaction_ts_1 = require("../../resources/transaction.js");
const tracked_ts_1 = require("../../resources/tracked.js");
const harper_logger_js_1 = require("../../utility/logging/harper_logger.js");
const rocksdb_js_1 = require("@harperfast/rocksdb-js");
const BridgeMethods_js_1 = __importDefault(require("./BridgeMethods.js"));
const lmdbGetBackup_js_1 = __importDefault(require("./lmdbBridge/lmdbMethods/lmdbGetBackup.js"));
const DeleteTransactionLogsBeforeResults_ts_1 = require("./DeleteTransactionLogsBeforeResults.js");
const { HDB_ERROR_MSGS } = hdbError_js_1.hdbErrors;
const DEFAULT_DATABASE = 'data';
const DELETE_CHUNK = 10000;
const DELETE_PAUSE_MS = 10;
/**
 * Bridge between the operations/REST/SQL API layer and the underlying resource layer.
 * Handles search (by conditions, hash, or value), schema/table/attribute CRUD, record
 * create/update/upsert/delete, audit log read/purge, and backup operations.
 */
class ResourceBridge extends BridgeMethods_js_1.default {
    async searchByConditions(searchObject) {
        if (searchObject.select !== undefined)
            searchObject.get_attributes = searchObject.select;
        const table = getTable(searchObject);
        if (!table) {
            throw new hdbError_js_1.ClientError(`Table ${searchObject.table} not found`);
        }
        searchObject.conditions = searchObject.conditions.map(mapCondition);
        function mapCondition(condition) {
            if ('conditions' in condition && condition.conditions) {
                condition.conditions = condition.conditions.map(mapCondition);
                return condition;
            }
            else {
                const c = condition;
                return {
                    attribute: c.attribute ?? c.search_attribute,
                    comparator: c.comparator ?? c.search_type,
                    value: c.value !== undefined ? c.value : c.search_value, // null is valid value
                };
            }
        }
        const validationError = (0, searchValidator_js_1.default)(searchObject, 'conditions');
        if (validationError) {
            throw (0, hdbError_js_1.handleHDBError)(validationError, validationError.message, 400, undefined, undefined, true);
        }
        return table.search({
            conditions: searchObject.conditions,
            //set the operator to always be lowercase for later evaluations
            operator: searchObject.operator ? searchObject.operator.toLowerCase() : undefined,
            limit: searchObject.limit,
            offset: searchObject.offset,
            reverse: searchObject.reverse,
            select: getSelect(searchObject, table),
            sort: searchObject.sort,
            allowFullScan: true, // operations API can do full scans by default, but REST is more cautious about what it allows
        }, {
            onlyIfCached: searchObject.onlyIfCached,
            noCacheStore: searchObject.noCacheStore,
            noCache: searchObject.noCache,
            replicateFrom: searchObject.replicateFrom,
        });
    }
    /**
     * Writes new table data to the system tables creates the environment file and creates two datastores to track created and updated
     * timestamps for new table data.
     * @param tableSystemData
     * @param tableCreateObj
     */
    async createTable(tableSystemData, tableCreateObj) {
        let attributes = tableCreateObj.attributes;
        const schemaDefined = Boolean(attributes);
        const primaryKeyName = tableCreateObj.primary_key || tableCreateObj.hash_attribute;
        if (attributes) {
            // allow for attributes to be specified, but do some massaging to make sure they are in the right form
            for (const attribute of attributes) {
                if (attribute.is_primary_key) {
                    attribute.isPrimaryKey = true;
                    delete attribute.is_primary_key;
                }
                else if (attribute.name === primaryKeyName && primaryKeyName)
                    attribute.isPrimaryKey = true;
            }
        }
        else {
            // legacy default schema for tables created through operations API without attributes
            if (!primaryKeyName)
                throw new hdbError_js_1.ClientError('A primary key must be specified with a `primary_key` property or with `attributes`');
            attributes = [
                { name: primaryKeyName, isPrimaryKey: true },
                { name: '__createdtime__', indexed: true },
                { name: '__updatedtime__', indexed: true },
            ];
        }
        (0, databases_ts_1.table)({
            database: tableCreateObj.database ?? tableCreateObj.schema,
            table: tableCreateObj.table,
            attributes,
            schemaDefined,
            expiration: tableCreateObj.expiration,
            audit: tableCreateObj.audit,
        });
    }
    async createAttribute(createAttributeObj) {
        await getTable(createAttributeObj).addAttributes([
            {
                name: createAttributeObj.attribute,
                indexed: createAttributeObj.indexed ?? true,
            },
        ]);
        return `attribute ${createAttributeObj.schema}.${createAttributeObj.table}.${createAttributeObj.attribute} successfully created.`;
    }
    async dropAttribute(dropAttributeObj) {
        const Table = getTable(dropAttributeObj);
        await Table.removeAttributes([dropAttributeObj.attribute]);
        if (!Table.schemaDefined) {
            // legacy behavior of deleting all the property values
            const property = dropAttributeObj.attribute;
            let resolution;
            const deleteRecord = (key, record, version) => {
                record = { ...record };
                delete record[property];
                return Table.primaryStore.put(key, record, version);
            };
            for (const { key, value: record, version } of Table.primaryStore.getRange({ start: true, versions: true })) {
                resolution = deleteRecord(key, record, version);
                await new Promise((resolve) => setImmediate(resolve));
            }
            await resolution;
        }
        return `successfully deleted ${dropAttributeObj.schema}.${dropAttributeObj.table}.${dropAttributeObj.attribute}`;
    }
    dropTable(dropTableObject) {
        return getTable(dropTableObject).dropTable();
    }
    createSchema(createSchemaObj) {
        (0, databases_ts_1.database)({
            database: createSchemaObj.schema,
            table: null,
        });
        return signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, hdbTerms_ts_1.OPERATIONS_ENUM.CREATE_SCHEMA, createSchemaObj.schema));
    }
    async dropSchema(dropSchemaObj) {
        await (0, databases_ts_1.dropDatabase)(dropSchemaObj.schema);
        signalling.signalSchemaChange(new itc_js_1.SchemaEventMsg(process.pid, hdbTerms_ts_1.OPERATIONS_ENUM.DROP_SCHEMA, dropSchemaObj.schema));
    }
    async updateRecords(updateObj) {
        updateObj.requires_existing = true;
        return this.upsertRecords(updateObj);
    }
    async createRecords(updateObj) {
        updateObj.requires_no_existing = true;
        return this.upsertRecords(updateObj);
    }
    async upsertRecords(upsertObj) {
        const { attributes } = (0, insertUpdateValidate_js_1.default)(upsertObj);
        let new_attributes;
        const Table = (0, databases_ts_1.getDatabases)()[upsertObj.schema][upsertObj.table];
        const context = {
            user: upsertObj.hdb_user,
            expiresAt: upsertObj.expiresAt,
            originatingOperation: upsertObj.operation,
        };
        if (upsertObj.replicateTo)
            context.replicateTo = upsertObj.replicateTo;
        if (upsertObj.replicatedConfirmation)
            context.replicatedConfirmation = upsertObj.replicatedConfirmation;
        return (0, transaction_ts_1.transaction)(context, async (transaction) => {
            if (!Table.schemaDefined) {
                new_attributes = [];
                for (const attribute_name of attributes) {
                    const existingAttribute = Table.attributes.find((existingAttribute) => existingAttribute.name == attribute_name);
                    if (!existingAttribute) {
                        new_attributes.push(attribute_name);
                    }
                }
                if (new_attributes.length > 0) {
                    await Table.addAttributes(new_attributes.map((name) => ({
                        name,
                        indexed: true,
                    })));
                }
            }
            const keys = [];
            const skipped = [];
            for (const record of upsertObj.records) {
                const id = record[Table.primaryKey];
                let existingRecord = id != undefined && (await Table.get(id, context));
                if ((upsertObj.requires_existing && !existingRecord) || (upsertObj.requires_no_existing && existingRecord)) {
                    skipped.push(record[Table.primaryKey]);
                    continue;
                }
                if (existingRecord)
                    existingRecord = (0, tracked_ts_1.collapseData)(existingRecord);
                for (const key in record) {
                    if (Object.prototype.hasOwnProperty.call(record, key)) {
                        let value = record[key];
                        if (typeof value === 'function') {
                            try {
                                const valueResults = value([[existingRecord]]);
                                if (Array.isArray(valueResults)) {
                                    value = valueResults[0].func_val;
                                    record[key] = value;
                                }
                            }
                            catch (error) {
                                error.message += 'Trying to set key ' + key + ' on object' + JSON.stringify(record);
                                throw error;
                            }
                        }
                    }
                }
                await (id == undefined
                    ? Table.create(record, context)
                    : existingRecord
                        ? Table.patch(record, context)
                        : Table.put(record, context));
                keys.push(record[Table.primaryKey]);
            }
            return {
                txn_time: transaction.timestamp,
                written_hashes: keys,
                new_attributes,
                skipped_hashes: skipped,
            };
        });
    }
    async deleteRecords(deleteObj) {
        const Table = (0, databases_ts_1.getDatabases)()[deleteObj.schema][deleteObj.table];
        const context = { user: deleteObj.hdb_user };
        if (deleteObj.replicateTo)
            context.replicateTo = deleteObj.replicateTo;
        if (deleteObj.replicatedConfirmation)
            context.replicatedConfirmation = deleteObj.replicatedConfirmation;
        return (0, transaction_ts_1.transaction)(context, async (transaction) => {
            const ids = deleteObj.ids || deleteObj.hash_values || deleteObj.records.map((record) => record[Table.primaryKey]);
            const deleted = [];
            const skipped = [];
            for (const id of ids) {
                if (await Table.delete(id, context))
                    deleted.push(id);
                else
                    skipped.push(id);
            }
            return createDeleteResponse(deleted, skipped, transaction.timestamp);
        });
    }
    /**
     * Deletes all records in a schema.table that fall behind a passed date.
     * @param deleteObj
     * {
     *     operation: 'delete_records_before' <string>,
     *     date: ISO-8601 format YYYY-MM-DD <string>,
     *     schema: Schema where table resides <string>,
     *     table: Table to delete records from <string>,
     * }
     * @returns {undefined}
     */
    async deleteRecordsBefore(deleteObj) {
        const Table = (0, databases_ts_1.getDatabases)()[deleteObj.schema][deleteObj.table];
        if (!Table.createdTimeProperty) {
            throw new hdbError_js_1.ClientError(`Table must have a '__createdtime__' attribute or @createdTime timestamp defined to perform this operation`);
        }
        const recordsToDelete = await Table.search({
            conditions: [
                {
                    attribute: Table.createdTimeProperty.name,
                    value: Date.parse(deleteObj.date),
                    comparator: hdbTerms_ts_1.VALUE_SEARCH_COMPARATORS.LESS,
                },
            ],
        });
        let deleteCalled = false;
        const deletedIds = [];
        const skippedIds = [];
        let i = 0;
        let ids = [];
        const chunkDelete = async () => {
            const deleteRes = await this.deleteRecords({
                schema: deleteObj.schema,
                table: deleteObj.table,
                ids,
            });
            deletedIds.push(...deleteRes.deleted_hashes);
            skippedIds.push(...deleteRes.skipped_hashes);
            await (0, common_utils_js_1.asyncSetTimeout)(DELETE_PAUSE_MS);
            ids = [];
            deleteCalled = true;
        };
        for await (const records of recordsToDelete) {
            ids.push(records[Table.primaryKey]);
            i++;
            if (i % DELETE_CHUNK === 0) {
                await chunkDelete();
            }
        }
        if (ids.length > 0)
            await chunkDelete();
        if (!deleteCalled) {
            return { message: 'No records found to delete' };
        }
        return createDeleteResponse(deletedIds, skippedIds, undefined);
    }
    /**
     * fetches records by their hash values and returns an Array of the results
     * @param {SearchByHashObject} searchObject
     */
    searchByHash(searchObject) {
        if (searchObject.select !== undefined)
            searchObject.get_attributes = searchObject.select;
        const validationError = (0, searchValidator_js_1.default)(searchObject, 'hashes');
        if (validationError) {
            throw validationError;
        }
        return getRecords(searchObject);
    }
    /**
     * Called by some SQL functions
     * @param searchObject
     */
    async getDataByHash(searchObject) {
        const map = new Map();
        searchObject._returnKeyValue = true;
        for await (const { key, value } of getRecords(searchObject, true)) {
            map.set(key, value);
        }
        return map;
    }
    searchByValue(searchObject, comparator) {
        if (comparator && hdbTerms_ts_1.VALUE_SEARCH_COMPARATORS_REVERSE_LOOKUP[comparator] === undefined) {
            throw new Error(`Value search comparator - ${comparator} - is not valid`);
        }
        if (searchObject.select !== undefined)
            searchObject.get_attributes = searchObject.select;
        if (searchObject.search_attribute !== undefined)
            searchObject.attribute = searchObject.search_attribute;
        if (searchObject.search_value !== undefined)
            searchObject.value = searchObject.search_value;
        const validationError = (0, searchValidator_js_1.default)(searchObject, 'value');
        if (validationError) {
            throw validationError;
        }
        const table = getTable(searchObject);
        if (!table) {
            throw new hdbError_js_1.ClientError(`Table ${searchObject.table} not found`);
        }
        let value = searchObject.value;
        if (value.includes?.('*')) {
            if (value.startsWith('*')) {
                if (value.endsWith('*')) {
                    if (value !== '*') {
                        comparator = 'contains';
                        value = value.slice(1, -1);
                    }
                }
                else {
                    comparator = 'ends_with';
                    value = value.slice(1);
                }
            }
            else if (value.endsWith('*')) {
                comparator = 'starts_with';
                value = value.slice(0, -1);
            }
        }
        if (comparator === hdbTerms_ts_1.VALUE_SEARCH_COMPARATORS.BETWEEN)
            value = [value, searchObject.end_value];
        const conditions = value === '*'
            ? []
            : [
                {
                    attribute: searchObject.attribute,
                    value,
                    comparator,
                },
            ];
        return table.search({
            conditions,
            allowFullScan: true,
            limit: searchObject.limit,
            offset: searchObject.offset,
            reverse: searchObject.reverse,
            sort: searchObject.sort,
            select: getSelect(searchObject, table),
        }, {
            onlyIfCached: searchObject.onlyIfCached,
            noCacheStore: searchObject.noCacheStore,
            noCache: searchObject.noCache,
            replicateFrom: searchObject.replicateFrom,
        });
    }
    async getDataByValue(searchObject, comparator) {
        const map = new Map();
        const table = getTable(searchObject);
        const attrs = searchObject.get_attributes;
        if (attrs && !attrs.includes(table.primaryKey) && attrs[0] !== '*')
            // ensure that we get the primary key so we can make a mapping
            attrs.push(table.primaryKey);
        for await (const record of this.searchByValue(searchObject, comparator)) {
            map.set(record[table.primaryKey], record);
        }
        return map;
    }
    resetReadTxn(schema, table) {
        getTable({ schema, table })?.primaryStore.resetReadTxn?.();
    }
    /**
     * Deletes transaction logs before a given timestamp.
     * @param deleteObj The request body
     * @returns
     */
    async deleteTransactionLogsBefore(deleteObj) {
        let totalResults = new DeleteTransactionLogsBeforeResults_ts_1.DeleteTransactionLogsBeforeResults();
        const before = deleteObj.timestamp instanceof Date
            ? deleteObj.timestamp.getTime()
            : typeof deleteObj.timestamp === 'string'
                ? Number.parseInt(deleteObj.timestamp)
                : deleteObj.timestamp;
        const table = getTable(deleteObj);
        if (!table) {
            // no table, check if any of the tables are RocksDB
            // since all tables share the same transaction log store, we break after the first
            // RocksDB table is found
            const tables = (0, databases_ts_1.getDatabases)()[deleteObj.database];
            if (tables) {
                for (const table of Object.values(tables)) {
                    if (table.primaryStore instanceof rocksdb_js_1.RocksDatabase) {
                        const deleted = table.primaryStore.purgeLogs({ before });
                        totalResults.log_files_deleted += deleted.length;
                        break;
                    }
                }
            }
        }
        else if (table.primaryStore instanceof rocksdb_js_1.RocksDatabase) {
            totalResults.log_files_deleted += table.primaryStore.purgeLogs({ before }).length;
        }
        else {
            await table.deleteHistory(before, deleteObj.cleanup_deleted_records);
        }
        return totalResults;
    }
    async readAuditLog(readAuditLogObj) {
        const table = getTable(readAuditLogObj);
        const histories = {};
        switch (readAuditLogObj.search_type) {
            case hdbTerms_ts_1.READ_AUDIT_LOG_SEARCH_TYPES_ENUM.HASH_VALUE:
                // get the history of each record
                for (const id of readAuditLogObj.search_values) {
                    histories[id] = (await table.getHistoryOfRecord(id)).map((auditRecord) => {
                        let operation = auditRecord.operation ?? auditRecord.type;
                        if (operation === 'put')
                            operation = 'upsert';
                        return {
                            operation,
                            timestamp: auditRecord.version,
                            user_name: auditRecord.user,
                            ids: [id],
                            records: [auditRecord.value],
                        };
                    });
                }
                return histories;
            case hdbTerms_ts_1.READ_AUDIT_LOG_SEARCH_TYPES_ENUM.USERNAME: {
                const users = readAuditLogObj.search_values;
                // do a full table scan of the history and find users
                for await (const entry of groupRecordsInHistory(table)) {
                    if (users.includes(entry.user_name)) {
                        const entriesForUser = histories[entry.user_name] || (histories[entry.user_name] = []);
                        entriesForUser.push(entry);
                    }
                }
                return histories;
            }
            default:
                return groupRecordsInHistory(table, readAuditLogObj.search_values?.[0], // start timestamp
                readAuditLogObj.search_values?.[1], // end timestamp
                readAuditLogObj.limit);
        }
    }
    async getBackup(getBackupObj) {
        return (0, lmdbGetBackup_js_1.default)(getBackupObj);
    }
}
exports.ResourceBridge = ResourceBridge;
function getSelect({ get_attributes }, table) {
    if (get_attributes) {
        if (get_attributes[0] === '*') {
            if (table.schemaDefined)
                return;
            else
                get_attributes = table.attributes.map((attribute) => attribute.name);
        }
        get_attributes.forceNulls = true;
        return get_attributes;
    }
}
/**
 * Iterator for asynchronous getting ids from an array
 */
function getRecords(searchObject, returnKeyValue) {
    const table = getTable(searchObject);
    const select = getSelect(searchObject, table);
    if (!table) {
        throw new hdbError_js_1.ClientError(`Table ${searchObject.table} not found`);
    }
    let lazy;
    if (select && table.attributes.length - select.length > 2 && select.length < 5)
        lazy = true;
    // we need to get the transaction and ensure that the transaction spans the entire duration
    // of the iteration
    const context = {
        user: searchObject.hdb_user,
        onlyIfCached: searchObject.onlyIfCached,
        noCacheStore: searchObject.noCacheStore,
        noCache: searchObject.noCache,
        replicateFrom: searchObject.replicateFrom,
    };
    let finishedIteration;
    (0, transaction_ts_1.transaction)(context, () => new Promise((resolve) => (finishedIteration = resolve)));
    const ids = searchObject.ids || searchObject.hash_values;
    let i = 0;
    return {
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    if (i < ids.length) {
                        const id = ids[i++];
                        let record;
                        try {
                            record = await table.get({ id, lazy, select }, context);
                            record = record && (0, tracked_ts_1.collapseData)(record);
                        }
                        catch (error) {
                            record = {
                                message: (0, harper_logger_js_1.errorToString)(error),
                            };
                        }
                        if (returnKeyValue)
                            return {
                                value: { key: id, value: record },
                            };
                        else
                            return { value: record };
                    }
                    else {
                        finishedIteration();
                        return { done: true };
                    }
                },
                return(value) {
                    finishedIteration();
                    return {
                        value,
                        done: true,
                    };
                },
                // eslint-disable-next-line no-unused-vars
                throw(error) {
                    finishedIteration();
                    return {
                        done: true,
                    };
                },
            };
        },
    };
}
/**
 * Gets the table object for the given database and table names.
 * @param operationObject The operation object containing the database and table names
 * @returns The table object or undefined if the table is not found
 */
function getTable(operationObject) {
    const databaseName = operationObject.database || operationObject.schema || DEFAULT_DATABASE;
    const tables = (0, databases_ts_1.getDatabases)()[databaseName];
    if (!tables)
        throw (0, hdbError_js_1.handleHDBError)(new Error(), HDB_ERROR_MSGS.SCHEMA_NOT_FOUND(databaseName), 404);
    return operationObject.table ? tables[operationObject.table] : undefined;
}
/**
 * creates the response object for deletes based on the deleted & skipped hashes
 * @param {[]} deleted - list of hash values successfully deleted
 * @param {[]} skipped - list  of hash values which did not get deleted
 * @param {number} txnTime - the transaction timestamp
 * @returns {{skipped_hashes: [], deleted_hashes: [], message: string}}
 */
function createDeleteResponse(deleted, skipped, txnTime) {
    const total = deleted.length + skipped.length;
    const plural = total === 1 ? 'record' : 'records';
    return {
        message: `${deleted.length} of ${total} ${plural} successfully deleted`,
        deleted_hashes: deleted,
        skipped_hashes: skipped,
        txn_time: txnTime,
    };
}
async function* groupRecordsInHistory(table, start, end, limit) {
    let enqueued;
    let count = 0;
    for await (const entry of table.getHistory(start, end)) {
        let operation = entry.operation ?? entry.type;
        if (operation === 'put')
            operation = 'upsert';
        const { id, version: timestamp, value } = entry;
        if (enqueued?.timestamp === timestamp) {
            enqueued.ids.push(id);
            enqueued.records.push(value);
        }
        else {
            if (enqueued) {
                yield enqueued;
                count++;
                if (limit && limit <= count) {
                    enqueued = undefined;
                    break;
                }
            }
            enqueued = {
                operation,
                user_name: entry.user,
                timestamp,
                ids: [id],
                records: [value],
            };
        }
    }
    if (enqueued)
        yield enqueued;
}
//# sourceMappingURL=ResourceBridge.js.map