@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
187 lines • 8.84 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Core
*/
import { bufferCount, defer, from, groupBy, map, mergeMap, of, range, reduce } from "rxjs";
import { Id64, OrderedId64Iterable } from "@itwin/core-bentley";
import { Descriptor, KeySet, PresentationError, PresentationStatus, } from "@itwin/presentation-common";
/** @internal */
export function parseFullClassName(fullClassName) {
const [schemaName, className] = fullClassName.split(/[:\.]/);
return [schemaName, className];
}
function getECSqlName(fullClassName) {
const [schemaName, className] = parseFullClassName(fullClassName);
return `[${schemaName}].[${className}]`;
}
/** @internal */
export function getContentItemsObservableFromElementIds(imodel, contentDescriptorGetter, contentSetGetter, elementIds, classParallelism, batchesParallelism, batchSize) {
return {
itemBatches: getElementClassesFromIds(imodel, elementIds).pipe(mergeMap(({ classFullName, ids }) => getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, () => createIdBatches(OrderedId64Iterable.sortArray(ids), batchSize), batchesParallelism), classParallelism)),
count: of(elementIds.length),
};
}
/** @internal */
export function getContentItemsObservableFromClassNames(imodel, contentDescriptorGetter, contentSetGetter, elementClasses, classParallelism, batchesParallelism, batchSize) {
return {
itemBatches: getClassesWithInstances(imodel, elementClasses).pipe(mergeMap((classFullName) => getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, () => getBatchedClassElementIds(imodel, classFullName, batchSize), batchesParallelism), classParallelism)),
count: from(getElementsCount(imodel, elementClasses)),
};
}
function getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, batcher, batchesParallelism) {
return defer(async () => {
const ruleset = createClassContentRuleset(classFullName);
const keys = new KeySet();
const descriptor = await contentDescriptorGetter({ rulesetOrId: ruleset, keys });
if (!descriptor) {
throw new PresentationError(PresentationStatus.Error, `Failed to get descriptor for class ${classFullName}`);
}
return { descriptor, keys, ruleset };
}).pipe(
// create elements' id batches
mergeMap((x) => batcher().pipe(map((batch) => ({ ...x, batch })))),
// request content for each batch, filter by IDs for performance
mergeMap(({ descriptor, keys, ruleset, batch }) => defer(async () => {
const filteringDescriptor = new Descriptor(descriptor);
filteringDescriptor.instanceFilter = {
selectClassName: classFullName,
expression: createElementIdsECExpressionFilter(batch),
};
return contentSetGetter({
rulesetOrId: ruleset,
keys,
descriptor: filteringDescriptor,
});
}).pipe(map((items) => ({ descriptor, items }))), batchesParallelism));
}
function createElementIdsECExpressionFilter(batch) {
let filter = "";
function appendCondition(cond) {
if (filter.length > 0) {
filter += " OR ";
}
filter += cond;
}
for (const item of batch) {
if (item.from === item.to) {
appendCondition(`this.ECInstanceId = ${item.from}`);
}
else {
appendCondition(`this.ECInstanceId >= ${item.from} AND this.ECInstanceId <= ${item.to}`);
}
}
return filter;
}
function createClassContentRuleset(fullClassName) {
const [schemaName, className] = parseFullClassName(fullClassName);
return {
id: `content/class-descriptor/${fullClassName}`,
rules: [
{
ruleType: "Content",
specifications: [
{
specType: "ContentInstancesOfSpecificClasses",
classes: {
schemaName,
classNames: [className],
arePolymorphic: false,
},
handlePropertiesPolymorphically: true,
},
],
},
],
};
}
/** Given a list of element ids, group them by class name. */
function getElementClassesFromIds(imodel, elementIds) {
const elementIdsBatchSize = 5000;
return range(0, elementIds.length / elementIdsBatchSize).pipe(mergeMap((batchIndex) => {
const idsFrom = batchIndex * elementIdsBatchSize;
const idsTo = Math.min(idsFrom + elementIdsBatchSize, elementIds.length);
return from(imodel.createQueryReader(`
SELECT ec_classname(e.ECClassId) className, GROUP_CONCAT(IdToHex(e.ECInstanceId)) ids
FROM bis.Element e
WHERE e.ECInstanceId IN (${elementIds.slice(idsFrom, idsTo).join(",")})
GROUP BY e.ECClassId
`));
}), map((row) => ({ className: row.className, ids: row.ids.split(",") })), groupBy(({ className }) => className), mergeMap((groups) => groups.pipe(reduce((acc, g) => {
g.ids.forEach((id) => acc.ids.push(id));
return {
classFullName: g.className,
ids: acc.ids,
};
}, { classFullName: "", ids: [] }))));
}
/** Given a list of full class names, get a stream of actual class names that have instances. */
function getClassesWithInstances(imodel, fullClassNames) {
return from(fullClassNames).pipe(mergeMap((fullClassName) => from(imodel.createQueryReader(`
SELECT ec_classname(e.ECClassId, 's.c') className
FROM ${getECSqlName(fullClassName)} e
GROUP BY e.ECClassId
`))), map((row) => row.className));
}
/**
* Given a sorted list of ECInstanceIds and a batch size, create a stream of batches. Because the IDs won't necessarily
* be sequential, a batch is defined a list of from-to pairs.
* @internal
*/
export function createIdBatches(sortedIds, batchSize) {
return range(0, sortedIds.length / batchSize).pipe(map((batchIndex) => {
const sequences = new Array();
const startIndex = batchIndex * batchSize;
const endIndex = Math.min((batchIndex + 1) * batchSize, sortedIds.length) - 1;
let fromId = sortedIds[startIndex];
let to = {
id: sortedIds[startIndex],
localId: Id64.getLocalId(sortedIds[startIndex]),
};
for (let i = startIndex + 1; i <= endIndex; ++i) {
const currLocalId = Id64.getLocalId(sortedIds[i]);
if (currLocalId !== to.localId + 1) {
sequences.push({ from: fromId, to: sortedIds[i - 1] });
fromId = sortedIds[i];
}
to = { id: sortedIds[i], localId: currLocalId };
}
sequences.push({ from: fromId, to: sortedIds[endIndex] });
return sequences;
}));
}
/**
* Query all ECInstanceIds from given class and stream from-to pairs that batch the items into batches of `batchSize` size.
* @internal
*/
export function getBatchedClassElementIds(imodel, fullClassName, batchSize) {
return from(imodel.createQueryReader(`SELECT IdToHex(ECInstanceId) id FROM ${getECSqlName(fullClassName)} ORDER BY ECInstanceId`)).pipe(map((row) => row.id), bufferCount(batchSize), map((batch) => [{ from: batch[0], to: batch[batch.length - 1] }]));
}
/** @internal */
export async function getElementsCount(db, classNames) {
const whereClause = (() => {
if (classNames === undefined || classNames.length === 0) {
return undefined;
}
// check if list contains only valid class names
const classNameRegExp = new RegExp(/^[\w]+[.:][\w]+$/);
const invalidName = classNames.find((name) => !name.match(classNameRegExp));
if (invalidName) {
throw new PresentationError(PresentationStatus.InvalidArgument, `Encountered invalid class name - ${invalidName}.
Valid class name formats: "<schema name or alias>.<class name>", "<schema name or alias>:<class name>"`);
}
return `e.ECClassId IS (${classNames.join(",")})`;
})();
const query = `
SELECT COUNT(e.ECInstanceId) AS elementCount
FROM bis.Element e
${whereClause ? `WHERE ${whereClause}` : ""}
`;
for await (const row of db.createQueryReader(query)) {
return row.elementCount;
}
return 0;
}
//# sourceMappingURL=ElementPropertiesHelper.js.map