Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presentation: Enforce result paging for hierarchy compare #678

Merged
merged 9 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion common/api/presentation-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DistinctValuesRequestOptions } from '@bentley/presentation-common';
import { EventSink } from '@bentley/imodeljs-backend';
import { ExtendedContentRequestOptions } from '@bentley/presentation-common';
import { ExtendedHierarchyRequestOptions } from '@bentley/presentation-common';
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 @@ -157,7 +158,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
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 @@ -13,9 +13,10 @@ import { BriefcaseDb, EventSink, IModelDb, IModelHost, IModelJsNative } from "@b
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 @@ -854,14 +855,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 @@ -882,7 +883,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