"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Workspace
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.workspaceManifestProperty = exports.workspaceDbFileExt = void 0;
exports.constructWorkspaceDb = constructWorkspaceDb;
exports.constructWorkspace = constructWorkspace;
exports.constructWorkspaceEditor = constructWorkspaceEditor;
exports.validateWorkspaceContainerId = validateWorkspaceContainerId;
exports.throwWorkspaceDbLoadErrors = throwWorkspaceDbLoadErrors;
const crypto_1 = require("crypto");
const fs = require("fs-extra");
const path_1 = require("path");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const CloudSqlite_1 = require("../../CloudSqlite");
const IModelHost_1 = require("../../IModelHost");
const IModelJsFs_1 = require("../../IModelJsFs");
const SQLiteDb_1 = require("../../SQLiteDb");
const Settings_1 = require("../../workspace/Settings");
const Workspace_1 = require("../../workspace/Workspace");
const WorkspaceEditor_1 = require("../../workspace/WorkspaceEditor");
const WorkspaceSqliteDb_1 = require("./WorkspaceSqliteDb");
const SettingsImpl_1 = require("./SettingsImpl");
const Symbols_1 = require("../Symbols");
const OnlineStatus_1 = require("../OnlineStatus");
function workspaceDbNameWithDefault(dbName) {
    return dbName ?? "workspace-db";
}
/** file extension for local WorkspaceDbs */
exports.workspaceDbFileExt = "itwin-workspace";
function makeWorkspaceCloudCache(arg) {
    const cache = CloudSqlite_1.CloudSqlite.CloudCaches.getCache(arg);
    if (undefined === cache.workspaceContainers) // if we just created this container, add the map.
        CloudSqlite_1.CloudSqlite.addHiddenProperty(cache, "workspaceContainers", new Map());
    return cache;
}
function getContainerFullId(props) {
    return `${props.baseUri}/${props.containerId}`;
}
function getWorkspaceCloudContainer(props, cache) {
    const id = getContainerFullId(props);
    let cloudContainer = cache.workspaceContainers.get(id);
    if (undefined !== cloudContainer)
        return cloudContainer;
    cloudContainer = CloudSqlite_1.CloudSqlite.createCloudContainer(props);
    cache.workspaceContainers.set(id, cloudContainer);
    cloudContainer.connectCount = 0;
    CloudSqlite_1.CloudSqlite.addHiddenProperty(cloudContainer, "sharedConnect", function () {
        if (this.connectCount++ === 0) {
            this.connect(cache);
            return true;
        }
        return false;
    });
    CloudSqlite_1.CloudSqlite.addHiddenProperty(cloudContainer, "sharedDisconnect", function () {
        if (--this.connectCount <= 0) {
            this.disconnect();
            cache.workspaceContainers.delete(id);
            this.connectCount = 0;
        }
    });
    return cloudContainer;
}
class WorkspaceContainerImpl {
    [Symbols_1._implementationProhibited] = undefined;
    workspace;
    filesDir;
    id;
    fromProps;
    _cloudContainer;
    get cloudContainer() {
        return this._cloudContainer;
    }
    _wsDbs = new Map();
    get dirName() { return (0, path_1.join)(this.workspace.containerDir, this.id); }
    constructor(workspace, props) {
        validateWorkspaceContainerId(props.containerId);
        this.workspace = workspace;
        this.id = props.containerId;
        this.fromProps = props;
        if (props.baseUri !== "")
            this._cloudContainer = getWorkspaceCloudContainer(props, this.workspace.getCloudCache());
        workspace.addContainer(this);
        this.filesDir = (0, path_1.join)(this.dirName, "Files");
        const cloudContainer = this.cloudContainer;
        if (undefined === cloudContainer)
            return;
        // sharedConnect returns true if we just connected (if the container is shared, it may have already been connected)
        if (cloudContainer.sharedConnect() && false !== props.syncOnConnect) {
            try {
                if (core_bentley_1.ProcessDetector.isMobileAppBackend || core_bentley_1.ProcessDetector.isElectronAppBackend) {
                    // Even though we've already confirmed that we are running in a native app backend,
                    // having code here that references NativeHost causes a runtime exception. So we use
                    // getOnlineStatus to determine whether we're online, which NativeHost keeps up to date.
                    if (!(0, OnlineStatus_1.getOnlineStatus)()) {
                        // If running in a native app and we're offline, don't check for changes.
                        // Doing so will fail and be caught below, but it has to wait for the network
                        // timeout.
                        return;
                    }
                }
                cloudContainer.checkForChanges();
            }
            catch {
                // must be offline
            }
        }
    }
    resolveDbFileName(props) {
        const container = this.cloudContainer;
        if (undefined === container)
            return (0, path_1.join)(this.dirName, `${props.dbName}.${exports.workspaceDbFileExt}`); // local file, versions not allowed
        return CloudSqlite_1.CloudSqlite.querySemverMatch({ ...props, container, dbName: workspaceDbNameWithDefault(props.dbName) });
    }
    addWorkspaceDb(toAdd) {
        if (undefined !== this._wsDbs.get(toAdd.dbName))
            core_common_1.WorkspaceError.throwError("already-exists", { message: `workspaceDb '${toAdd.dbName}' already exists in workspace` });
        this._wsDbs.set(toAdd.dbName, toAdd);
    }
    getWorkspaceDb(props) {
        return this._wsDbs.get(workspaceDbNameWithDefault(props?.dbName)) ?? new WorkspaceDbImpl(props ?? {}, this);
    }
    closeWorkspaceDb(toDrop) {
        const name = toDrop.dbName;
        const wsDb = this._wsDbs.get(name);
        if (wsDb === toDrop) {
            this._wsDbs.delete(name);
            wsDb.close();
        }
    }
    close() {
        for (const [_name, db] of this._wsDbs)
            db.close();
        this._wsDbs.clear();
        this.cloudContainer?.sharedDisconnect();
    }
}
/** Implementation of WorkspaceDb */
class WorkspaceDbImpl {
    [Symbols_1._implementationProhibited] = undefined;
    sqliteDb = new WorkspaceSqliteDb_1.WorkspaceSqliteDb();
    dbName;
    _container;
    onClose = new core_bentley_1.BeEvent();
    dbFileName;
    _manifest;
    /** true if this WorkspaceDb is currently open */
    get isOpen() { return this.sqliteDb.isOpen; }
    get container() { return this._container; }
    queryFileResource(rscName) {
        const info = this.sqliteDb[Symbols_1._nativeDb].queryEmbeddedFile(rscName);
        if (undefined === info)
            return undefined;
        // since resource names can contain illegal characters, path separators, etc., we make the local file name from its hash, in hex.
        let localFileName = (0, path_1.join)(this._container.filesDir, (0, crypto_1.createHash)("sha1").update(this.dbFileName).update(rscName).digest("hex"));
        if (info.fileExt !== "") // since some applications may expect to see the extension, append it here if it was supplied.
            localFileName = `${localFileName}.${info.fileExt}`;
        return { localFileName, info };
    }
    constructor(props, container) {
        this.dbName = workspaceDbNameWithDefault(props.dbName);
        CloudSqlite_1.CloudSqlite.validateDbName(this.dbName);
        this._container = container;
        this.dbFileName = container.resolveDbFileName(props);
        container.addWorkspaceDb(this);
        if (true === props.prefetch)
            this.prefetch();
    }
    open() {
        this.sqliteDb.openDb(this.dbFileName, core_bentley_1.OpenMode.Readonly, this._container.cloudContainer);
    }
    close() {
        if (this.isOpen) {
            this.onClose.raiseEvent();
            this.sqliteDb.closeDb();
            this._container.closeWorkspaceDb(this);
        }
    }
    get version() {
        const cloudContainer = this.container.cloudContainer;
        if (undefined === cloudContainer)
            return "1.0.0"; // local file, no versioning. return default
        return CloudSqlite_1.CloudSqlite.parseDbFileName(this.dbFileName).version;
    }
    get manifest() {
        return this._manifest ??= this.withOpenDb((db) => {
            const manifestJson = db[Symbols_1._nativeDb].queryFileProperty(exports.workspaceManifestProperty, true);
            return manifestJson ? JSON.parse(manifestJson) : { workspaceName: this.dbName };
        });
    }
    withOpenDb(operation) {
        const done = this.isOpen ? () => { } : (this.open(), () => this.close());
        try {
            return operation(this.sqliteDb);
        }
        finally {
            done();
        }
    }
    getString(rscName) {
        return this.withOpenDb((db) => {
            return db.withSqliteStatement("SELECT value from strings WHERE id=?", (stmt) => {
                stmt.bindString(1, rscName);
                return core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValueString(0) : undefined;
            });
        });
    }
    getBlobReader(rscName) {
        return this.sqliteDb.withSqliteStatement("SELECT rowid from blobs WHERE id=?", (stmt) => {
            stmt.bindString(1, rscName);
            const blobReader = SQLiteDb_1.SQLiteDb.createBlobIO();
            blobReader.open(this.sqliteDb[Symbols_1._nativeDb], { tableName: "blobs", columnName: "value", row: stmt.getValueInteger(0) });
            return blobReader;
        });
    }
    getBlob(rscName) {
        return this.withOpenDb((db) => {
            return db.withSqliteStatement("SELECT value from blobs WHERE id=?", (stmt) => {
                stmt.bindString(1, rscName);
                return core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step() ? stmt.getValueBlob(0) : undefined;
            });
        });
    }
    getFile(rscName, targetFileName) {
        return this.withOpenDb((db) => {
            const file = this.queryFileResource(rscName);
            if (!file)
                return undefined;
            const info = file.info;
            const localFileName = targetFileName ?? file.localFileName;
            // check whether the file is already up to date.
            const stat = fs.existsSync(localFileName) && fs.statSync(localFileName);
            if (stat && Math.round(stat.mtimeMs) === info.date && stat.size === info.size)
                return localFileName; // yes, we're done
            // extractEmbeddedFile fails if the file exists or if the directory does not exist
            if (stat)
                fs.removeSync(localFileName);
            else
                IModelJsFs_1.IModelJsFs.recursiveMkDirSync((0, path_1.dirname)(localFileName));
            db[Symbols_1._nativeDb].extractEmbeddedFile({ name: rscName, localFileName });
            const date = new Date(info.date);
            fs.utimesSync(localFileName, date, date); // set the last-modified date of the file to match date in container
            fs.chmodSync(localFileName, "0444"); // set file readonly
            return localFileName;
        });
    }
    prefetch(opts) {
        const cloudContainer = this._container.cloudContainer;
        if (cloudContainer === undefined)
            core_common_1.WorkspaceError.throwError("no-cloud-container", { message: "no cloud container to prefetch" });
        return CloudSqlite_1.CloudSqlite.startCloudPrefetch(cloudContainer, this.dbFileName, opts);
    }
    queryResources(args) {
        const table = "blob" !== args.type ? "strings" : "blobs";
        this.withOpenDb((db) => {
            const where = undefined !== args.namePattern ? ` WHERE id ${args.nameCompare ?? "="} ?` : "";
            db.withSqliteStatement(`SELECT id from ${table}${where}`, (stmt) => {
                function* makeIterable() {
                    while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
                        yield stmt.getValueString(0);
                    }
                }
                if (undefined !== args.namePattern) {
                    stmt.bindString(1, args.namePattern);
                }
                args.callback(makeIterable());
            });
        });
    }
}
/** Implementation of Workspace */
class WorkspaceImpl {
    [Symbols_1._implementationProhibited] = undefined;
    _containers = new Map();
    containerDir;
    settings;
    _cloudCache;
    getCloudCache() {
        return this._cloudCache ??= makeWorkspaceCloudCache({ cacheName: "Workspace", cacheSize: "20G" });
    }
    constructor(settings, opts) {
        this.settings = settings;
        this.containerDir = opts?.containerDir ?? (0, path_1.join)(IModelHost_1.IModelHost.cacheDir, "Workspace");
        let settingsFiles = opts?.settingsFiles;
        if (settingsFiles) {
            if (typeof settingsFiles === "string")
                settingsFiles = [settingsFiles];
            settingsFiles.forEach((file) => settings.addFile(file, Settings_1.SettingsPriority.application));
        }
    }
    addContainer(toAdd) {
        if (undefined !== this._containers.get(toAdd.id))
            core_common_1.WorkspaceError.throwError("container-exists", { message: `container ${toAdd.id} already exists in workspace` });
        this._containers.set(toAdd.id, toAdd);
    }
    findContainer(containerId) {
        return this._containers.get(containerId);
    }
    getContainer(props) {
        return this.findContainer(props.containerId) ?? new WorkspaceContainerImpl(this, props);
    }
    async getContainerAsync(props) {
        const accessToken = props.accessToken ?? ((props.baseUri === "") || props.isPublic) ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ ...props, accessLevel: "read" });
        return this.getContainer({ ...props, accessToken });
    }
    async getWorkspaceDb(props) {
        let container = this.findContainer(props.containerId);
        if (undefined === container) {
            const accessToken = props.isPublic ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ accessLevel: "read", ...props });
            container = new WorkspaceContainerImpl(this, { ...props, accessToken });
        }
        return container.getWorkspaceDb(props);
    }
    async loadSettingsDictionary(props, problems) {
        if (!Array.isArray(props))
            props = [props];
        for (const prop of props) {
            const db = await this.getWorkspaceDb(prop);
            db.open();
            try {
                const manifest = db.manifest;
                const dictProps = { name: prop.resourceName, workspaceDb: db, priority: prop.priority };
                // don't load if we already have this dictionary. Happens if the same WorkspaceDb is in more than one list
                if (undefined === this.settings.getDictionary(dictProps)) {
                    const settingsJson = db.getString(prop.resourceName);
                    if (undefined === settingsJson)
                        throwWorkspaceDbLoadError(`could not load setting dictionary resource '${prop.resourceName}' from: '${manifest.workspaceName}'`, prop, db);
                    db.close(); // don't leave this db open in case we're going to find another dictionary in it recursively.
                    this.settings.addJson(dictProps, settingsJson);
                    const dict = this.settings.getDictionary(dictProps);
                    if (dict) {
                        Workspace_1.Workspace.onSettingsDictionaryLoadedFn({ dict, from: db });
                        // if the dictionary we just loaded has a "settingsWorkspaces" entry, load them too, recursively
                        const nested = dict.getSetting(Workspace_1.WorkspaceSettingNames.settingsWorkspaces);
                        if (nested !== undefined) {
                            IModelHost_1.IModelHost.settingsSchemas.validateSetting(nested, Workspace_1.WorkspaceSettingNames.settingsWorkspaces);
                            await this.loadSettingsDictionary(nested, problems);
                        }
                    }
                }
            }
            catch (e) {
                db.close();
                problems?.push(e);
            }
        }
    }
    close() {
        this.settings.close();
        for (const [_id, container] of this._containers)
            container.close();
        this._containers.clear();
    }
    resolveWorkspaceDbSetting(settingName, filter) {
        const settingDef = IModelHost_1.IModelHost.settingsSchemas.settingDefs.get(settingName);
        const combine = settingDef?.combineArray === true;
        filter = filter ?? (() => true);
        const result = [];
        for (const entry of this.settings.getSettingEntries(settingName)) {
            for (const dbProp of entry.value) {
                if (filter(dbProp, entry.dictionary)) {
                    result.push(dbProp);
                }
            }
            if (!combine) {
                break;
            }
        }
        return result;
    }
    async getWorkspaceDbs(args) {
        const dbList = (args.settingName !== undefined) ? this.resolveWorkspaceDbSetting(args.settingName, args.filter) : args.dbs;
        const result = [];
        const pushUnique = (wsDb) => {
            for (const db of result) {
                // if we already have this db, skip it. The test below also has to consider that we create a separate WorkspaceDb object for the same
                // database from more than one Workspace (though then they must use a "shared" CloudContainer).
                if (db === wsDb || ((db.container.cloudContainer === wsDb.container.cloudContainer) && (db.dbFileName === wsDb.dbFileName)))
                    return; // this db is redundant
            }
            result.push(wsDb);
        };
        for (const dbProps of dbList) {
            try {
                pushUnique(await this.getWorkspaceDb(dbProps));
            }
            catch (e) {
                const loadErr = e;
                loadErr.wsDbProps = dbProps;
                args.problems?.push(loadErr);
            }
        }
        return result;
    }
}
const workspaceEditorName = "WorkspaceEditor"; // name of the cache for the editor workspace
class EditorWorkspaceImpl extends WorkspaceImpl {
    getCloudCache() {
        return this._cloudCache ??= makeWorkspaceCloudCache({ cacheName: workspaceEditorName, cacheSize: "20G" });
    }
}
class EditorImpl {
    [Symbols_1._implementationProhibited] = undefined;
    workspace = new EditorWorkspaceImpl(new SettingsImpl_1.SettingsImpl(), { containerDir: (0, path_1.join)(IModelHost_1.IModelHost.cacheDir, workspaceEditorName) });
    async initializeContainer(args) {
        class CloudAccess extends CloudSqlite_1.CloudSqlite.DbAccess {
            static _cacheName = workspaceEditorName;
            static async initializeWorkspace(args) {
                const props = await this.createBlobContainer({ scope: args.scope, metadata: { ...args.metadata, containerType: "workspace" } });
                const dbFullName = CloudSqlite_1.CloudSqlite.makeSemverName(workspaceDbNameWithDefault(args.dbName), "0.0.0");
                await super._initializeDb({ ...args, props, dbName: dbFullName, dbType: WorkspaceSqliteDb_1.WorkspaceSqliteDb, blockSize: "4M" });
                return props;
            }
        }
        return CloudAccess.initializeWorkspace(args);
    }
    async createNewCloudContainer(args) {
        const cloudContainer = await this.initializeContainer(args);
        const userToken = await IModelHost_1.IModelHost.authorizationClient?.getAccessToken();
        const accessToken = await CloudSqlite_1.CloudSqlite.requestToken({ ...cloudContainer, accessLevel: "write", userToken });
        return this.getContainer({ accessToken, ...cloudContainer, writeable: true, description: args.metadata.description });
    }
    getContainer(props) {
        return this.workspace.findContainer(props.containerId) ?? new EditorContainerImpl(this.workspace, props);
    }
    async getContainerAsync(props) {
        const accessToken = props.accessToken ?? (props.baseUri === "") ? "" : await CloudSqlite_1.CloudSqlite.requestToken({ ...props, accessLevel: "write" });
        return this.getContainer({ ...props, accessToken });
    }
    close() {
        this.workspace.close();
    }
}
class EditorContainerImpl extends WorkspaceContainerImpl {
    get cloudContainer() {
        return super.cloudContainer;
    }
    get cloudProps() {
        const cloudContainer = this.cloudContainer;
        if (undefined === cloudContainer)
            return undefined;
        return {
            baseUri: cloudContainer.baseUri,
            containerId: cloudContainer.containerId,
            storageType: cloudContainer.storageType,
            isPublic: cloudContainer.isPublic,
        };
    }
    async createNewWorkspaceDbVersion(args) {
        const container = this.cloudContainer;
        if (undefined === container)
            core_common_1.WorkspaceError.throwError("no-cloud-container", { message: "versions require cloud containers" });
        const fromDb = { ...args.fromProps, dbName: workspaceDbNameWithDefault(args.fromProps?.dbName) };
        return CloudSqlite_1.CloudSqlite.createNewDbVersion(container, { ...args, fromDb });
    }
    getWorkspaceDb(props) {
        return this.getEditableDb(props);
    }
    getEditableDb(props) {
        const db = this._wsDbs.get(workspaceDbNameWithDefault(props.dbName)) ?? new EditableDbImpl(props, this);
        if (this.cloudContainer && !CloudSqlite_1.CloudSqlite.isSemverEditable(db.dbFileName, this.cloudContainer)) {
            this._wsDbs.delete(workspaceDbNameWithDefault(props.dbName));
            core_common_1.CloudSqliteError.throwError("already-published", { message: `${db.dbFileName} has been published and is not editable. Make a new version first.` });
        }
        return db;
    }
    acquireWriteLock(user) {
        if (this.cloudContainer) {
            this.cloudContainer.acquireWriteLock(user);
            this.cloudContainer.writeLockHeldBy = user;
        }
    }
    releaseWriteLock() {
        if (this.cloudContainer) {
            this.cloudContainer.releaseWriteLock();
            this.cloudContainer.writeLockHeldBy = undefined;
        }
    }
    abandonChanges() {
        if (this.cloudContainer) {
            this.cloudContainer.abandonChanges();
            this.cloudContainer.writeLockHeldBy = undefined;
        }
    }
    async createDb(args) {
        if (!this.cloudContainer) {
            WorkspaceEditor_1.WorkspaceEditor.createEmptyDb({ localFileName: this.resolveDbFileName(args), manifest: args.manifest });
        }
        else {
            // currently the only way to create a workspaceDb in a cloud container is to create a temporary workspaceDb and upload it.
            const tempDbFile = (0, path_1.join)(IModelHost_1.KnownLocations.tmpdir, `empty.${exports.workspaceDbFileExt}`);
            if (fs.existsSync(tempDbFile))
                IModelJsFs_1.IModelJsFs.removeSync(tempDbFile);
            WorkspaceEditor_1.WorkspaceEditor.createEmptyDb({ localFileName: tempDbFile, manifest: args.manifest });
            await CloudSqlite_1.CloudSqlite.uploadDb(this.cloudContainer, { localFileName: tempDbFile, dbName: CloudSqlite_1.CloudSqlite.makeSemverName(workspaceDbNameWithDefault(args.dbName)) });
            IModelJsFs_1.IModelJsFs.removeSync(tempDbFile);
        }
        return this.getWorkspaceDb(args);
    }
}
class EditableDbImpl extends WorkspaceDbImpl {
    get container() {
        (0, core_bentley_1.assert)(this._container instanceof EditorContainerImpl);
        return this._container;
    }
    static validateResourceName(name) {
        if (name.trim() !== name)
            core_common_1.WorkspaceError.throwError("invalid-name", { message: "resource name may not have leading or trailing spaces" });
        if (name.length > 1024) {
            core_common_1.WorkspaceError.throwError("invalid-name", { message: "resource name too long" });
        }
    }
    validateResourceSize(val) {
        const len = typeof val === "string" ? val.length : val.byteLength;
        if (len > (1024 * 1024 * 1024)) // one gigabyte
            core_common_1.WorkspaceError.throwError("too-large", { message: "value is too large" });
    }
    get cloudProps() {
        const props = this._container.cloudProps;
        if (props === undefined)
            return undefined;
        const parsed = CloudSqlite_1.CloudSqlite.parseDbFileName(this.dbFileName);
        return { ...props, dbName: parsed.dbName, version: parsed.version };
    }
    open() {
        this.sqliteDb.openDb(this.dbFileName, core_bentley_1.OpenMode.ReadWrite, this._container.cloudContainer);
    }
    close() {
        if (this.isOpen) {
            // whenever we close an EditableDb, update the name of the last editor in the manifest
            const lastEditedBy = this._container.cloudContainer?.writeLockHeldBy;
            if (lastEditedBy !== undefined)
                this.updateManifest({ ...this.manifest, lastEditedBy });
            // make sure all changes were saved before we close
            this.sqliteDb.saveChanges();
        }
        super.close();
    }
    getFileModifiedTime(localFileName) {
        return Math.round(fs.statSync(localFileName).mtimeMs);
    }
    performWriteSql(rscName, sql, bind) {
        this.sqliteDb.withSqliteStatement(sql, (stmt) => {
            stmt.bindString(1, rscName);
            bind?.(stmt);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc) {
                if (core_bentley_1.DbResult.BE_SQLITE_CONSTRAINT_PRIMARYKEY === rc)
                    core_common_1.WorkspaceError.throwError("resource-exists", { message: `resource "${rscName}" already exists` });
                core_common_1.WorkspaceError.throwError("write-error", { message: `workspace [${sql}], rc=${rc}` });
            }
        });
        this.sqliteDb.saveChanges();
    }
    updateManifest(manifest) {
        this.sqliteDb[Symbols_1._nativeDb].saveFileProperty(exports.workspaceManifestProperty, JSON.stringify(manifest));
        this._manifest = undefined;
    }
    updateSettingsResource(settings, rscName) {
        this.updateString(rscName ?? "settingsDictionary", JSON.stringify(settings));
    }
    addString(rscName, val) {
        EditableDbImpl.validateResourceName(rscName);
        this.validateResourceSize(val);
        this.performWriteSql(rscName, "INSERT INTO strings(id,value) VALUES(?,?)", (stmt) => stmt.bindString(2, val));
    }
    updateString(rscName, val) {
        this.validateResourceSize(val);
        this.performWriteSql(rscName, "INSERT INTO strings(id,value) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value WHERE value!=excluded.value", (stmt) => stmt.bindString(2, val));
    }
    removeString(rscName) {
        this.performWriteSql(rscName, "DELETE FROM strings WHERE id=?");
    }
    addBlob(rscName, val) {
        EditableDbImpl.validateResourceName(rscName);
        this.validateResourceSize(val);
        this.performWriteSql(rscName, "INSERT INTO blobs(id,value) VALUES(?,?)", (stmt) => stmt.bindBlob(2, val));
    }
    updateBlob(rscName, val) {
        this.validateResourceSize(val);
        this.performWriteSql(rscName, "INSERT INTO blobs(id,value) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET value=excluded.value WHERE value!=excluded.value", (stmt) => stmt.bindBlob(2, val));
    }
    getBlobWriter(rscName) {
        return this.sqliteDb.withSqliteStatement("SELECT rowid from blobs WHERE id=?", (stmt) => {
            stmt.bindString(1, rscName);
            const blobWriter = SQLiteDb_1.SQLiteDb.createBlobIO();
            blobWriter.open(this.sqliteDb[Symbols_1._nativeDb], { tableName: "blobs", columnName: "value", row: stmt.getValueInteger(0), writeable: true });
            return blobWriter;
        });
    }
    removeBlob(rscName) {
        this.performWriteSql(rscName, "DELETE FROM blobs WHERE id=?");
    }
    addFile(rscName, localFileName, fileExt) {
        EditableDbImpl.validateResourceName(rscName);
        fileExt = fileExt ?? (0, path_1.extname)(localFileName);
        if (fileExt?.[0] === ".")
            fileExt = fileExt.slice(1);
        this.sqliteDb[Symbols_1._nativeDb].embedFile({ name: rscName, localFileName, date: this.getFileModifiedTime(localFileName), fileExt });
    }
    updateFile(rscName, localFileName) {
        this.queryFileResource(rscName); // throws if not present
        this.sqliteDb[Symbols_1._nativeDb].replaceEmbeddedFile({ name: rscName, localFileName, date: this.getFileModifiedTime(localFileName) });
    }
    removeFile(rscName) {
        const file = this.queryFileResource(rscName);
        if (undefined === file)
            core_common_1.WorkspaceError.throwError("does-not-exist", { message: `file resource "${rscName}" does not exist` });
        if (file && fs.existsSync(file.localFileName))
            fs.unlinkSync(file.localFileName);
        this.sqliteDb[Symbols_1._nativeDb].removeEmbeddedFile(rscName);
    }
}
function constructWorkspaceDb(props, container) {
    return new WorkspaceDbImpl(props, container);
}
function constructWorkspace(settings, opts) {
    return new WorkspaceImpl(settings, opts);
}
function constructWorkspaceEditor() {
    return new EditorImpl();
}
/**
 * Validate that a WorkspaceContainer.Id is valid.
 * The rules for ContainerIds (from Azure, see https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata):
 *  - may only contain lower case letters, numbers or dashes
 *  - may not start or end with with a dash nor have more than one dash in a row
 *  - may not be shorter than 3 or longer than 63 characters
 */
function validateWorkspaceContainerId(id) {
    if (!/^(?=.{3,63}$)[a-z0-9]+(-[a-z0-9]+)*$/g.test(id))
        core_common_1.WorkspaceError.throwError("invalid-name", { message: `invalid containerId: [${id}]` });
}
exports.workspaceManifestProperty = { namespace: "workspace", name: "manifest" };
function throwWorkspaceDbLoadError(message, wsDbProps, wsDb) {
    core_common_1.WorkspaceError.throwError("load-error", { message, wsDb, wsDbProps });
}
function throwWorkspaceDbLoadErrors(message, wsLoadErrors) {
    core_common_1.WorkspaceError.throwError("load-errors", { message, wsLoadErrors });
}
//# sourceMappingURL=WorkspaceImpl.js.map