Skip to content

Commit

Permalink
Presentation: Enforce result paging for hierarchy compare (#678)
Browse files Browse the repository at this point in the history
* Enforce hierarchy compare result paging

* extract-api

* rush change

* Restore broken public api

* rush extract-api

* Rename to 'continuationToken'

* Add test for hierarchy compare paging

* Update RPC version
  • Loading branch information
saskliutas authored Feb 5, 2021
1 parent 4801756 commit 64cd8f5
Show file tree
Hide file tree
Showing 21 changed files with 941 additions and 102 deletions.
5 changes: 4 additions & 1 deletion common/api/presentation-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { EventSink } from '@bentley/imodeljs-backend';
import { ExtendedContentRequestOptions } from '@bentley/presentation-common';
import { ExtendedHierarchyRequestOptions } from '@bentley/presentation-common';
import { FormatProps } from '@bentley/imodeljs-quantity';
import { HierarchyCompareInfo } from '@bentley/presentation-common';
import { HierarchyRequestOptions } from '@bentley/presentation-common';
import { Id64String } from '@bentley/bentleyjs-core';
import { IDisposable } from '@bentley/bentleyjs-core';
Expand Down Expand Up @@ -158,7 +159,7 @@ export class PresentationManager {
// @deprecated (undocumented)
compareHierarchies(requestContext: ClientRequestContext, requestOptions: PresentationDataCompareOptions<IModelDb, NodeKey>): Promise<PartialHierarchyModification[]>;
// @beta
compareHierarchies(requestOptions: WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>): Promise<PartialHierarchyModification[]>;
compareHierarchies(requestOptions: WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>): Promise<HierarchyCompareInfo>;
// @deprecated
computeSelection(requestContext: ClientRequestContext, requestOptions: SelectionScopeRequestOptions<IModelDb>, ids: Id64String[], scopeId: string): Promise<KeySet>;
// @beta
Expand Down Expand Up @@ -277,6 +278,8 @@ export interface PresentationManagerProps {
};
// @alpha
updatesPollInterval?: number;
// @alpha
useMmap?: boolean | number;
}

// @public
Expand Down
41 changes: 40 additions & 1 deletion common/api/presentation-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,34 @@ export enum GroupingSpecificationTypes {
SameLabelInstance = "SameLabelInstance"
}

// @alpha (undocumented)
export interface HierarchyCompareInfo {
// (undocumented)
changes: PartialHierarchyModification[];
// (undocumented)
continuationToken?: {
prevHierarchyNode: string;
currHierarchyNode: string;
};
}

// @alpha (undocumented)
export namespace HierarchyCompareInfo {
export function fromJSON(json: HierarchyCompareInfoJSON): HierarchyCompareInfo;
export function toJSON(obj: HierarchyCompareInfo): HierarchyCompareInfoJSON;
}

// @alpha (undocumented)
export interface HierarchyCompareInfoJSON {
// (undocumented)
changes: PartialHierarchyModificationJSON[];
// (undocumented)
continuationToken?: {
prevHierarchyNode: string;
currHierarchyNode: string;
};
}

// @public
export interface HierarchyRequestOptions<TIModel> extends RequestOptionsWithRuleset<TIModel> {
}
Expand Down Expand Up @@ -1523,13 +1551,20 @@ export const PRESENTATION_COMMON_ROOT: string;

// @alpha
export interface PresentationDataCompareOptions<TIModel, TNodeKey> extends RequestOptionsWithRuleset<TIModel> {
// (undocumented)
continuationToken?: {
prevHierarchyNode: string;
currHierarchyNode: string;
};
// (undocumented)
expandedNodeKeys?: TNodeKey[];
// (undocumented)
prev: {
rulesetOrId?: Ruleset | string;
rulesetVariables?: RulesetVariable[];
};
// (undocumented)
resultSetSize?: number;
}

// @alpha
Expand All @@ -1548,8 +1583,10 @@ export enum PresentationRpcEvents {

// @public
export class PresentationRpcInterface extends RpcInterface {
// @alpha
// @alpha @deprecated (undocumented)
compareHierarchies(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse<PartialHierarchyModificationJSON[]>;
// @alpha (undocumented)
compareHierarchiesPaged(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse<HierarchyCompareInfoJSON>;
// (undocumented)
computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse<KeySetJSON>;
// @deprecated (undocumented)
Expand Down Expand Up @@ -2071,6 +2108,8 @@ export class RpcRequestsHandler implements IDisposable {
// (undocumented)
compareHierarchies(options: PresentationDataCompareOptions<IModelRpcProps, NodeKeyJSON>): Promise<PartialHierarchyModificationJSON[]>;
// (undocumented)
compareHierarchiesPaged(options: PresentationDataCompareOptions<IModelRpcProps, NodeKeyJSON>): Promise<HierarchyCompareInfoJSON>;
// (undocumented)
computeSelection(options: SelectionScopeRequestOptions<IModelRpcProps>, ids: Id64String[], scopeId: string): Promise<KeySetJSON>;
// (undocumented)
dispose(): void;
Expand Down
3 changes: 3 additions & 0 deletions common/api/summary/presentation-common.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public;GroupingRule
public;GroupingSpecification = ClassGroup | PropertyGroup | SameLabelInstanceGroup
public;GroupingSpecificationBase
public;GroupingSpecificationTypes
alpha;HierarchyCompareInfo
alpha;HierarchyCompareInfo
alpha;HierarchyCompareInfoJSON
public;HierarchyRequestOptions
public;HierarchyRpcRequestOptions = PresentationRpcRequestOptions
alpha;HierarchyUpdateInfo = typeof UPDATE_FULL | PartialHierarchyModification[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/presentation-backend",
"comment": "Enforce max result set size for hierarchy compare.",
"type": "none"
}
],
"packageName": "@bentley/presentation-backend",
"email": "24278440+saskliutas@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/presentation-common",
"comment": "Added HierarchyCompareInfo object that describes hierarchy changes and next step from which comparison should be continued.",
"type": "none"
}
],
"packageName": "@bentley/presentation-common",
"email": "24278440+saskliutas@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/presentation-frontend",
"comment": "Changed 'compareHierarchy' to build result in pages for massive result sets. ",
"type": "none"
}
],
"packageName": "@bentley/presentation-frontend",
"email": "24278440+saskliutas@users.noreply.github.com"
}
71 changes: 71 additions & 0 deletions full-stack-tests/presentation/src/frontend/Update.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,77 @@ describe("Update", () => {

});

