@itwin/presentation-backend
Version:
Backend of iTwin.js Presentation library
195 lines • 9.67 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseFullClassName = parseFullClassName;
exports.getContentItemsObservableFromElementIds = getContentItemsObservableFromElementIds;
exports.getContentItemsObservableFromClassNames = getContentItemsObservableFromClassNames;
exports.createIdBatches = createIdBatches;
exports.getBatchedClassElementIds = getBatchedClassElementIds;
exports.getElementsCount = getElementsCount;
const rxjs_1 = require("rxjs");
const core_bentley_1 = require("@itwin/core-bentley");
const presentation_common_1 = require("@itwin/presentation-common");
/** @internal */
function parseFullClassName(fullClassName) {
const [schemaName, className] = fullClassName.split(/[:\.]/);
return [schemaName, className];
}
function getECSqlName(fullClassName) {
const [schemaName, className] = parseFullClassName(fullClassName);
return `[${schemaName}].[${className}]`;
}
/** @internal */
function getContentItemsObservableFromElementIds(imodel, contentDescriptorGetter, contentSetGetter, elementIds, classParallelism, batchesParallelism, batchSize) {
return {
itemBatches: getElementClassesFromIds(imodel, elementIds).pipe((0, rxjs_1.mergeMap)(({ classFullName, ids }) => getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, () => createIdBatches(core_bentley_1.OrderedId64Iterable.sortArray(ids), batchSize), batchesParallelism), classParallelism)),
count: (0, rxjs_1.of)(elementIds.length),
};
}
/** @internal */
function getContentItemsObservableFromClassNames(imodel, contentDescriptorGetter, contentSetGetter, elementClasses, classParallelism, batchesParallelism, batchSize) {
return {
itemBatches: getClassesWithInstances(imodel, elementClasses).pipe((0, rxjs_1.mergeMap)((classFullName) => getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, () => getBatchedClassElementIds(imodel, classFullName, batchSize), batchesParallelism), classParallelism)),
count: (0, rxjs_1.from)(getElementsCount(imodel, elementClasses)),
};
}
function getBatchedClassContentItems(classFullName, contentDescriptorGetter, contentSetGetter, batcher, batchesParallelism) {
return (0, rxjs_1.defer)(async () => {
const ruleset = createClassContentRuleset(classFullName);
const keys = new presentation_common_1.KeySet();
const descriptor = await contentDescriptorGetter({ rulesetOrId: ruleset, keys });
if (!descriptor) {
throw new presentation_common_1.PresentationError(presentation_common_1.PresentationStatus.Error, `Failed to get descriptor for class ${classFullName}`);
}
return { descriptor, keys, ruleset };
}).pipe(
// create elements' id batches
(0, rxjs_1.mergeMap)((x) => batcher().pipe((0, rxjs_1.map)((batch) => ({ ...x, batch })))),
// request content for each batch, filter by IDs for performance
(0, rxjs_1.mergeMap)(({ descriptor, keys, ruleset, batch }) => (0, rxjs_1.defer)(async () => {
const filteringDescriptor = new presentation_common_1.Descriptor(descriptor);
filteringDescriptor.instanceFilter = {
selectClassName: classFullName,
expression: createElementIdsECExpressionFilter(batch),
};
return contentSetGetter({
rulesetOrId: ruleset,
keys,
descriptor: filteringDescriptor,
});
}).pipe((0, rxjs_1.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 (0, rxjs_1.range)(0, elementIds.length / elementIdsBatchSize).pipe((0, rxjs_1.mergeMap)((batchIndex) => {
const idsFrom = batchIndex * elementIdsBatchSize;
const idsTo = Math.min(idsFrom + elementIdsBatchSize, elementIds.length);
return (0, rxjs_1.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
`));
}), (0, rxjs_1.map)((row) => ({ className: row.className, ids: row.ids.split(",") })), (0, rxjs_1.groupBy)(({ className }) => className), (0, rxjs_1.mergeMap)((groups) => groups.pipe((0, rxjs_1.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 (0, rxjs_1.from)(fullClassNames).pipe((0, rxjs_1.mergeMap)((fullClassName) => (0, rxjs_1.from)(imodel.createQueryReader(`
SELECT ec_classname(e.ECClassId, 's.c') className
FROM ${getECSqlName(fullClassName)} e
GROUP BY e.ECClassId
`))), (0, rxjs_1.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
*/
function createIdBatches(sortedIds, batchSize) {
return (0, rxjs_1.range)(0, sortedIds.length / batchSize).pipe((0, rxjs_1.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: core_bentley_1.Id64.getLocalId(sortedIds[startIndex]),
};
for (let i = startIndex + 1; i <= endIndex; ++i) {
const currLocalId = core_bentley_1.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
*/
function getBatchedClassElementIds(imodel, fullClassName, batchSize) {
return (0, rxjs_1.from)(imodel.createQueryReader(`SELECT IdToHex(ECInstanceId) id FROM ${getECSqlName(fullClassName)} ORDER BY ECInstanceId`)).pipe((0, rxjs_1.map)((row) => row.id), (0, rxjs_1.bufferCount)(batchSize), (0, rxjs_1.map)((batch) => [{ from: batch[0], to: batch[batch.length - 1] }]));
}
/** @internal */
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 presentation_common_1.PresentationError(presentation_common_1.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