@itwin/core-backend
Version:
iTwin.js backend components
1,012 lines (1,011 loc) • 63.6 kB
JavaScript
"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" });