describe("paging", () => {

it("collects results from multiple pages", async () => {
const ruleset = await Presentation.presentation.rulesets().add({
id: faker.random.uuid(),
rules: [{
ruleType: RuleTypes.RootNodes,
specifications: [{
specType: ChildNodeSpecificationTypes.CustomNode,
type: "T_ROOT-1",
label: "root-1",
}],
}],
});
expect(ruleset).to.not.be.undefined;

const { result, unmount } = renderHook(
(props: PresentationTreeNodeLoaderProps) => usePresentationTreeNodeLoader(props),
{ initialProps: { imodel, ruleset, pagingSize: 100, enableHierarchyAutoUpdate: true } },
);
await loadHierarchy(result.current);
unmount();

const modifiedRuleset = await Presentation.presentation.rulesets().modify(ruleset, {
rules: [
{
ruleType: RuleTypes.RootNodes,
specifications: [{
specType: ChildNodeSpecificationTypes.CustomNode,
type: "T_ROOT-0",
label: "root-0",
}],
},
...ruleset.rules,
{
ruleType: RuleTypes.RootNodes,
specifications: [{
specType: ChildNodeSpecificationTypes.CustomNode,
type: "T_ROOT-2",
label: "root-2",
}],
},
],
});
expect(modifiedRuleset).to.not.be.undefined;

const rpcSpy = sinon.spy(Presentation.presentation.rpcRequestsHandler, "compareHierarchiesPaged");
const changes = await Presentation.presentation.compareHierarchies({
imodel,
prev: {
rulesetOrId: ruleset,
rulesetVariables: [],
},
rulesetOrId: modifiedRuleset,
rulesetVariables: [],
resultSetSize: 1,
});
expect(changes).to.containSubset([{
type: "Insert",
node: { key: { type: "T_ROOT-0" } },
position: 0,
}, {
type: "Insert",
node: { key: { type: "T_ROOT-2" } },
position: 2,
}]);
expect(rpcSpy).to.be.calledTwice;
});

});

});

});
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import { FormatProps } from "@bentley/imodeljs-quantity";
import {
Content, ContentDescriptorRequestOptions, ContentFlags, ContentRequestOptions, DefaultContentDisplayTypes, Descriptor, DescriptorOverrides,
DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DisplayValueGroup, DistinctValuesRequestOptions, ExtendedContentRequestOptions,
ExtendedHierarchyRequestOptions, getLocalesDirectory, HierarchyRequestOptions, InstanceKey, KeySet, LabelDefinition, LabelRequestOptions, Node,
NodeKey, NodePathElement, Paged, PagedResponse, PartialHierarchyModification, PresentationDataCompareOptions, PresentationError, PresentationStatus,
PresentationUnitSystem, RequestPriority, Ruleset, SelectionInfo, SelectionScope, SelectionScopeRequestOptions,
ExtendedHierarchyRequestOptions, getLocalesDirectory, HierarchyCompareInfo, HierarchyRequestOptions, InstanceKey, KeySet, LabelDefinition,
LabelRequestOptions, Node, NodeKey, NodePathElement, Paged, PagedResponse, PartialHierarchyModification, PresentationDataCompareOptions,
PresentationError, PresentationStatus, PresentationUnitSystem, RequestPriority, Ruleset, SelectionInfo, SelectionScope,
SelectionScopeRequestOptions,
} from "@bentley/presentation-common";
import { PresentationBackendLoggerCategory } from "./BackendLoggerCategory";
import { PRESENTATION_BACKEND_ASSETS_ROOT, PRESENTATION_COMMON_ASSETS_ROOT } from "./Constants";
Expand Down Expand Up @@ -121,7 +122,7 @@ export interface HybridCacheConfig extends HierarchyCacheConfigBase {
export interface UnitSystemFormat {
unitSystems: PresentationUnitSystem[];
format: FormatProps;
};
}

