"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.sync = exports.destroy = exports.revertClient = exports.updateClient = exports.update = exports.create = exports.fetchAppPackage = exports.fetchAppDefinition = exports.fetch = exports.getAppUrl = void 0;
const environment_1 = __importDefault(require("../../environment"));
const package_json_1 = __importDefault(require("../../../package.json"));
const staticViews_1 = require("../../db/views/staticViews");
const fileSystem_1 = require("../../utilities/fileSystem");
const utils_1 = require("../../db/utils");
const { BUILTIN_ROLE_IDS, AccessController, } = require("@devlego/backend-core/roles");
const { CacheKeys, bustCache } = require("@devlego/backend-core/cache");
const { getAllApps, isDevAppID, getProdAppID, Replication, } = require("@devlego/backend-core/db");
const constants_1 = require("../../constants");
const workerRequests_1 = require("../../utilities/workerRequests");
const utilities_1 = require("../../utilities");
const redis_1 = require("../../utilities/redis");
const clientLibrary_1 = require("../../utilities/fileSystem/clientLibrary");
const { getTenantId, isMultiTenant } = require("@devlego/backend-core/tenancy");
const user_1 = require("./user");
const { app: appCache } = require("@devlego/backend-core/cache");
const utils_2 = require("../../automations/utils");
const backend_core_1 = require("@devlego/backend-core");
const logging_1 = require("../../automations/logging");
const rows_1 = require("../../utilities/usageQuota/rows");
const pro_1 = require("@devlego/pro");
const backend_core_2 = require("@devlego/backend-core");
const types_1 = require("@devlego/types");
const URL_REGEX_SLASH = /\/|\\/g;
// utility function, need to do away with this
function getLayouts() {
    return __awaiter(this, void 0, void 0, function* () {
        const db = backend_core_1.context.getAppDB();
        return (yield db.allDocs((0, utils_1.getLayoutParams)(null, {
            include_docs: true,
        }))).rows.map((row) => row.doc);
    });
}
function getScreens() {
    return __awaiter(this, void 0, void 0, function* () {
        const db = backend_core_1.context.getAppDB();
        return (yield db.allDocs((0, utils_1.getScreenParams)(null, {
            include_docs: true,
        }))).rows.map((row) => row.doc);
    });
}
function getUserRoleId(ctx) {
    return !ctx.user.role || !ctx.user.role._id
        ? BUILTIN_ROLE_IDS.PUBLIC
        : ctx.user.role._id;
}
const getAppUrl = (ctx) => {
    // construct the url
    let url;
    if (ctx.request.body.url) {
        // if the url is provided, use that
        url = encodeURI(ctx.request.body.url);
    }
    else if (ctx.request.body.name) {
        // otherwise use the name
        url = encodeURI(`${ctx.request.body.name}`);
    }
    if (url) {
        url = `/${url.replace(URL_REGEX_SLASH, "")}`.toLowerCase();
    }
    return url;
};
exports.getAppUrl = getAppUrl;
const checkAppUrl = (ctx, apps, url, currentAppId) => {
    if (currentAppId) {
        apps = apps.filter((app) => app.appId !== currentAppId);
    }
    if (apps.some((app) => app.url === url)) {
        ctx.throw(400, "App URL is already in use.");
    }
};
const checkAppName = (ctx, apps, name, currentAppId) => {
    // TODO: Replace with Joi
    if (!name) {
        ctx.throw(400, "Name is required");
    }
    if (currentAppId) {
        apps = apps.filter((app) => app.appId !== currentAppId);
    }
    if (apps.some((app) => app.name === name)) {
        ctx.throw(400, "App name is already in use.");
    }
};
function createInstance(template) {
    return __awaiter(this, void 0, void 0, function* () {
        const tenantId = isMultiTenant() ? getTenantId() : null;
        const baseAppId = (0, utils_1.generateAppID)(tenantId);
        const appId = (0, utils_1.generateDevAppID)(baseAppId);
        yield backend_core_1.context.updateAppId(appId);
        const db = backend_core_1.context.getAppDB();
        yield db.put({
            _id: "_design/database",
            // view collation information, read before writing any complex views:
            // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
            views: {},
        });
        // NOTE: indexes need to be created before any tables/templates
        // add view for linked rows
        yield (0, staticViews_1.createLinkView)();
        yield (0, staticViews_1.createRoutingView)();
        yield (0, staticViews_1.createAllSearchIndex)();
        // replicate the template data to the instance DB
        // this is currently very hard to test, downloading and importing template files
        if (template && template.templateString) {
            const { ok } = yield db.load((0, utilities_1.stringToReadStream)(template.templateString));
            if (!ok) {
                throw "Error loading database dump from memory.";
            }
        }
        else if (template && template.useTemplate === "true") {
            /* istanbul ignore next */
            const { ok } = yield db.load(yield (0, fileSystem_1.getTemplateStream)(template));
            if (!ok) {
                throw "Error loading database dump from template.";
            }
        }
        else {
            // create the users table
            yield db.put(constants_1.USERS_TABLE_SCHEMA);
        }
        return { _id: appId };
    });
}
const fetch = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const dev = ctx.query && ctx.query.status === utils_1.AppStatus.DEV;
    const all = ctx.query && ctx.query.status === utils_1.AppStatus.ALL;
    const apps = yield getAllApps({ dev, all });
    // get the locks for all the dev apps
    if (dev || all) {
        const locks = yield (0, redis_1.getAllLocks)();
        for (let app of apps) {
            if (app.status !== "development") {
                continue;
            }
            const lock = locks.find((lock) => lock.appId === app.appId);
            if (lock) {
                app.lockedBy = lock.user;
            }
            else {
                // make sure its definitely not present
                delete app.lockedBy;
            }
        }
    }
    ctx.body = yield (0, logging_1.checkAppMetadata)(apps);
});
exports.fetch = fetch;
const fetchAppDefinition = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const layouts = yield getLayouts();
    const userRoleId = getUserRoleId(ctx);
    const accessController = new AccessController();
    const screens = yield accessController.checkScreensAccess(yield getScreens(), userRoleId);
    ctx.body = {
        layouts,
        screens,
        libraries: ["@budibase/standard-components"],
    };
});
exports.fetchAppDefinition = fetchAppDefinition;
const fetchAppPackage = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const db = backend_core_1.context.getAppDB();
    const application = yield db.get(utils_1.DocumentTypes.APP_METADATA);
    const layouts = yield getLayouts();
    let screens = yield getScreens();
    // Only filter screens if the user is not a builder
    if (!(ctx.user.builder && ctx.user.builder.global)) {
        const userRoleId = getUserRoleId(ctx);
        const accessController = new AccessController();
        screens = yield accessController.checkScreensAccess(screens, userRoleId);
    }
    ctx.body = {
        application,
        screens,
        layouts,
        clientLibPath: (0, utilities_1.clientLibraryPath)(ctx.params.appId, application.version),
    };
});
exports.fetchAppPackage = fetchAppPackage;
const performAppCreate = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const apps = yield getAllApps({ dev: true });
    const name = ctx.request.body.name;
    checkAppName(ctx, apps, name);
    const url = (0, exports.getAppUrl)(ctx);
    checkAppUrl(ctx, apps, url);
    const { useTemplate, templateKey, templateString } = ctx.request.body;
    const instanceConfig = {
        useTemplate,
        key: templateKey,
        templateString,
    };
    if (ctx.request.files && ctx.request.files.templateFile) {
        instanceConfig.file = ctx.request.files.templateFile;
    }
    const instance = yield createInstance(instanceConfig);
    const appId = instance._id;
    const db = backend_core_1.context.getAppDB();
    let _rev;
    try {
        // if template there will be an existing doc
        const existing = yield db.get(utils_1.DocumentTypes.APP_METADATA);
        _rev = existing._rev;
    }
    catch (err) {
        // nothing to do
    }
    const newApplication = {
        _id: utils_1.DocumentTypes.APP_METADATA,
        _rev,
        appId: instance._id,
        type: "app",
        version: package_json_1.default.version,
        componentLibraries: ["@budibase/standard-components"],
        name: name,
        url: url,
        template: ctx.request.body.template,
        instance: instance,
        tenantId: getTenantId(),
        updatedAt: new Date().toISOString(),
        createdAt: new Date().toISOString(),
        status: utils_1.AppStatus.DEV,
        navigation: {
            navigation: "Top",
            title: name,
            navWidth: "Large",
            navBackground: "var(--spectrum-global-color-gray-100)",
            links: [
                {
                    url: "/home",
                    text: "Home",
                },
            ],
        },
        theme: "spectrum--light",
        customTheme: {
            buttonBorderRadius: "16px",
        },
    };
    const response = yield db.put(newApplication, { force: true });
    newApplication._rev = response.rev;
    /* istanbul ignore next */
    if (!environment_1.default.isTest()) {
        yield (0, fileSystem_1.createApp)(appId);
    }
    yield appCache.invalidateAppMetadata(appId, newApplication);
    return newApplication;
});
const creationEvents = (request, app) => __awaiter(void 0, void 0, void 0, function* () {
    var _a;
    let creationFns = [];
    const body = request.body;
    if (body.useTemplate === "true") {
        // from template
        if (body.templateKey && body.templateKey !== "undefined") {
            creationFns.push(a => backend_core_2.events.app.templateImported(a, body.templateKey));
        }
        // from file
        else if ((_a = request.files) === null || _a === void 0 ? void 0 : _a.templateFile) {
            creationFns.push(a => backend_core_2.events.app.fileImported(a));
        }
        // unknown
        else {
            console.error("Could not determine template creation event");
        }
    }
    creationFns.push(a => backend_core_2.events.app.created(a));
    for (let fn of creationFns) {
        yield fn(app);
    }
});
const appPostCreate = (ctx, app) => __awaiter(void 0, void 0, void 0, function* () {
    const tenantId = getTenantId();
    yield backend_core_2.migrations.backPopulateMigrations({
        type: types_1.MigrationType.APP,
        tenantId,
        appId: app.appId,
    });
    yield creationEvents(ctx.request, app);
    // app import & template creation
    if (ctx.request.body.useTemplate === "true") {
        const rows = yield (0, rows_1.getUniqueRows)([app.appId]);
        const rowCount = rows ? rows.length : 0;
        if (rowCount) {
            try {
                yield pro_1.quotas.addRows(rowCount);
            }
            catch (err) {
                if (err.code && err.code === backend_core_2.errors.codes.USAGE_LIMIT_EXCEEDED) {
                    // this import resulted in row usage exceeding the quota
                    // delete the app
                    // skip pre and post steps as no rows have been added to quotas yet
                    ctx.params.appId = app.appId;
                    yield destroyApp(ctx);
                }
                throw err;
            }
        }
    }
});
const create = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const newApplication = yield pro_1.quotas.addApp(() => performAppCreate(ctx));
    yield appPostCreate(ctx, newApplication);
    yield bustCache(CacheKeys.CHECKLIST);
    ctx.body = newApplication;
    ctx.status = 200;
});
exports.create = create;
// This endpoint currently operates as a PATCH rather than a PUT
// Thus name and url fields are handled only if present
const update = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const apps = yield getAllApps({ dev: true });
    // validation
    const name = ctx.request.body.name;
    if (name) {
        checkAppName(ctx, apps, name, ctx.params.appId);
    }
    const url = (0, exports.getAppUrl)(ctx);
    if (url) {
        checkAppUrl(ctx, apps, url, ctx.params.appId);
        ctx.request.body.url = url;
    }
    const app = yield updateAppPackage(ctx.request.body, ctx.params.appId);
    yield backend_core_2.events.app.updated(app);
    ctx.status = 200;
    ctx.body = app;
});
exports.update = update;
const updateClient = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    // Get current app version
    const db = backend_core_1.context.getAppDB();
    const application = yield db.get(utils_1.DocumentTypes.APP_METADATA);
    const currentVersion = application.version;
    // Update client library and manifest
    if (!environment_1.default.isTest()) {
        yield (0, clientLibrary_1.backupClientLibrary)(ctx.params.appId);
        yield (0, clientLibrary_1.updateClientLibrary)(ctx.params.appId);
    }
    // Update versions in app package
    const updatedToVersion = package_json_1.default.version;
    const appPackageUpdates = {
        version: updatedToVersion,
        revertableVersion: currentVersion,
    };
    const app = yield updateAppPackage(appPackageUpdates, ctx.params.appId);
    yield backend_core_2.events.app.versionUpdated(app, currentVersion, updatedToVersion);
    ctx.status = 200;
    ctx.body = app;
});
exports.updateClient = updateClient;
const revertClient = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    // Check app can be reverted
    const db = backend_core_1.context.getAppDB();
    const application = yield db.get(utils_1.DocumentTypes.APP_METADATA);
    if (!application.revertableVersion) {
        ctx.throw(400, "There is no version to revert to");
    }
    // Update client library and manifest
    if (!environment_1.default.isTest()) {
        yield (0, clientLibrary_1.revertClientLibrary)(ctx.params.appId);
    }
    // Update versions in app package
    const currentVersion = application.version;
    const revertedToVersion = application.revertableVersion;
    const appPackageUpdates = {
        version: revertedToVersion,
        revertableVersion: null,
    };
    const app = yield updateAppPackage(appPackageUpdates, ctx.params.appId);
    yield backend_core_2.events.app.versionReverted(app, currentVersion, revertedToVersion);
    ctx.status = 200;
    ctx.body = app;
});
exports.revertClient = revertClient;
const destroyApp = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    let appId = ctx.params.appId;
    let isUnpublish = ctx.query && ctx.query.unpublish;
    if (isUnpublish) {
        appId = getProdAppID(appId);
    }
    const db = isUnpublish ? backend_core_1.context.getProdAppDB() : backend_core_1.context.getAppDB();
    const app = yield db.get(utils_1.DocumentTypes.APP_METADATA);
    const result = yield db.destroy();
    if (isUnpublish) {
        yield pro_1.quotas.removePublishedApp();
        yield backend_core_2.events.app.unpublished(app);
    }
    else {
        yield pro_1.quotas.removeApp();
        yield backend_core_2.events.app.deleted(app);
    }
    /* istanbul ignore next */
    if (!environment_1.default.isTest() && !isUnpublish) {
        yield (0, fileSystem_1.deleteApp)(appId);
    }
    // automations only in production
    if (isUnpublish) {
        yield (0, utils_2.cleanupAutomations)(appId);
    }
    // remove app role when the dev app is deleted (no trace of app anymore)
    else {
        yield (0, workerRequests_1.removeAppFromUserRoles)(ctx, appId);
    }
    yield appCache.invalidateAppMetadata(appId);
    return result;
});
const preDestroyApp = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const rows = yield (0, rows_1.getUniqueRows)([ctx.params.appId]);
    ctx.rowCount = rows.length;
});
const postDestroyApp = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    const rowCount = ctx.rowCount;
    if (rowCount) {
        yield pro_1.quotas.removeRows(rowCount);
    }
});
const destroy = (ctx) => __awaiter(void 0, void 0, void 0, function* () {
    yield preDestroyApp(ctx);
    const result = yield destroyApp(ctx);
    yield postDestroyApp(ctx);
    ctx.status = 200;
    ctx.body = result;
});
exports.destroy = destroy;
const sync = (ctx, next) => __awaiter(void 0, void 0, void 0, function* () {
    if (environment_1.default.DISABLE_AUTO_PROD_APP_SYNC) {
        ctx.status = 200;
        ctx.body = {
            message: "App sync disabled. You can reenable with the DISABLE_AUTO_PROD_APP_SYNC environment variable.",
        };
        return next();
    }
    const appId = ctx.params.appId;
    if (!isDevAppID(appId)) {
        ctx.throw(400, "This action cannot be performed for production apps");
    }
    // replicate prod to dev
    const prodAppId = getProdAppID(appId);
    try {
        // specific case, want to make sure setup is skipped
        const prodDb = backend_core_1.context.getProdAppDB({ skip_setup: true });
        const info = yield prodDb.info();
        if (info.error)
            throw info.error;
    }
    catch (err) {
        // the database doesn't exist. Don't replicate
        ctx.status = 200;
        ctx.body = {
            message: "App sync not required, app not deployed.",
        };
        return next();
    }
    const replication = new Replication({
        source: prodAppId,
        target: appId,
    });
    let error;
    try {
        yield replication.replicate({
            filter: function (doc) {
                return doc._id !== utils_1.DocumentTypes.APP_METADATA;
            },
        });
    }
    catch (err) {
        error = err;
    }
    finally {
        yield replication.close();
    }
    // sync the users
    yield (0, user_1.syncGlobalUsers)();
    if (error) {
        ctx.throw(400, error);
    }
    else {
        ctx.body = {
            message: "App sync completed successfully.",
        };
    }
});
exports.sync = sync;
const updateAppPackage = (appPackage, appId) => __awaiter(void 0, void 0, void 0, function* () {
    return backend_core_1.context.doInAppContext(appId, () => __awaiter(void 0, void 0, void 0, function* () {
        const db = backend_core_1.context.getAppDB();
        const application = yield db.get(utils_1.DocumentTypes.APP_METADATA);
        const newAppPackage = Object.assign(Object.assign({}, application), appPackage);
        if (appPackage._rev !== application._rev) {
            newAppPackage._rev = application._rev;
        }
        // the locked by property is attached by server but generated from
        // Redis, shouldn't ever store it
        delete newAppPackage.lockedBy;
        yield db.put(newAppPackage);
        // remove any cached metadata, so that it will be updated
        yield appCache.invalidateAppMetadata(appId);
        return newAppPackage;
    }));
});
