UNPKG

@itwin/core-backend

Version:
1,012 lines (1,011 loc) • 63.6 kB
"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 ViewDefinitions */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ViewStore = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const CloudSqlite_1 = require("./CloudSqlite"); const SQLiteDb_1 = require("./SQLiteDb"); const Category_1 = require("./Category"); const Model_1 = require("./Model"); const Symbols_1 = require("./internal/Symbols"); /* eslint-disable @typescript-eslint/no-non-null-assertion */ // cspell:ignore nocase rowid /** * A ViewStore is a database that stores Views and related data. It is used to store and retrieve views for iTwin.js. * It can either be a local SQLite file, or a CloudSqlite database stored in a cloud container. To use a CloudSqlite * database, you must first create a container in Blob Storage and then call [[ViewStore.CloudAccess.initializeDb]]. * * A ViewStore can hold: * - Views * - DisplayStyles * - CategorySelectors * - ModelSelectors * - RenderTimelines * - Searches * - Tags * - Thumbnails * - ViewGroups * * Views are added to a ViewStore via ViewDefinitionProps that may hold references to a DisplayStyle, CategorySelector, ModelSelector, or RenderTimeline. * Before storing a View, you must first add any referenced DisplayStyles, CategorySelectors, ModelSelectors, and RenderTimelines to the * ViewStore. The "add" methods return a string that uniquely identifies the object in the ViewStore. * You should set the ViewDefinitionProps's displayStyle, categorySelector, modelSelector, or renderTimeline member to the returned string. * When you load a ViewDefinition from the ViewStore, the member may be used to load the DisplayStyle, CategorySelector, * ModelSelector, RenderTimeline, etc. * * A IdString is a string that uniquely identifies a row in one of the ViewStore's internal tables. The string holds a base-36 integer * that starts with "@" (vs. "0x" for ElementIds). For example, if you store a DisplayStyle and it is assigned the ViewStore Id "@y1", then you * should set the ViewDefinitionProps's displayStyle member to "@y1". When you load the ViewDefinition from the ViewStore, the "@Y1" may be used to * alo load the DisplayStyle from the ViewStore. * * Views are organized into hierarchical ViewGroups (like file and folder hierarchies on a file system). A View is always stored "in" a ViewGroup, and * views must have a name that is unique within the ViewGroup. ViewGroups may each have a default ViewId. * The root ViewGroup is named "Root" and has a RowId of 1. The root ViewGroup can not be deleted. * View names and ViewGroup names may not contain either "/" or "@". ViewGroups are stored in the "viewGroups" table. * * Views may be "tagged" with one or more Tags. Tags are named with an arbitrary string that can be used to group Views. A Tag may * be associated with multiple Views, and a View may have multiple Tags. Tags are stored in the "tags" table. * * Views may optionally have a thumbnail, paired via the View's Id. Thumbnails are stored in the "thumbnails" table. * * Note: All ElementIds and ModelIds in ModelSelectors, CategorySelectors, DisplayStyles, Timelines, etc. are converted to guid-based identifiers when stored in the ViewStore. * They are then remapped back to their Ids when loaded from the ViewStore. This allows the ViewStore to be used with more than one iModel, * provided that the same Guids are used in each iModel. This is done by storing the set of unique Guids in the "guids" table, and then * creating a reference to the row in the "guids" table via the special Id prefix "^". For example, if a category selector contains the * Id "0x123", then the guid from element 0x123 is stored in the "guids" table, and the category selector is stored with the rowId of the entry * in the guid table (e.g. "^1w"). When the category selector is loaded from the ViewStore, the guid is looked up in the "guids" table and * the iModel is queried for the element with that guid. That element's Id (which may or may not be 0x123) is then returned in the category selector. * * @beta */ var ViewStore; (function (ViewStore) { ViewStore.tableName = { categorySelectors: "categorySelectors", displayStyles: "displayStyles", viewGroups: "viewGroups", guids: "guids", modelSelectors: "modelSelectors", taggedViews: "taggedViews", tags: "tags", thumbnails: "thumbnails", timelines: "timelines", searches: "searches", views: "views", }; /** convert a RowId to a RowString (base-36 integer with a leading "@") */ ViewStore.fromRowId = (rowId) => { return `@${rowId.toString(36)}`; }; /** convert a guid RowId to a GuidRowString (base-36 integer with a leading "^") */ const guidRowToString = (rowId) => { return `^${rowId.toString(36)}`; }; /** determine if a string is a guid row string (base-36 integer with a leading "^") */ const isGuidRowString = (id) => true === id?.startsWith("^"); /** @internal */ ViewStore.toRowId = (id) => { if (typeof id === "number") return id; if (!core_common_1.ViewStoreRpc.isViewStoreId(id) && !isGuidRowString(id)) core_common_1.ViewStoreError.throwError("invalid-value", { message: `invalid value: ${id}` }); return parseInt(id.slice(1), 36); }; const maybeToRowId = (id) => undefined === id ? undefined : ViewStore.toRowId(id); const cloneProps = (from) => JSON.parse(JSON.stringify(from)); const blankElementProps = (from, classFullName, idString, name) => { from.id = ViewStore.fromRowId(ViewStore.toRowId(idString)); from.classFullName = classFullName; from.model = core_common_1.IModel.dictionaryId; from.code = { spec: "0x1", scope: "0x1", value: name }; return from; }; const validateName = (name, msg) => { if (name.trim().length === 0 || (/[@^#<>:"/\\"`'|?*\u0000-\u001F]/g.test(name))) core_common_1.ViewStoreError.throwError("invalid-value", { message: `illegal ${msg} name "${name}"` }); }; ViewStore.defaultViewGroupId = 1; class ViewDb extends SQLiteDb_1.VersionedSqliteDb { myVersion = "4.0.0"; _iModel; _guidMap; get guidMap() { return this._guidMap; } set guidMap(guidMap) { this._guidMap = guidMap; } get iModel() { return this._iModel; } set iModel(iModel) { this._iModel = iModel; } constructor(arg) { super(); this._iModel = arg?.iModel; this._guidMap = arg?.guidMap ?? this._iModel?.elements; // this is only so tests can mock guids } /** create all the tables for a new ViewDb */ createDDL() { const baseCols = "Id INTEGER PRIMARY KEY AUTOINCREMENT,json TEXT,owner TEXT"; this.createTable({ tableName: ViewStore.tableName.views, columns: `${baseCols},name TEXT NOT NULL COLLATE NOCASE,className TEXT NOT NULL,private BOOLEAN NOT NULL,` + `groupId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.viewGroups}(Id) ON DELETE CASCADE, ` + `modelSel INTEGER REFERENCES ${ViewStore.tableName.modelSelectors}(Id), ` + `categorySel INTEGER NOT NULL REFERENCES ${ViewStore.tableName.categorySelectors}(Id), ` + `displayStyle INTEGER NOT NULL REFERENCES ${ViewStore.tableName.displayStyles}(Id)`, constraints: "UNIQUE(groupId,name)", addTimestamp: true, }); this.createTable({ tableName: ViewStore.tableName.viewGroups, columns: `${baseCols},name TEXT NOT NULL COLLATE NOCASE,parent INTEGER NOT NULL REFERENCES ${ViewStore.tableName.viewGroups}(Id) ON DELETE CASCADE` + `,defaultViewId INTEGER REFERENCES ${ViewStore.tableName.views}(Id)`, constraints: "UNIQUE(parent,name)", addTimestamp: true, }); // for tables that have a "name" column, we want to enforce case-insensitive uniqueness. Names may be null. const makeTable = (table, extra) => { this.createTable({ tableName: table, columns: `${baseCols},name TEXT UNIQUE COLLATE NOCASE${extra ?? ""}`, addTimestamp: true }); }; makeTable(ViewStore.tableName.modelSelectors); makeTable(ViewStore.tableName.categorySelectors); makeTable(ViewStore.tableName.displayStyles); makeTable(ViewStore.tableName.timelines); makeTable(ViewStore.tableName.tags); makeTable(ViewStore.tableName.searches); this.createTable({ tableName: ViewStore.tableName.thumbnails, columns: `Id INTEGER PRIMARY KEY REFERENCES ${ViewStore.tableName.views} (Id) ON DELETE CASCADE,json,owner,data BLOB NOT NULL` }); this.createTable({ tableName: ViewStore.tableName.taggedViews, columns: `viewId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.views} (Id) ON DELETE CASCADE,` + `tagId INTEGER NOT NULL REFERENCES ${ViewStore.tableName.tags} (Id) ON DELETE CASCADE`, constraints: `UNIQUE(tagId,viewId)`, }); this.createTable({ tableName: ViewStore.tableName.guids, columns: `guid BLOB NOT NULL UNIQUE` }); this.addViewGroupRow({ name: "Root", json: JSON.stringify({}) }); } /** get the row in the "guids" table for a given guid. If the guid is not present, return 0 */ getGuidRow(guid) { return this.withPreparedSqliteStatement(`SELECT rowId FROM ${ViewStore.tableName.guids} WHERE guid=?`, (stmt) => { stmt.bindGuid(1, guid); return !stmt.nextRow() ? 0 : stmt.getValueInteger(0); }); } /** @internal */ getGuid(rowid) { return this.withSqliteStatement(`SELECT guid FROM ${ViewStore.tableName.guids} WHERE rowId=?`, (stmt) => { stmt.bindInteger(1, rowid); return !stmt.nextRow() ? undefined : stmt.getValueGuid(0); }); } /** @internal */ iterateGuids(rowIds, fn) { this.withSqliteStatement(`SELECT guid FROM ${ViewStore.tableName.guids} WHERE rowId=?`, (stmt) => { for (const rowId of rowIds) { stmt.reset(); stmt.bindInteger(1, rowId); if (stmt.nextRow()) fn(stmt.getValueGuid(0), rowId); } }); } /** @internal */ addGuid(guid) { const existing = this.getGuidRow(guid); return existing !== 0 ? existing : this.withPreparedSqliteStatement(`INSERT INTO ${ViewStore.tableName.guids} (guid) VALUES(?)`, (stmt) => { stmt.bindGuid(1, guid); stmt.stepForWrite(); return this[Symbols_1._nativeDb].getLastInsertRowId(); }); } /** @internal */ addViewRow(args) { validateName(args.name, "view"); return this.withSqliteStatement(`INSERT INTO ${ViewStore.tableName.views} (className,name,json,owner,private,groupId,modelSel,categorySel,displayStyle) VALUES(?,?,?,?,?,?,?,?,?)`, (stmt) => { stmt.bindString(1, args.className); stmt.bindString(2, args.name); stmt.bindString(3, args.json); stmt.maybeBindString(4, args.owner); stmt.bindBoolean(5, args.isPrivate ?? false); stmt.bindInteger(6, args.groupId ?? 1); stmt.maybeBindInteger(7, args.modelSel); stmt.bindInteger(8, args.categorySel); stmt.bindInteger(9, args.displayStyle); stmt.stepForWrite(); return this[Symbols_1._nativeDb].getLastInsertRowId(); }); } /** @internal */ addViewGroupRow(args) { validateName(args.name, "group"); return this.withSqliteStatement(`INSERT INTO ${ViewStore.tableName.viewGroups} (name,owner,parent,json) VALUES(?,?,?,?)`, (stmt) => { stmt.bindString(1, args.name); stmt.maybeBindString(2, args.owner); stmt.bindInteger(3, args.parentId ?? 1); stmt.bindString(4, args.json); stmt.stepForWrite(); return this[Symbols_1._nativeDb].getLastInsertRowId(); }); } addTableRow(table, args) { return this.withSqliteStatement(`INSERT INTO ${table} (name,json,owner) VALUES(?,?,?)`, (stmt) => { stmt.maybeBindString(1, args.name); stmt.bindString(2, args.json); stmt.maybeBindString(3, args.owner); stmt.stepForWrite(); return this[Symbols_1._nativeDb].getLastInsertRowId(); }); } /** add a row to the "modelSelectors" table, return the RowId * @internal */ addModelSelectorRow(args) { return this.addTableRow(ViewStore.tableName.modelSelectors, args); } /** add a row to the "categorySelectors" table, return the RowId * @internal */ addCategorySelectorRow(args) { return this.addTableRow(ViewStore.tableName.categorySelectors, args); } /** add a row to the "displayStyles" table, return the RowId * @internal */ addDisplayStyleRow(args) { return this.addTableRow(ViewStore.tableName.displayStyles, args); } /** add a row to the "timelines" table, return the RowId * @internal */ addTimelineRow(args) { return this.addTableRow(ViewStore.tableName.timelines, args); } /** add a row to the "tags" table, return the RowId * @internal */ addTag(args) { return this.addTableRow(ViewStore.tableName.tags, args); } /** add a row to the "searches" table, return the RowId * @internal */ async addSearch(args) { return this.addTableRow(ViewStore.tableName.searches, args); } /** add or update a row in the "thumbnails" table, return the RowId * @internal */ addOrReplaceThumbnailRow(args) { return this.withSqliteStatement(`INSERT OR REPLACE INTO ${ViewStore.tableName.thumbnails} (Id,json,owner,data) VALUES(?,?,?,?)`, (stmt) => { stmt.bindInteger(1, args.viewId); stmt.bindString(2, JSON.stringify(args.format)); stmt.maybeBindString(3, args.owner); stmt.bindBlob(4, args.data); stmt.stepForWrite(); return this[Symbols_1._nativeDb].getLastInsertRowId(); }); } deleteFromTable(table, id) { this.withSqliteStatement(`DELETE FROM ${table} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, ViewStore.toRowId(id)); stmt.stepForWrite(); }); } /** @internal */ deleteViewRow(id) { return this.deleteFromTable(ViewStore.tableName.views, id); } async deleteViewGroup(args) { const rowId = this.findViewGroup(args.name); if (rowId === 1) core_common_1.ViewStoreError.throwError("group-error", { message: "Cannot delete root group" }); return this.deleteFromTable(ViewStore.tableName.viewGroups, rowId); } deleteModelSelectorSync(id) { return this.deleteFromTable(ViewStore.tableName.modelSelectors, id); } async deleteModelSelector(args) { return this.deleteModelSelectorSync(args.id); } deleteCategorySelectorSync(id) { return this.deleteFromTable(ViewStore.tableName.categorySelectors, id); } deleteDisplayStyleSync(id) { return this.deleteFromTable(ViewStore.tableName.displayStyles, id); } deleteTimelineSync(id) { return this.deleteFromTable(ViewStore.tableName.timelines, id); } deleteTagSync(arg) { const id = this.findTagByName(arg.name); return this.deleteFromTable(ViewStore.tableName.tags, id); } async deleteTag(arg) { return this.deleteTagSync(arg); } async deleteCategorySelector(args) { return this.deleteCategorySelectorSync(args.id); } async deleteDisplayStyle(args) { return this.deleteDisplayStyleSync(args.id); } async deleteTimeline(args) { return this.deleteTimelineSync(args.id); } deleteSearch(id) { return this.deleteFromTable(ViewStore.tableName.searches, id); } deleteThumbnailSync(id) { return this.deleteFromTable(ViewStore.tableName.thumbnails, ViewStore.toRowId(id)); } async deleteThumbnail(arg) { return this.deleteThumbnailSync(arg.viewId); } /** get the data for a view from the database * @internal */ getViewRow(viewId) { return this.withSqliteStatement(`SELECT className,name,json,owner,private,groupId,modelSel,categorySel,displayStyle FROM ${ViewStore.tableName.views} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, viewId); return !stmt.nextRow() ? undefined : { className: stmt.getValueString(0), name: stmt.getValueString(1), json: stmt.getValueString(2), owner: stmt.getValueStringMaybe(3), isPrivate: stmt.getValueBoolean(4), groupId: stmt.getValueInteger(5), modelSel: stmt.getValueIntegerMaybe(6), categorySel: stmt.getValueInteger(7), displayStyle: stmt.getValueInteger(8), }; }); } /** @internal */ getThumbnailRow(viewId) { return this.withSqliteStatement(`SELECT json,owner,data FROM ${ViewStore.tableName.thumbnails} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, viewId); return !stmt.nextRow() ? undefined : { viewId, format: JSON.parse(stmt.getValueString(0)), owner: stmt.getValueStringMaybe(1), data: stmt.getValueBlob(2), }; }); } /** @internal */ getViewGroup(id) { return this.withSqliteStatement(`SELECT name,owner,json,parent,defaultViewId FROM ${ViewStore.tableName.viewGroups} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, id); return !stmt.nextRow() ? undefined : { name: stmt.getValueString(0), owner: stmt.getValueStringMaybe(1), json: stmt.getValueString(2), parentId: stmt.getValueInteger(3), defaultViewId: stmt.getValueIntegerMaybe(4), }; }); } getTableRow(table, id) { return this.withSqliteStatement(`SELECT name,json,owner FROM ${table} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, id); return !stmt.nextRow() ? undefined : { name: stmt.getValueStringMaybe(0), json: stmt.getValueString(1), owner: stmt.getValueStringMaybe(2), }; }); } /** read a ModelSelector given a rowId * @internal */ getModelSelectorRow(id) { return this.getTableRow(ViewStore.tableName.modelSelectors, id); } /** read a CategorySelector given a rowId * @internal */ getCategorySelectorRow(id) { return this.getTableRow(ViewStore.tableName.categorySelectors, id); } /** read a DisplayStyle given a rowId * @internal */ getDisplayStyleRow(id) { return this.getTableRow(ViewStore.tableName.displayStyles, id); } /** @internal */ getTimelineRow(id) { return this.getTableRow(ViewStore.tableName.timelines, id); } /** @internal */ getTag(id) { return this.getTableRow(ViewStore.tableName.tags, id); } /** @internal */ getSearch(id) { return this.getTableRow(ViewStore.tableName.searches, id); } updateJson(table, id, json) { this.withSqliteStatement(`UPDATE ${table} SET json=? WHERE Id=?`, (stmt) => { stmt.bindString(1, json); stmt.bindInteger(2, ViewStore.toRowId(id)); stmt.stepForWrite(); }); } async updateViewShared(arg) { if (!arg.isShared && arg.owner === undefined) core_common_1.ViewStoreError.throwError("no-owner", { message: "owner must be defined for private views" }); this.withSqliteStatement(`UPDATE ${ViewStore.tableName.views} SET private=?,owner=? WHERE Id=?`, (stmt) => { stmt.bindBoolean(1, !arg.isShared); stmt.maybeBindString(2, arg.owner); stmt.bindInteger(3, ViewStore.toRowId(arg.viewId)); stmt.stepForWrite(); }); } /** @internal */ updateViewGroupJson(groupId, json) { return this.updateJson(ViewStore.tableName.viewGroups, groupId, json); } /** @internal */ updateModelSelectorJson(modelSelectorId, json) { return this.updateJson(ViewStore.tableName.modelSelectors, modelSelectorId, json); } /** @internal */ updateCategorySelectorJson(categorySelectorId, json) { return this.updateJson(ViewStore.tableName.categorySelectors, categorySelectorId, json); } /** @internal */ updateDisplayStyleJson(styleId, json) { return this.updateJson(ViewStore.tableName.displayStyles, styleId, json); } /** @internal */ updateTimelineJson(timelineId, json) { return this.updateJson(ViewStore.tableName.timelines, timelineId, json); } /** @internal */ updateSearchJson(searchId, json) { return this.updateJson(ViewStore.tableName.searches, searchId, json); } updateName(table, id, name) { this.withSqliteStatement(`UPDATE ${table} SET name=? WHERE Id=?`, (stmt) => { stmt.maybeBindString(1, name); stmt.bindInteger(2, ViewStore.toRowId(id)); stmt.stepForWrite(); }); } async renameView(args) { return this.updateName(ViewStore.tableName.views, args.viewId, args.name); } async renameViewGroup(args) { return this.updateName(ViewStore.tableName.viewGroups, args.groupId, args.name); } async renameModelSelector(args) { return this.updateName(ViewStore.tableName.modelSelectors, args.id, args.name); } async renameCategorySelector(args) { return this.updateName(ViewStore.tableName.categorySelectors, args.id, args.name); } async renameDisplayStyle(args) { return this.updateName(ViewStore.tableName.displayStyles, args.id, args.name); } async renameTimeline(args) { return this.updateName(ViewStore.tableName.timelines, args.id, args.name); } async renameSearch(args) { return this.updateName(ViewStore.tableName.searches, args.id, args.name); } async renameTag(args) { this.withSqliteStatement(`UPDATE ${ViewStore.tableName.tags} SET name=? WHERE name=?`, (stmt) => { stmt.bindString(1, args.newName); stmt.bindString(2, args.oldName); stmt.stepForWrite(); }); } /** @internal */ addTagToView(args) { this.withSqliteStatement(`INSERT OR IGNORE INTO ${ViewStore.tableName.taggedViews} (viewId,tagId) VALUES(?,?)`, (stmt) => { stmt.bindInteger(1, args.viewId); stmt.bindInteger(2, args.tagId); stmt.stepForWrite(); }); } deleteViewTag(args) { this.withSqliteStatement(`DELETE FROM ${ViewStore.tableName.taggedViews} WHERE viewId=? AND tagId=?`, (stmt) => { stmt.bindInteger(1, args.viewId); stmt.bindInteger(2, args.tagId); stmt.stepForWrite(); }); } /** @internal */ findViewsForTag(tagId) { return this.withSqliteStatement(`SELECT viewId FROM ${ViewStore.tableName.taggedViews} WHERE tagId=?`, (stmt) => { stmt.bindInteger(1, tagId); const list = []; while (stmt.nextRow()) list.push(stmt.getValueInteger(0)); return list; }); } findByName(table, name) { return this.withSqliteStatement(`SELECT Id FROM ${table} WHERE name=?`, (stmt) => { stmt.bindString(1, name); return !stmt.nextRow() ? 0 : stmt.getValueInteger(0); }); } /** @internal */ getViewGroupByName(name, parentId) { return this.withSqliteStatement(`SELECT Id FROM ${ViewStore.tableName.viewGroups} WHERE name=? AND parent=?`, (stmt) => { stmt.bindString(1, name); stmt.bindInteger(2, parentId); return !stmt.nextRow() ? 0 : stmt.getValueInteger(0); }); } /** @internal */ findModelSelectorByName(name) { return this.findByName(ViewStore.tableName.modelSelectors, name); } /** @internal */ findCategorySelectorByName(name) { return this.findByName(ViewStore.tableName.categorySelectors, name); } /** @internal */ findDisplayStyleByName(name) { return this.findByName(ViewStore.tableName.displayStyles, name); } /** @internal */ findTagByName(name) { return this.findByName(ViewStore.tableName.tags, name); } /** @internal */ findTimelineByName(name) { return this.findByName(ViewStore.tableName.timelines, name); } /** @internal */ findSearchByName(name) { return this.findByName(ViewStore.tableName.searches, name); } getViewInfoSync(id) { const maybeId = (rowId) => rowId ? ViewStore.fromRowId(rowId) : undefined; return this.withPreparedSqliteStatement(`SELECT owner,className,name,private,groupId,modelSel,categorySel,displayStyle FROM ${ViewStore.tableName.views} WHERE id=?`, (stmt) => { const viewId = ViewStore.toRowId(id); stmt.bindInteger(1, viewId); return stmt.nextRow() ? { id: ViewStore.fromRowId(viewId), owner: stmt.getValueString(0), className: stmt.getValueString(1), name: stmt.getValueStringMaybe(2), isPrivate: stmt.getValueBoolean(3), groupId: ViewStore.fromRowId(stmt.getValueInteger(4)), modelSelectorId: maybeId(stmt.getValueInteger(5)), categorySelectorId: ViewStore.fromRowId(stmt.getValueInteger(6)), displayStyleId: ViewStore.fromRowId(stmt.getValueInteger(7)), tags: this.getTagsForView(viewId), } : undefined; }); } async getViewInfo(args) { return this.getViewInfoSync(args.viewId); } async findViewsByOwner(args) { const list = []; this.withSqliteStatement(`SELECT Id FROM ${ViewStore.tableName.views} WHERE owner=? ORDER BY Id ASC`, (stmt) => { stmt.bindString(1, args.owner); while (stmt.nextRow()) { const info = this.getViewInfoSync(stmt.getValueInteger(0)); if (info) list.push(info); } }); return list; } /** @internal */ findTagIdsForView(viewId) { return this.withSqliteStatement(`SELECT tagId FROM ${ViewStore.tableName.taggedViews} WHERE viewId=?`, (stmt) => { stmt.bindInteger(1, viewId); const list = []; while (stmt.nextRow()) list.push(stmt.getValueInteger(0)); return list; }); } toGuidRow(id) { if (undefined === id) return undefined; const fedGuid = this.guidMap.getFederationGuidFromId(id); return fedGuid ? this.addGuid(fedGuid) : undefined; } toCompressedGuidRows(ids) { const result = new Set(); for (const id of (typeof ids === "string" ? core_bentley_1.CompressedId64Set.iterable(ids) : ids)) { const guidRow = this.toGuidRow(id); if (undefined !== guidRow) result.add(core_bentley_1.Id64.fromLocalAndBriefcaseIds(guidRow, 0)); } return core_bentley_1.CompressedId64Set.compressSet(result); } fromGuidRow(guidRow) { return this.guidMap.getIdFromFederationGuid(this.getGuid(guidRow)); } fromGuidRowString(id) { return (typeof id !== "string" || !isGuidRowString(id)) ? id : this.fromGuidRow(ViewStore.toRowId(id)); } iterateCompressedGuidRows(guidRows, callback) { if (typeof guidRows !== "string") return; for (const rowId64String of core_bentley_1.CompressedId64Set.iterable(guidRows)) { const elId = this.fromGuidRow(core_bentley_1.Id64.getLocalId(rowId64String)); if (undefined !== elId) callback(elId); } } fromCompressedGuidRows(guidRows) { const result = new Set(); this.iterateCompressedGuidRows(guidRows, (id) => result.add(id)); return core_bentley_1.CompressedId64Set.compressSet(result); } toGuidRowMember(base, memberName) { const id = base?.[memberName]; if (id === undefined) return; if (typeof id === "string") { if (isGuidRowString(id)) { // member is already a guid row. Make sure it exists. if (undefined === this.getGuid(ViewStore.toRowId(id))) core_common_1.ViewStoreError.throwError("invalid-member", { message: `${memberName} id does not exist` }); return; } const guidRow = this.toGuidRow(id); if (undefined !== guidRow) { base[memberName] = guidRowToString(guidRow); return; } } core_common_1.ViewStoreError.throwError("invalid-member", { message: `invalid ${memberName}: ${id} ` }); } fromGuidRowMember(base, memberName) { const id = base?.[memberName]; if (id === undefined) return; if (typeof id === "string" && isGuidRowString(id)) { const elId = this.fromGuidRow(ViewStore.toRowId(id)); if (undefined !== elId) { base[memberName] = elId; return; } } core_common_1.ViewStoreError.throwError("invalid-member", { message: `invalid ${memberName}: ${id} ` }); } verifyRowId(table, rowIdString) { try { const rowId = ViewStore.toRowId(rowIdString); this.withSqliteStatement(`SELECT 1 FROM ${table} WHERE Id=?`, (stmt) => { stmt.bindInteger(1, rowId); if (!stmt.nextRow()) core_common_1.ViewStoreError.throwError("not-found", { message: `missing: ${rowIdString} ` }); }); return rowId; } catch (err) { core_common_1.ViewStoreError.throwError("invalid-value", { message: `invalid Id for ${table}: ${err.message} ` }); } } scriptToGuids(script) { const scriptProps = []; for (const model of script) { const modelGuidRow = this.toGuidRow(model.modelId); if (modelGuidRow) { model.modelId = guidRowToString(modelGuidRow); scriptProps.push(model); for (const batch of model.elementTimelines) batch.elementIds = this.toCompressedGuidRows(batch.elementIds); } } return scriptProps; } scriptFromGuids(script, omitElementIds) { const scriptProps = []; for (const model of script) { const modelId = this.fromGuidRow(ViewStore.toRowId(model.modelId)); if (modelId) { model.modelId = modelId; scriptProps.push(model); for (const batch of model.elementTimelines) { if (undefined !== batch.elementIds) batch.elementIds = omitElementIds ? "" : this.fromCompressedGuidRows(batch.elementIds); } } } return scriptProps; } async addViewGroup(args) { const parentId = args.parentId ? this.findViewGroup(args.parentId) : ViewStore.defaultViewGroupId; const json = JSON.stringify({}); return ViewStore.fromRowId(this.addViewGroupRow({ name: args.name, parentId, json, owner: args.owner })); } async getViewGroups(args) { const parentIdRow = args.parent ? this.findViewGroup(args.parent) : ViewStore.defaultViewGroupId; const groups = []; this.withSqliteStatement(`SELECT Id,Name FROM ${ViewStore.tableName.viewGroups} WHERE parent=?`, (stmt) => { stmt.bindInteger(1, parentIdRow); while (stmt.nextRow()) { const id = stmt.getValueInteger(0); if (id !== ViewStore.defaultViewGroupId) // don't include root group groups.push({ id: ViewStore.fromRowId(id), name: stmt.getValueString(1) }); } }); return groups; } makeSelectorJson(props, entity) { const selector = { ...props }; // shallow copy if (selector.query) { selector.query = { ...selector.query }; // shallow copy selector.query.from = selector.query.from.toLowerCase().replace(".", ":").replace("bis:", "biscore:"); if (!this.iModel.getJsClass(selector.query.from).is(entity)) core_common_1.ViewStoreError.throwError("invalid-value", { message: `query must select from ${entity.classFullName}` }); if (selector.query.adds) selector.query.adds = this.toCompressedGuidRows(selector.query.adds); if (selector.query.removes) selector.query.removes = this.toCompressedGuidRows(selector.query.removes); } else { if (!(selector.ids.length)) core_common_1.ViewStoreError.throwError("invalid-value", { message: `Selector must specify at least one ${entity.className}` }); selector.ids = this.toCompressedGuidRows(selector.ids); } return JSON.stringify(selector); } querySelectorValues(json, bindings) { if (typeof json !== "object") core_common_1.ViewStoreError.throwError("invalid-value", { message: "invalid selector" }); const props = json; if (!props.query) { // there's no query, so the ids are the list of elements return (typeof props.ids === "string") ? core_bentley_1.CompressedId64Set.decompressArray(this.fromCompressedGuidRows(props.ids)) : []; } const query = props.query; const sql = `SELECT ECInstanceId FROM ${query.only ? "ONLY " : ""}${query.from}${query.where ? ` WHERE ${query.where}` : ""}`; const ids = new Set(); try { // eslint-disable-next-line @typescript-eslint/no-deprecated this.iModel.withPreparedStatement(sql, (stmt) => { if (bindings) stmt.bindValues(bindings); for (const el of stmt) { if (typeof el.id === "string") ids.add(el.id); } }); this.iterateCompressedGuidRows(props.query.adds, (id) => ids.add(id)); this.iterateCompressedGuidRows(props.query.removes, (id) => ids.delete(id)); // removes take precedence over adds } catch (err) { core_bentley_1.Logger.logError("ViewStore", `querySelectorValues: ${err.message}`); } return [...ids]; } async addCategorySelector(args) { const json = this.makeSelectorJson(args.selector, Category_1.Category); return ViewStore.fromRowId(this.addCategorySelectorRow({ name: args.name, owner: args.owner, json })); } async updateCategorySelector(args) { const rowId = this.getRowId(ViewStore.tableName.categorySelectors, args); const json = this.makeSelectorJson(args.selector, Category_1.Category); return this.updateCategorySelectorJson(rowId, json); } getRowId(table, arg) { return undefined !== arg.name ? this.findByName(table, arg.name) : ViewStore.toRowId(arg.id); } /** @internal */ getCategorySelectorSync(args) { const rowId = this.getRowId(ViewStore.tableName.categorySelectors, args); const row = this.getCategorySelectorRow(rowId); if (undefined === row) core_common_1.ViewStoreError.throwError("not-found", { message: "CategorySelector not found" }); const props = blankElementProps({}, "BisCore:CategorySelector", rowId, row.name); props.categories = this.querySelectorValues(JSON.parse(row.json), args.bindings); return props; } async getCategorySelector(args) { return this.getCategorySelectorSync(args); } async addModelSelector(args) { const json = this.makeSelectorJson(args.selector, Model_1.Model); return ViewStore.fromRowId(this.addModelSelectorRow({ name: args.name, owner: args.owner, json })); } async updateModelSelector(args) { const rowId = this.getRowId(ViewStore.tableName.modelSelectors, args); const json = this.makeSelectorJson(args.selector, Model_1.Model); return this.updateModelSelectorJson(rowId, json); } getModelSelectorSync(args) { const rowId = this.getRowId(ViewStore.tableName.modelSelectors, args); const row = this.getModelSelectorRow(rowId); if (undefined === row) core_common_1.ViewStoreError.throwError("not-found", { message: "ModelSelector not found" }); const props = blankElementProps({}, "BisCore:ModelSelector", rowId, row?.name); props.models = this.querySelectorValues(JSON.parse(row.json), args.bindings); return props; } async getModelSelector(args) { return this.getModelSelectorSync(args); } makeTimelineJson(timeline) { timeline = cloneProps(timeline); if (!Array.isArray(timeline)) core_common_1.ViewStoreError.throwError("not-found", { message: "Timeline has no entries" }); return JSON.stringify(this.scriptToGuids(timeline)); } async addTimeline(args) { const json = this.makeTimelineJson(args.timeline); return ViewStore.fromRowId(this.addTimelineRow({ name: args.name, owner: args.owner, json })); } async updateTimeline(args) { const rowId = this.getRowId(ViewStore.tableName.timelines, args); const json = this.makeTimelineJson(args.timeline); return this.updateTimelineJson(rowId, json); } getTimelineSync(args) { const rowId = this.getRowId(ViewStore.tableName.timelines, args); const row = this.getTimelineRow(rowId); if (undefined === row) core_common_1.ViewStoreError.throwError("not-found", { message: "Timeline not found" }); const props = blankElementProps({}, "BisCore:RenderTimeline", rowId, row?.name); props.script = JSON.stringify(this.scriptFromGuids(JSON.parse(row.json), false)); return props; } async getTimeline(args) { return this.getTimelineSync(args); } /** make a JSON string for a DisplayStyle */ makeDisplayStyleJson(args) { const settings = cloneProps(args.settings); // don't modify input if (settings.subCategoryOvr) { const outOvr = []; for (const ovr of settings.subCategoryOvr) { const subCategoryGuidRow = this.toGuidRow(ovr.subCategory); if (subCategoryGuidRow) { ovr.subCategory = guidRowToString(subCategoryGuidRow); outOvr.push(ovr); } } settings.subCategoryOvr = outOvr; } if (settings.excludedElements) settings.excludedElements = this.toCompressedGuidRows(settings.excludedElements); const settings3d = settings; if (settings3d.planProjections) { const planProjections = {}; for (const entry of Object.entries(settings3d.planProjections)) { const modelGuidRow = this.toGuidRow(entry[0]); if (modelGuidRow) planProjections[guidRowToString(modelGuidRow)] = entry[1]; } settings3d.planProjections = planProjections; } if (settings.renderTimeline) { if (!core_common_1.ViewStoreRpc.isViewStoreId(settings.renderTimeline)) this.toGuidRowMember(settings, "renderTimeline"); delete settings.scheduleScript; } else if (settings.scheduleScript) { const scriptProps = this.scriptToGuids(settings.scheduleScript); if (scriptProps.length > 0) settings.scheduleScript = scriptProps; } return JSON.stringify({ settings, className: args.className }); } async addDisplayStyle(args) { const json = this.makeDisplayStyleJson(args); return ViewStore.fromRowId(this.addDisplayStyleRow({ name: args.name, owner: args.owner, json })); } async updateDisplayStyle(args) { const rowId = this.getRowId(ViewStore.tableName.displayStyles, args); const json = this.makeDisplayStyleJson(args); return this.updateDisplayStyleJson(rowId, json); } getDisplayStyleSync(args) { const rowId = this.getRowId(ViewStore.tableName.displayStyles, args); const row = this.getDisplayStyleRow(rowId); if (undefined === row) core_common_1.ViewStoreError.throwError("not-found", { message: "DisplayStyle not found" }); const val = JSON.parse(row.json); const props = blankElementProps({}, val.className, rowId, row.name); props.jsonProperties = { styles: val.settings }; const settings = val.settings; if (settings.subCategoryOvr) { const subCatOvr = []; for (const ovr of settings.subCategoryOvr) { const id = this.fromGuidRowString(ovr.subCategory); if (undefined !== id) { ovr.subCategory = id; subCatOvr.push(ovr); } } settings.subCategoryOvr = subCatOvr; } if (settings.excludedElements) settings.excludedElements = this.fromCompressedGuidRows(settings.excludedElements); const settings3d = settings; if (settings3d.planProjections) { const planProjections = {}; for (const entry of Object.entries(settings3d.planProjections)) { const modelId = this.fromGuidRowString(entry[0]); if (undefined !== modelId) planProjections[modelId] = entry[1]; } settings3d.planProjections = planProjections; } if (isGuidRowString(settings.renderTimeline)) settings.renderTimeline = this.fromGuidRowString(settings.renderTimeline); if (undefined !== settings.renderTimeline) { delete settings.scheduleScript; } else if (settings.scheduleScript) { delete settings.renderTimeline; settings.scheduleScript = this.scriptFromGuids(settings.scheduleScript, args.opts?.omitScheduleScriptElementIds === true); } return props; } async getDisplayStyle(args) { return this.getDisplayStyleSync(args); } makeViewDefinitionProps(viewDefinition) { const viewDef = cloneProps(viewDefinition); // don't modify input this.verifyRowId(ViewStore.tableName.categorySelectors, viewDef.categorySelectorId); this.verifyRowId(ViewStore.tableName.displayStyles, viewDef.displayStyleId); if (viewDef.modelSelectorId) this.verifyRowId(ViewStore.tableName.modelSelectors, viewDef.modelSelectorId); this.toGuidRowMember(viewDef, "baseModelId"); this.toGuidRowMember(viewDef.jsonProperties?.viewDetails, "acs"); const props = viewDef; delete props.id; delete props.federationGuid; delete props.parent; delete props.code; delete props.model; return viewDef; } addViewDefinition(args) { const name = args.viewDefinition.code.value; if (name === undefined) core_common_1.ViewStoreError.throwError("not-found", { message: "ViewDefinition must have a name" }); const groupId = args.group ? this.findViewGroup(args.group) : ViewStore.defaultViewGroupId; const maybeRow = (rowString) => rowString ? ViewStore.toRowId(rowString) : undefined; const viewDef = this.makeViewDefinitionProps(args.viewDefinition); try { return this.addViewRow({ name, className: viewDef.classFullName, owner: args.owner, groupId, isPrivate: args.isPrivate, json: JSON.stringify(viewDef), modelSel: maybeRow(viewDef.modelSelectorId), categorySel: ViewStore.toRowId(viewDef.categorySelectorId), displayStyle: ViewStore.toRowId(viewDef.displayStyleId), }); } catch (e) { const err = e; if (err.errorId === "DuplicateValue") err.message = `View "${name}" already exists`; throw e; } } async updateViewDefinition(args) { const maybeRow = (rowString) => rowString ? ViewStore.toRowId(rowString) : undefined; const viewDef = this.makeViewDefinitionProps(args.viewDefinition); this.withSqliteStatement(`UPDATE ${ViewStore.tableName.views} SET json=?,modelSel=?,categorySel=?,displayStyle=? WHERE Id=?`, (stmt) => { stmt.bindString(1, JSON.stringify(viewDef)); stmt.maybeBindInteger(2, maybeRow(viewDef.modelSelectorId)); stmt.bindInteger(3, ViewStore.toRowId(viewDef.categorySelectorId)); stmt.bindInteger(4, ViewStore.toRowId(viewDef.displayStyleId)); stmt.bindInteger(5, ViewStore.toRowId(args.viewId)); stmt.stepForWrite(); }); } getViewDefinitionSync(args) { const viewId = ViewStore.toRowId(args.viewId); const row = this.getViewRow(viewId); if (undefined === row) core_common_1.ViewStoreError.throwError("not-found", { message: "View not found" });