/**
* Properties that can be used to configure [[PresentationManager]]
Expand Down Expand Up @@ -885,14 +886,14 @@ export class PresentationManager {
* TODO: Return results in pages
* @beta
*/
public async compareHierarchies(requestOptions: WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>): Promise<PartialHierarchyModification[]>;
public async compareHierarchies(requestContextOrOptions: ClientRequestContext | WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>, deprecatedRequestOptions?: PresentationDataCompareOptions<IModelDb, NodeKey>): Promise<PartialHierarchyModification[]> {
public async compareHierarchies(requestOptions: WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>): Promise<HierarchyCompareInfo>;
public async compareHierarchies(requestContextOrOptions: ClientRequestContext | WithClientRequestContext<PresentationDataCompareOptions<IModelDb, NodeKey>>, deprecatedRequestOptions?: PresentationDataCompareOptions<IModelDb, NodeKey>): Promise<HierarchyCompareInfo | PartialHierarchyModification[]> {
if (requestContextOrOptions instanceof ClientRequestContext) {
return this.compareHierarchies({ ...deprecatedRequestOptions!, requestContext: requestContextOrOptions });
return (await this.compareHierarchies({ ...deprecatedRequestOptions!, requestContext: requestContextOrOptions })).changes;
}

if (!requestContextOrOptions.prev.rulesetOrId && !requestContextOrOptions.prev.rulesetVariables)
return [];
return { changes: [] };

const { strippedOptions: { prev, rulesetVariables, ...options } } = this.registerRuleset(requestContextOrOptions);

Expand All @@ -913,7 +914,7 @@ export class PresentationManager {
currRulesetVariables: JSON.stringify(currRulesetVariables),
expandedNodeKeys: JSON.stringify(options.expandedNodeKeys ?? []),
};
return this.request(params, (key: string, value: any) => ((key === "") ? value.map(PartialHierarchyModification.fromJSON) : value));
return this.request(params, (key: string, value: any) => ((key === "") ? HierarchyCompareInfo.fromJSON(value) : value));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { IModelRpcProps } from "@bentley/imodeljs-common";
import {
ContentDescriptorRpcRequestOptions, ContentJSON, ContentRpcRequestOptions, Descriptor, DescriptorJSON, DescriptorOverrides, DiagnosticsOptions,
DiagnosticsScopeLogs, DisplayLabelRpcRequestOptions, DisplayLabelsRpcRequestOptions, DisplayValueGroup, DisplayValueGroupJSON,
DistinctValuesRpcRequestOptions, ExtendedContentRpcRequestOptions, ExtendedHierarchyRpcRequestOptions, HierarchyRpcRequestOptions, InstanceKey,
InstanceKeyJSON, isContentDescriptorRequestOptions, isDisplayLabelRequestOptions, isExtendedContentRequestOptions,
isExtendedHierarchyRequestOptions, ItemJSON, KeySet, KeySetJSON, LabelDefinition, LabelDefinitionJSON, LabelRpcRequestOptions, Node, NodeJSON,
NodeKey, NodeKeyJSON, NodePathElement, NodePathElementJSON, Paged, PagedResponse, PageOptions, PartialHierarchyModification,
PartialHierarchyModificationJSON, PresentationDataCompareRpcOptions, PresentationError, PresentationRpcInterface, PresentationRpcResponse,
PresentationStatus, Ruleset, SelectionInfo, SelectionScope, SelectionScopeRpcRequestOptions,
DistinctValuesRpcRequestOptions, ExtendedContentRpcRequestOptions, ExtendedHierarchyRpcRequestOptions, HierarchyCompareInfo,
HierarchyCompareInfoJSON, HierarchyRpcRequestOptions, InstanceKey, InstanceKeyJSON, isContentDescriptorRequestOptions, isDisplayLabelRequestOptions,
isExtendedContentRequestOptions, isExtendedHierarchyRequestOptions, ItemJSON, KeySet, KeySetJSON, LabelDefinition, LabelDefinitionJSON,
LabelRpcRequestOptions, Node, NodeJSON, NodeKey, NodeKeyJSON, NodePathElement, NodePathElementJSON, Paged, PagedResponse, PageOptions,
PartialHierarchyModification, PartialHierarchyModificationJSON, PresentationDataCompareRpcOptions, PresentationError, PresentationRpcInterface,
PresentationRpcResponse, PresentationStatus, Ruleset, SelectionInfo, SelectionScope, SelectionScopeRpcRequestOptions,
} from "@bentley/presentation-common";
import { PresentationBackendLoggerCategory } from "./BackendLoggerCategory";
import { Presentation } from "./Presentation";
Expand Down Expand Up @@ -364,18 +364,35 @@ export class PresentationRpcImpl extends PresentationRpcInterface {
...(options.expandedNodeKeys ? { expandedNodeKeys: options.expandedNodeKeys.map(NodeKey.fromJSON) } : undefined),
};
const result = await this.getManager(requestOptions.clientId).compareHierarchies(options);
return result.map(PartialHierarchyModification.toJSON);
return result.changes.map(PartialHierarchyModification.toJSON);
});
}

public async compareHierarchiesPaged(token: IModelRpcProps, requestOptions: PresentationDataCompareRpcOptions): PresentationRpcResponse<HierarchyCompareInfoJSON> {
return this.makeRequest(token, "compareHierarchies", requestOptions, async (options) => {
options = {
...options,
...(options.expandedNodeKeys ? { expandedNodeKeys: options.expandedNodeKeys.map(NodeKey.fromJSON) } : undefined),
resultSetSize: getValidPageSize(requestOptions.resultSetSize),
};
const result = await this.getManager(requestOptions.clientId).compareHierarchies(options);
return HierarchyCompareInfo.toJSON(result);
});
}
}

const enforceValidPageSize = <TOptions extends Paged<object>>(requestOptions: TOptions): TOptions & { paging: PageOptions } => {
const requestedPageSize = requestOptions.paging?.size ?? 0;
if (requestedPageSize === 0 || requestedPageSize > MAX_ALLOWED_PAGE_SIZE)
return { ...requestOptions, paging: { ...requestOptions.paging, size: MAX_ALLOWED_PAGE_SIZE } };
const validPageSize = getValidPageSize(requestOptions.paging?.size);
if (!requestOptions.paging || requestOptions.paging.size !== validPageSize)
return { ...requestOptions, paging: { ...requestOptions.paging, size: validPageSize } };
return requestOptions as (TOptions & { paging: PageOptions });
};

const getValidPageSize = (size: number | undefined) => {
const requestedSize = size ?? 0;
return (requestedSize === 0 || requestedSize > MAX_ALLOWED_PAGE_SIZE) ? MAX_ALLOWED_PAGE_SIZE : requestedSize;
};

const nodeKeyFromJson = (json: NodeKeyJSON | undefined): NodeKey | undefined => {
if (!json)
return undefined;
Expand Down
Loading

0 comments on commit 64cd8f5

Please sign in to comment.