diff --git a/common/api/presentation-backend.api.md b/common/api/presentation-backend.api.md index ff9b64071c93..cfc300c51cde 100644 --- a/common/api/presentation-backend.api.md +++ b/common/api/presentation-backend.api.md @@ -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'; @@ -158,7 +159,7 @@ export class PresentationManager { // @deprecated (undocumented) compareHierarchies(requestContext: ClientRequestContext, requestOptions: PresentationDataCompareOptions): Promise; // @beta - compareHierarchies(requestOptions: WithClientRequestContext>): Promise; + compareHierarchies(requestOptions: WithClientRequestContext>): Promise; // @deprecated computeSelection(requestContext: ClientRequestContext, requestOptions: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise; // @beta @@ -277,6 +278,8 @@ export interface PresentationManagerProps { }; // @alpha updatesPollInterval?: number; + // @alpha + useMmap?: boolean | number; } // @public diff --git a/common/api/presentation-common.api.md b/common/api/presentation-common.api.md index 26fb0239b848..7d7cf3178617 100644 --- a/common/api/presentation-common.api.md +++ b/common/api/presentation-common.api.md @@ -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 extends RequestOptionsWithRuleset { } @@ -1523,6 +1551,11 @@ export const PRESENTATION_COMMON_ROOT: string; // @alpha export interface PresentationDataCompareOptions extends RequestOptionsWithRuleset { + // (undocumented) + continuationToken?: { + prevHierarchyNode: string; + currHierarchyNode: string; + }; // (undocumented) expandedNodeKeys?: TNodeKey[]; // (undocumented) @@ -1530,6 +1563,8 @@ export interface PresentationDataCompareOptions extends Reque rulesetOrId?: Ruleset | string; rulesetVariables?: RulesetVariable[]; }; + // (undocumented) + resultSetSize?: number; } // @alpha @@ -1548,8 +1583,10 @@ export enum PresentationRpcEvents { // @public export class PresentationRpcInterface extends RpcInterface { - // @alpha + // @alpha @deprecated (undocumented) compareHierarchies(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse; + // @alpha (undocumented) + compareHierarchiesPaged(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse; // (undocumented) computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse; // @deprecated (undocumented) @@ -2071,6 +2108,8 @@ export class RpcRequestsHandler implements IDisposable { // (undocumented) compareHierarchies(options: PresentationDataCompareOptions): Promise; // (undocumented) + compareHierarchiesPaged(options: PresentationDataCompareOptions): Promise; + // (undocumented) computeSelection(options: SelectionScopeRequestOptions, ids: Id64String[], scopeId: string): Promise; // (undocumented) dispose(): void; diff --git a/common/api/summary/presentation-common.exports.csv b/common/api/summary/presentation-common.exports.csv index e5cc1e914595..a1d4d171acba 100644 --- a/common/api/summary/presentation-common.exports.csv +++ b/common/api/summary/presentation-common.exports.csv @@ -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[] diff --git a/common/changes/@bentley/presentation-backend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json b/common/changes/@bentley/presentation-backend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json new file mode 100644 index 000000000000..9d6c8d964710 --- /dev/null +++ b/common/changes/@bentley/presentation-backend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json @@ -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" +} \ No newline at end of file diff --git a/common/changes/@bentley/presentation-common/presentation-hierarchy_compare_paging_2021-01-29-14-50.json b/common/changes/@bentley/presentation-common/presentation-hierarchy_compare_paging_2021-01-29-14-50.json new file mode 100644 index 000000000000..402d49345a8e --- /dev/null +++ b/common/changes/@bentley/presentation-common/presentation-hierarchy_compare_paging_2021-01-29-14-50.json @@ -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" +} \ No newline at end of file diff --git a/common/changes/@bentley/presentation-frontend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json b/common/changes/@bentley/presentation-frontend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json new file mode 100644 index 000000000000..f58ed0ff2b50 --- /dev/null +++ b/common/changes/@bentley/presentation-frontend/presentation-hierarchy_compare_paging_2021-01-29-14-50.json @@ -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" +} \ No newline at end of file diff --git a/full-stack-tests/presentation/src/frontend/Update.test.tsx b/full-stack-tests/presentation/src/frontend/Update.test.tsx index 29436e680c8f..36e68e57a453 100644 --- a/full-stack-tests/presentation/src/frontend/Update.test.tsx +++ b/full-stack-tests/presentation/src/frontend/Update.test.tsx @@ -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; + }); + + }); + }); }); diff --git a/presentation/backend/src/presentation-backend/PresentationManager.ts b/presentation/backend/src/presentation-backend/PresentationManager.ts index 9b347da19954..54ac7293139c 100644 --- a/presentation/backend/src/presentation-backend/PresentationManager.ts +++ b/presentation/backend/src/presentation-backend/PresentationManager.ts @@ -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"; @@ -121,7 +122,7 @@ export interface HybridCacheConfig extends HierarchyCacheConfigBase { export interface UnitSystemFormat { unitSystems: PresentationUnitSystem[]; format: FormatProps; -}; +} /** * Properties that can be used to configure [[PresentationManager]] @@ -885,14 +886,14 @@ export class PresentationManager { * TODO: Return results in pages * @beta */ - public async compareHierarchies(requestOptions: WithClientRequestContext>): Promise; - public async compareHierarchies(requestContextOrOptions: ClientRequestContext | WithClientRequestContext>, deprecatedRequestOptions?: PresentationDataCompareOptions): Promise { + public async compareHierarchies(requestOptions: WithClientRequestContext>): Promise; + public async compareHierarchies(requestContextOrOptions: ClientRequestContext | WithClientRequestContext>, deprecatedRequestOptions?: PresentationDataCompareOptions): Promise { 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); @@ -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)); } } diff --git a/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts b/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts index 94829ba2d077..2add67b4167b 100644 --- a/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts +++ b/presentation/backend/src/presentation-backend/PresentationRpcImpl.ts @@ -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"; @@ -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 { + 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 = >(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; diff --git a/presentation/backend/src/test/PresentationManager.test.ts b/presentation/backend/src/test/PresentationManager.test.ts index 2dbddfed77dc..0d9799d2081c 100644 --- a/presentation/backend/src/test/PresentationManager.test.ts +++ b/presentation/backend/src/test/PresentationManager.test.ts @@ -15,8 +15,8 @@ import { ArrayTypeDescription, ContentDescriptorRequestOptions, ContentFlags, ContentJSON, ContentRequestOptions, DefaultContentDisplayTypes, Descriptor, DescriptorJSON, DiagnosticsOptions, DiagnosticsScopeLogs, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, DistinctValuesRequestOptions, ExtendedContentRequestOptions, ExtendedHierarchyRequestOptions, FieldDescriptor, FieldDescriptorType, FieldJSON, getLocalesDirectory, - HierarchyRequestOptions, InstanceKey, ItemJSON, KeySet, KindOfQuantityInfo, LabelDefinition, LabelRequestOptions, NestedContentFieldJSON, NodeJSON, - NodeKey, Paged, PageOptions, PartialHierarchyModification, PartialHierarchyModificationJSON, PresentationDataCompareOptions, PresentationError, + HierarchyCompareInfo, HierarchyCompareInfoJSON, HierarchyRequestOptions, InstanceKey, ItemJSON, KeySet, KindOfQuantityInfo, LabelDefinition, + LabelRequestOptions, NestedContentFieldJSON, NodeJSON, NodeKey, Paged, PageOptions, PresentationDataCompareOptions, PresentationError, PresentationUnitSystem, PrimitiveTypeDescription, PropertiesFieldJSON, PropertyInfoJSON, PropertyJSON, RegisteredRuleset, RequestPriority, Ruleset, SelectClassInfoJSON, SelectionInfo, SelectionScope, StandardNodeTypes, StructTypeDescription, VariableValueTypes, } from "@bentley/presentation-common"; @@ -94,6 +94,7 @@ describe("PresentationManager", () => { cacheConfig: { mode: HierarchyCacheMode.Disk, directory: "" }, contentCacheSize: undefined, defaultFormats: {}, + useMmap: undefined, }); }); }); @@ -131,6 +132,7 @@ describe("PresentationManager", () => { cacheConfig, contentCacheSize: 999, defaultFormats, + useMmap: 666, }; const expectedCacheConfig = { mode: HierarchyCacheMode.Memory, @@ -146,6 +148,7 @@ describe("PresentationManager", () => { cacheConfig: expectedCacheConfig, contentCacheSize: 999, defaultFormats: { length: { unitSystems: [PresentationUnitSystem.BritishImperial], serializedFormat: JSON.stringify(formatProps) } }, + useMmap: 666, }); }); }); @@ -163,6 +166,7 @@ describe("PresentationManager", () => { cacheConfig: { mode: HierarchyCacheMode.Disk, directory: "" }, contentCacheSize: undefined, defaultFormats: {}, + useMmap: undefined, }); }); constructorSpy.resetHistory(); @@ -182,6 +186,7 @@ describe("PresentationManager", () => { cacheConfig: expectedConfig, contentCacheSize: undefined, defaultFormats: {}, + useMmap: undefined, }); }); }); @@ -199,6 +204,7 @@ describe("PresentationManager", () => { cacheConfig: { mode: HierarchyCacheMode.Hybrid, disk: undefined }, contentCacheSize: undefined, defaultFormats: {}, + useMmap: undefined, }); }); constructorSpy.resetHistory(); @@ -223,6 +229,7 @@ describe("PresentationManager", () => { cacheConfig: expectedConfig, contentCacheSize: undefined, defaultFormats: {}, + useMmap: undefined, }); }); }); @@ -1153,11 +1160,13 @@ describe("PresentationManager", () => { }; // what the addon returns - const addonResponse: PartialHierarchyModificationJSON[] = setup([{ - type: "Insert", - position: 1, - node: createRandomECInstancesNodeJSON(), - }]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [{ + type: "Insert", + position: 1, + node: createRandomECInstancesNodeJSON(), + }], + }); // test const options: PresentationDataCompareOptions = { @@ -1171,7 +1180,7 @@ describe("PresentationManager", () => { expandedNodeKeys: [nodeKey], }; const result = await manager.compareHierarchies(ClientRequestContext.current, options); - verifyWithExpectedResult(result, addonResponse.map(PartialHierarchyModification.fromJSON), expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse).changes, expectedParams); }); it("requests addon to compare hierarchies based on ruleset and variables' changes", async () => { @@ -1192,11 +1201,13 @@ describe("PresentationManager", () => { }; // what the addon returns - const addonResponse: PartialHierarchyModificationJSON[] = setup([{ - type: "Insert", - position: 1, - node: createRandomECInstancesNodeJSON(), - }]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [{ + type: "Insert", + position: 1, + node: createRandomECInstancesNodeJSON(), + }], + }); // test const options: WithClientRequestContext> = { @@ -1211,7 +1222,7 @@ describe("PresentationManager", () => { expandedNodeKeys: [nodeKey], }; const result = await manager.compareHierarchies(options); - verifyWithExpectedResult(result, addonResponse.map(PartialHierarchyModification.fromJSON), expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse), expectedParams); }); it("requests addon to compare hierarchies based on ruleset changes", async () => { @@ -1228,10 +1239,12 @@ describe("PresentationManager", () => { }; // what the addon returns - const addonResponse: PartialHierarchyModificationJSON[] = setup([{ - type: "Delete", - node: createRandomECInstancesNodeJSON(), - }]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [{ + type: "Delete", + node: createRandomECInstancesNodeJSON(), + }], + }); // test const options: WithClientRequestContext> = { @@ -1243,7 +1256,7 @@ describe("PresentationManager", () => { rulesetOrId: "test", }; const result = await manager.compareHierarchies(options); - verifyWithExpectedResult(result, addonResponse.map(PartialHierarchyModification.fromJSON), expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse), expectedParams); }); it("requests addon to compare hierarchies based on ruleset variables' changes", async () => { @@ -1263,11 +1276,13 @@ describe("PresentationManager", () => { }; // what the addon returns - const addonResponse: PartialHierarchyModificationJSON[] = setup([{ - type: "Update", - node: createRandomECInstancesNodeJSON(), - changes: [], - }]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [{ + type: "Update", + node: createRandomECInstancesNodeJSON(), + changes: [], + }], + }); // test const options: WithClientRequestContext> = { @@ -1280,7 +1295,7 @@ describe("PresentationManager", () => { rulesetVariables: [var2], }; const result = await manager.compareHierarchies(options); - verifyWithExpectedResult(result, addonResponse.map(PartialHierarchyModification.fromJSON), expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse), expectedParams); }); it("returns empty result if neither ruleset nor ruleset variables changed", async () => { @@ -1292,7 +1307,7 @@ describe("PresentationManager", () => { rulesetOrId: "test", }); nativePlatformMock.verify(async (x) => x.handleRequest(moq.It.isAny(), moq.It.isAny()), moq.Times.never()); - expect(result).to.deep.eq([]); + expect(result).to.deep.eq({ changes: [] }); }); it("throws when trying to compare hierarchies with different ruleset ids", async () => { @@ -1327,7 +1342,9 @@ describe("PresentationManager", () => { }; // what the addon returns - setup([]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [], + }); // test const options: WithClientRequestContext> = { @@ -1340,7 +1357,7 @@ describe("PresentationManager", () => { expandedNodeKeys: [], }; const result = await manager.compareHierarchies(options); - verifyWithExpectedResult(result, [], expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse), expectedParams); }); it("uses `locale` from options for comparison", async () => { @@ -1360,7 +1377,9 @@ describe("PresentationManager", () => { }; // what the addon returns - setup([]); + const addonResponse: HierarchyCompareInfoJSON = setup({ + changes: [], + }); // test const options: WithClientRequestContext> = { @@ -1374,7 +1393,7 @@ describe("PresentationManager", () => { expandedNodeKeys: [], }; const result = await manager.compareHierarchies(options); - verifyWithExpectedResult(result, [], expectedParams); + verifyWithExpectedResult(result, HierarchyCompareInfo.fromJSON(addonResponse), expectedParams); }); }); diff --git a/presentation/backend/src/test/PresentationRpcImpl.test.ts b/presentation/backend/src/test/PresentationRpcImpl.test.ts index 968ef711617b..590f6dabf8f8 100644 --- a/presentation/backend/src/test/PresentationRpcImpl.test.ts +++ b/presentation/backend/src/test/PresentationRpcImpl.test.ts @@ -12,10 +12,10 @@ import { ContentDescriptorRequestOptions, ContentDescriptorRpcRequestOptions, ContentRequestOptions, ContentRpcRequestOptions, Descriptor, DescriptorJSON, DescriptorOverrides, DiagnosticsScopeLogs, DisplayLabelRequestOptions, DisplayLabelRpcRequestOptions, DisplayLabelsRequestOptions, DisplayLabelsRpcRequestOptions, DistinctValuesRequestOptions, ExtendedContentRequestOptions, ExtendedContentRpcRequestOptions, - ExtendedHierarchyRequestOptions, ExtendedHierarchyRpcRequestOptions, FieldDescriptor, FieldDescriptorType, HierarchyRequestOptions, - HierarchyRpcRequestOptions, HierarchyUpdateInfo, InstanceKey, Item, KeySet, KeySetJSON, Node, NodeKey, NodePathElement, Paged, PageOptions, - PartialHierarchyModification, PresentationDataCompareOptions, PresentationDataCompareRpcOptions, PresentationError, PresentationRpcRequestOptions, - PresentationStatus, SelectionScopeRequestOptions, VariableValueTypes, + ExtendedHierarchyRequestOptions, ExtendedHierarchyRpcRequestOptions, FieldDescriptor, FieldDescriptorType, HierarchyCompareInfo, + HierarchyRequestOptions, HierarchyRpcRequestOptions, InstanceKey, Item, KeySet, KeySetJSON, Node, NodeKey, NodePathElement, Paged, PageOptions, + PresentationDataCompareOptions, PresentationDataCompareRpcOptions, PresentationError, PresentationRpcRequestOptions, PresentationStatus, + SelectionScopeRequestOptions, VariableValueTypes, } from "@bentley/presentation-common"; import * as moq from "@bentley/presentation-common/lib/test/_helpers/Mocks"; import { ResolvablePromise } from "@bentley/presentation-common/lib/test/_helpers/Promises"; @@ -1631,13 +1631,15 @@ describe("PresentationRpcImpl", () => { }); - describe("compareHierarchies", () => { + describe("[deprecated] compareHierarchies", () => { it("calls manager for comparison based on ruleset changes", async () => { - const result: PartialHierarchyModification[] = [{ - type: "Delete", - node: createRandomECInstancesNode(), - }]; + const result: HierarchyCompareInfo = { + changes: [{ + type: "Delete", + node: createRandomECInstancesNode(), + }], + }; const rpcOptions: PresentationDataCompareRpcOptions = { ...defaultRpcParams, prev: { @@ -1656,14 +1658,16 @@ describe("PresentationRpcImpl", () => { .verifiable(); const actualResult = await impl.compareHierarchies(testData.imodelToken, rpcOptions); presentationManagerMock.verifyAll(); - expect(actualResult.result).to.deep.eq(HierarchyUpdateInfo.toJSON(result)); + expect(actualResult.result).to.deep.eq(HierarchyCompareInfo.toJSON(result).changes); }); it("calls manager for comparison based on ruleset variables' changes", async () => { - const result: PartialHierarchyModification[] = [{ - type: "Delete", - node: createRandomECInstancesNode(), - }]; + const result: HierarchyCompareInfo = { + changes: [{ + type: "Delete", + node: createRandomECInstancesNode(), + }], + }; const rpcOptions: PresentationDataCompareRpcOptions = { ...defaultRpcParams, prev: { @@ -1684,7 +1688,104 @@ describe("PresentationRpcImpl", () => { .verifiable(); const actualResult = await impl.compareHierarchies(testData.imodelToken, rpcOptions); presentationManagerMock.verifyAll(); - expect(actualResult.result).to.deep.eq(HierarchyUpdateInfo.toJSON(result)); + expect(actualResult.result).to.deep.eq(HierarchyCompareInfo.toJSON(result).changes); + }); + + }); + + describe("compareHierarchiesPaged", () => { + + it("calls manager for comparison based on ruleset changes", async () => { + const result: HierarchyCompareInfo = { + changes: [{ + type: "Delete", + node: createRandomECInstancesNode(), + }], + }; + const rpcOptions: PresentationDataCompareRpcOptions = { + ...defaultRpcParams, + prev: { + rulesetOrId: "1", + }, + rulesetOrId: "2", + resultSetSize: 10, + }; + const managerOptions: WithClientRequestContext> = { + requestContext: ClientRequestContext.current, + imodel: testData.imodelMock.object, + prev: rpcOptions.prev, + rulesetOrId: rpcOptions.rulesetOrId, + resultSetSize: 10, + }; + presentationManagerMock.setup((x) => x.compareHierarchies(managerOptions)) + .returns(async () => result) + .verifiable(); + const actualResult = await impl.compareHierarchiesPaged(testData.imodelToken, rpcOptions); + presentationManagerMock.verifyAll(); + expect(actualResult.result).to.deep.eq(HierarchyCompareInfo.toJSON(result)); + }); + + it("calls manager for comparison based on ruleset variables' changes", async () => { + const result: HierarchyCompareInfo = { + changes: [{ + type: "Delete", + node: createRandomECInstancesNode(), + }], + }; + const rpcOptions: PresentationDataCompareRpcOptions = { + ...defaultRpcParams, + prev: { + rulesetVariables: [{ id: "test", type: VariableValueTypes.Int, value: 123 }], + }, + rulesetOrId: "2", + expandedNodeKeys: [createRandomECInstancesNodeKeyJSON()], + resultSetSize: 10, + }; + const managerOptions: WithClientRequestContext> = { + requestContext: ClientRequestContext.current, + imodel: testData.imodelMock.object, + prev: rpcOptions.prev, + rulesetOrId: rpcOptions.rulesetOrId, + expandedNodeKeys: rpcOptions.expandedNodeKeys!.map(NodeKey.fromJSON), + resultSetSize: 10, + }; + presentationManagerMock.setup((x) => x.compareHierarchies(managerOptions)) + .returns(async () => result) + .verifiable(); + const actualResult = await impl.compareHierarchiesPaged(testData.imodelToken, rpcOptions); + presentationManagerMock.verifyAll(); + expect(actualResult.result).to.deep.eq(HierarchyCompareInfo.toJSON(result)); + }); + + it("enforces maximum result set size", async () => { + const result: HierarchyCompareInfo = { + changes: [{ + type: "Delete", + node: createRandomECInstancesNode(), + }], + }; + const rpcOptions: PresentationDataCompareRpcOptions = { + ...defaultRpcParams, + prev: { + rulesetVariables: [{ id: "test", type: VariableValueTypes.Int, value: 123 }], + }, + rulesetOrId: "2", + expandedNodeKeys: [createRandomECInstancesNodeKeyJSON()], + }; + const managerOptions: WithClientRequestContext> = { + requestContext: ClientRequestContext.current, + imodel: testData.imodelMock.object, + prev: rpcOptions.prev, + rulesetOrId: rpcOptions.rulesetOrId, + expandedNodeKeys: rpcOptions.expandedNodeKeys!.map(NodeKey.fromJSON), + resultSetSize: MAX_ALLOWED_PAGE_SIZE, + }; + presentationManagerMock.setup((x) => x.compareHierarchies(managerOptions)) + .returns(async () => result) + .verifiable(); + const actualResult = await impl.compareHierarchiesPaged(testData.imodelToken, rpcOptions); + presentationManagerMock.verifyAll(); + expect(actualResult.result).to.deep.eq(HierarchyCompareInfo.toJSON(result)); }); }); diff --git a/presentation/common/src/presentation-common/PresentationManagerOptions.ts b/presentation/common/src/presentation-common/PresentationManagerOptions.ts index f041c159a50f..02314177f1dd 100644 --- a/presentation/common/src/presentation-common/PresentationManagerOptions.ts +++ b/presentation/common/src/presentation-common/PresentationManagerOptions.ts @@ -197,6 +197,11 @@ export interface PresentationDataCompareOptions extends Reque rulesetVariables?: RulesetVariable[]; }; expandedNodeKeys?: TNodeKey[]; + continuationToken?: { + prevHierarchyNode: string; + currHierarchyNode: string; + }; + resultSetSize?: number; } /** diff --git a/presentation/common/src/presentation-common/PresentationRpcInterface.ts b/presentation/common/src/presentation-common/PresentationRpcInterface.ts index f20bd24c99d9..4b43a59256db 100644 --- a/presentation/common/src/presentation-common/PresentationRpcInterface.ts +++ b/presentation/common/src/presentation-common/PresentationRpcInterface.ts @@ -26,7 +26,7 @@ import { SelectionScopeRequestOptions, } from "./PresentationManagerOptions"; import { SelectionScope } from "./selection/SelectionScope"; -import { PartialHierarchyModificationJSON } from "./Update"; +import { HierarchyCompareInfoJSON, PartialHierarchyModificationJSON } from "./Update"; import { Omit, PagedResponse } from "./Utils"; /** @@ -137,7 +137,7 @@ export class PresentationRpcInterface extends RpcInterface { public static readonly interfaceName = "PresentationRpcInterface"; // eslint-disable-line @typescript-eslint/naming-convention /** The semantic version of the interface. */ - public static interfaceVersion = "2.6.3"; + public static interfaceVersion = "2.7.0"; /*=========================================================================================== NOTE: Any add/remove/change to the methods below requires an update of the interface version. @@ -205,8 +205,11 @@ export class PresentationRpcInterface extends RpcInterface { // TODO: need to enforce paging on this public async computeSelection(_token: IModelRpcProps, _options: SelectionScopeRpcRequestOptions, _ids: Id64String[], _scopeId: string): PresentationRpcResponse { return this.forward(arguments); } - /** @alpha TODO: need to page results of this */ + /** @alpha @deprecated Use [[compareHierarchiesPaged]] */ public async compareHierarchies(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse { return this.forward(arguments); } + + /** @alpha */ + public async compareHierarchiesPaged(_token: IModelRpcProps, _options: PresentationDataCompareRpcOptions): PresentationRpcResponse { return this.forward(arguments); } } /** @alpha */ diff --git a/presentation/common/src/presentation-common/RpcRequestsHandler.ts b/presentation/common/src/presentation-common/RpcRequestsHandler.ts index ba3b3329e6c6..a931dbcc8441 100644 --- a/presentation/common/src/presentation-common/RpcRequestsHandler.ts +++ b/presentation/common/src/presentation-common/RpcRequestsHandler.ts @@ -25,7 +25,7 @@ import { } from "./PresentationManagerOptions"; import { PresentationRpcInterface, PresentationRpcRequestOptions, PresentationRpcResponse } from "./PresentationRpcInterface"; import { SelectionScope } from "./selection/SelectionScope"; -import { PartialHierarchyModificationJSON } from "./Update"; +import { HierarchyCompareInfoJSON, PartialHierarchyModificationJSON } from "./Update"; import { Omit, PagedResponse } from "./Utils"; /** @@ -186,6 +186,10 @@ export class RpcRequestsHandler implements IDisposable { } public async compareHierarchies(options: PresentationDataCompareOptions): Promise { return this.request>( - this.rpcClient.compareHierarchies.bind(this.rpcClient), options); + this.rpcClient.compareHierarchies.bind(this.rpcClient), options); // eslint-disable-line deprecation/deprecation + } + public async compareHierarchiesPaged(options: PresentationDataCompareOptions): Promise { + return this.request>( + this.rpcClient.compareHierarchiesPaged.bind(this.rpcClient), options); } } diff --git a/presentation/common/src/presentation-common/Update.ts b/presentation/common/src/presentation-common/Update.ts index 4dc23094e8aa..46caf088a5f1 100644 --- a/presentation/common/src/presentation-common/Update.ts +++ b/presentation/common/src/presentation-common/Update.ts @@ -180,3 +180,40 @@ export interface NodeUpdateInfoJSON { new: unknown; }>; } + +/** @alpha */ +export interface HierarchyCompareInfoJSON { + changes: PartialHierarchyModificationJSON[]; + continuationToken?: { + prevHierarchyNode: string; + currHierarchyNode: string; + }; +} + +/** @alpha */ +export interface HierarchyCompareInfo { + changes: PartialHierarchyModification[]; + continuationToken?: { + prevHierarchyNode: string; + currHierarchyNode: string; + }; +} + +/** @alpha */ +export namespace HierarchyCompareInfo { + /** Serialize given object to JSON. */ + export function toJSON(obj: HierarchyCompareInfo): HierarchyCompareInfoJSON { + return { + ...obj, + changes: obj.changes.map((change) => PartialHierarchyModification.toJSON(change)), + }; + } + + /** Deserialize given object from JSON */ + export function fromJSON(json: HierarchyCompareInfoJSON): HierarchyCompareInfo { + return { + ...json, + changes: json.changes.map((change) => PartialHierarchyModification.fromJSON(change)), + }; + } +} diff --git a/presentation/common/src/test/PresentationRpcInterface.test.ts b/presentation/common/src/test/PresentationRpcInterface.test.ts index b1ed11255774..09c327f6ee7c 100644 --- a/presentation/common/src/test/PresentationRpcInterface.test.ts +++ b/presentation/common/src/test/PresentationRpcInterface.test.ts @@ -306,7 +306,7 @@ describe("PresentationRpcInterface", () => { expect(spy).to.be.calledOnceWith(toArguments(token, options, ids, scopeId)); }); - it("forwards compareHierarchies call", async () => { + it("[deprecated] forwards compareHierarchies call", async () => { const options: PresentationDataCompareRpcOptions = { prev: { rulesetOrId: "test1", @@ -314,7 +314,20 @@ describe("PresentationRpcInterface", () => { rulesetOrId: "test2", expandedNodeKeys: [], }; - await rpcInterface.compareHierarchies(token, options); + await rpcInterface.compareHierarchies(token, options); // eslint-disable-line deprecation/deprecation + expect(spy).to.be.calledOnceWith(toArguments(token, options)); + }); + + it("forwards compareHierarchiesPaged call", async () => { + const options: PresentationDataCompareRpcOptions = { + prev: { + rulesetOrId: "test1", + }, + rulesetOrId: "test2", + expandedNodeKeys: [], + resultSetSize: 10, + }; + await rpcInterface.compareHierarchiesPaged(token, options); expect(spy).to.be.calledOnceWith(toArguments(token, options)); }); diff --git a/presentation/common/src/test/RpcRequestsHandler.test.ts b/presentation/common/src/test/RpcRequestsHandler.test.ts index 97427dc59fc6..78b8502b3ace 100644 --- a/presentation/common/src/test/RpcRequestsHandler.test.ts +++ b/presentation/common/src/test/RpcRequestsHandler.test.ts @@ -9,9 +9,9 @@ import * as moq from "typemoq"; import { Id64String } from "@bentley/bentleyjs-core"; import { IModelRpcProps, RpcInterface, RpcInterfaceDefinition, RpcManager } from "@bentley/imodeljs-common"; import { - ContentRequestOptions, DescriptorJSON, DistinctValuesRpcRequestOptions, HierarchyRequestOptions, KeySet, KeySetJSON, Paged, - PartialHierarchyModificationJSON, PresentationError, PresentationRpcInterface, PresentationRpcRequestOptions, PresentationRpcResponse, - PresentationStatus, RpcRequestsHandler, SelectionInfo, SelectionScopeRequestOptions, + ContentRequestOptions, DescriptorJSON, DistinctValuesRpcRequestOptions, HierarchyRequestOptions, KeySet, KeySetJSON, Paged, PresentationError, + PresentationRpcInterface, PresentationRpcRequestOptions, PresentationRpcResponse, PresentationStatus, RpcRequestsHandler, SelectionInfo, + SelectionScopeRequestOptions, } from "../presentation-common"; import { FieldDescriptorType } from "../presentation-common/content/Fields"; import { ItemJSON } from "../presentation-common/content/Item"; @@ -25,6 +25,7 @@ import { ContentDescriptorRpcRequestOptions, DisplayLabelRpcRequestOptions, DisplayLabelsRpcRequestOptions, ExtendedContentRpcRequestOptions, ExtendedHierarchyRpcRequestOptions, PresentationDataCompareRpcOptions, } from "../presentation-common/PresentationRpcInterface"; +import { HierarchyCompareInfoJSON, PartialHierarchyModificationJSON } from "../presentation-common/Update"; import { createRandomDescriptorJSON, createRandomECInstanceKeyJSON, createRandomECInstancesNodeJSON, createRandomECInstancesNodeKeyJSON, createRandomLabelDefinitionJSON, createRandomNodePathElementJSON, createRandomSelectionScope, @@ -497,7 +498,7 @@ describe("RpcRequestsHandler", () => { rpcInterfaceMock.verifyAll(); }); - it("forwards compareHierarchies call", async () => { + it("[deprecated] forwards compareHierarchies call", async () => { const handlerOptions: PresentationDataCompareOptions = { imodel: token, prev: { @@ -514,12 +515,39 @@ describe("RpcRequestsHandler", () => { rulesetOrId: "test2", expandedNodeKeys: [...handlerOptions.expandedNodeKeys!], }; - const result = new Array(); - rpcInterfaceMock.setup(async (x) => x.compareHierarchies(token, rpcOptions)).returns(async () => successResponse(result)).verifiable(); + const result: PartialHierarchyModificationJSON[] = []; + rpcInterfaceMock.setup(async (x) => x.compareHierarchies(token, rpcOptions)).returns(async () => successResponse(result)).verifiable(); // eslint-disable-line deprecation/deprecation expect(await handler.compareHierarchies(handlerOptions)).to.eq(result); rpcInterfaceMock.verifyAll(); }); + it("forwards compareHierarchiesPaged call", async () => { + const handlerOptions: PresentationDataCompareOptions = { + imodel: token, + prev: { + rulesetOrId: "test1", + }, + rulesetOrId: "test2", + expandedNodeKeys: [createRandomECInstancesNodeKeyJSON()], + resultSetSize: 10, + }; + const rpcOptions: PresentationDataCompareRpcOptions = { + clientId, + prev: { + rulesetOrId: "test1", + }, + rulesetOrId: "test2", + expandedNodeKeys: [...handlerOptions.expandedNodeKeys!], + resultSetSize: 10, + }; + const result: HierarchyCompareInfoJSON = { + changes: [], + }; + rpcInterfaceMock.setup(async (x) => x.compareHierarchiesPaged(token, rpcOptions)).returns(async () => successResponse(result)).verifiable(); // eslint-disable-line deprecation/deprecation + expect(await handler.compareHierarchiesPaged(handlerOptions)).to.eq(result); + rpcInterfaceMock.verifyAll(); + }); + }); }); diff --git a/presentation/common/src/test/Update.test.snap b/presentation/common/src/test/Update.test.snap index 53af523e6395..e5b5f7a9df58 100644 --- a/presentation/common/src/test/Update.test.snap +++ b/presentation/common/src/test/Update.test.snap @@ -1,5 +1,284 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`HierarchyCompareInfo fromJSON deserializes \`HierarchyCompareInfo\` from JSON 1`] = ` +Object { + "changes": Array [ + Object { + "node": Object { + "backColor": "rgb( + 191, + 115, + 94 + )", + "description": "Et repellat quis est est asperiores temporibus cumque.", + "foreColor": undefined, + "hasChildren": true, + "isCheckboxEnabled": true, + "isCheckboxVisible": true, + "isChecked": true, + "isEditable": true, + "isExpanded": false, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "Shoes", + "id": "0x155f30000011c09", + }, + Object { + "className": "SMS", + "id": "0xd3f50000016daf", + }, + ], + "pathFromRoot": Array [ + "c2680f08-74c2-48cc-900d-9a8f47408838", + "1a9bdf5c-0ec1-4e63-8a9d-5fb57ca52b06", + ], + "type": "ECInstancesNode", + }, + "label": Object { + "displayValue": "Swaziland", + "rawValue": "Incredible Steel Pizza", + "typeName": "string", + }, + }, + "position": 123, + "type": "Insert", + }, + Object { + "changes": Array [ + Object { + "name": "test", + "new": "new value", + "old": "old value", + }, + ], + "node": Object { + "backColor": "rgb( + 244, + 241, + 89 + )", + "description": undefined, + "foreColor": "#C00C97", + "hasChildren": true, + "isCheckboxEnabled": false, + "isCheckboxVisible": true, + "isChecked": false, + "isEditable": true, + "isExpanded": true, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "Advanced", + "id": "0xa758000001501f", + }, + Object { + "className": "Coordinator", + "id": "0x169970000006d19", + }, + ], + "pathFromRoot": Array [ + "208993b4-f58e-46eb-8bc2-d6848985b709", + "d9ef8dd8-680a-4a70-bb5e-2d5c60b2ce40", + ], + "type": "ECInstancesNode", + }, + "label": Object { + "displayValue": "Assurance", + "rawValue": "systemic", + "typeName": "string", + }, + }, + "type": "Update", + }, + Object { + "node": Object { + "backColor": undefined, + "description": undefined, + "foreColor": "#410292", + "hasChildren": true, + "isCheckboxEnabled": false, + "isCheckboxVisible": true, + "isChecked": false, + "isEditable": true, + "isExpanded": true, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "Incredible", + "id": "0x14e820000005fc6", + }, + Object { + "className": "Bedfordshire", + "id": "0xda7e00000098b7", + }, + ], + "pathFromRoot": Array [ + "c70f0c9c-7ba5-43f8-b647-85e112778e09", + "27c1006f-5b72-4573-9451-3b6b9e1a66cc", + ], + "type": "ECInstancesNode", + }, + "label": Object { + "displayValue": "deposit", + "rawValue": "Configuration", + "typeName": "string", + }, + }, + "type": "Delete", + }, + ], + "continuationToken": Object { + "currHierarchyNode": "currHierarchyNode", + "prevHierarchyNode": "prevHierarchyNode", + }, +} +`; + +exports[`HierarchyCompareInfo toJSON serializes \`HierarchyCompareInfo\` to JSON 1`] = ` +Object { + "changes": Array [ + Object { + "node": Object { + "backColor": "rgb( + 84, + 200, + 71 + )", + "description": "Eaque aliquam omnis rem aut.", + "foreColor": undefined, + "hasChildren": true, + "imageId": undefined, + "isCheckboxEnabled": false, + "isCheckboxVisible": false, + "isChecked": true, + "isEditable": false, + "isExpanded": false, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "calculating", + "id": "0x290e00000004fb", + }, + Object { + "className": "PCI", + "id": "0x12dc10000018297", + }, + ], + "pathFromRoot": Array [ + "40445966-a025-41c4-b51e-3c79af24b298", + "ea013b1d-aa19-4dab-8516-1da8428818b1", + ], + "type": "ECInstancesNode", + }, + "labelDefinition": Object { + "displayValue": "AGP", + "rawValue": "Electronics", + "typeName": "string", + }, + }, + "position": 123, + "type": "Insert", + }, + Object { + "changes": Array [ + Object { + "name": "test", + "new": "new value", + "old": "old value", + }, + ], + "node": Object { + "backColor": undefined, + "description": undefined, + "foreColor": undefined, + "hasChildren": true, + "imageId": undefined, + "isCheckboxEnabled": false, + "isCheckboxVisible": false, + "isChecked": true, + "isEditable": false, + "isExpanded": false, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "Pants", + "id": "0x66ab0000009c21", + }, + Object { + "className": "wireless", + "id": "0xab74000001800c", + }, + ], + "pathFromRoot": Array [ + "83658d13-45ce-4096-b0f1-724f9321d8df", + "ca1e952b-35e0-409f-ae85-b5c0c4a2deaf", + ], + "type": "ECInstancesNode", + }, + "labelDefinition": Object { + "displayValue": "Oregon", + "rawValue": "deposit", + "typeName": "string", + }, + }, + "type": "Update", + }, + Object { + "node": Object { + "backColor": "rgb( + 122, + 163, + 46 + )", + "description": undefined, + "foreColor": "#6682D5", + "hasChildren": false, + "imageId": undefined, + "isCheckboxEnabled": false, + "isCheckboxVisible": false, + "isChecked": false, + "isEditable": true, + "isExpanded": false, + "isSelectionDisabled": true, + "key": Object { + "instanceKeys": Array [ + Object { + "className": "purple", + "id": "0x3fce0000017ef6", + }, + Object { + "className": "Steel", + "id": "0x4059000000a6cd", + }, + ], + "pathFromRoot": Array [ + "d3097bf9-1ae1-4b58-ad3b-6bc9a7ccc2bc", + "d64e153b-3a16-4e3c-92a0-51e240565b04", + ], + "type": "ECInstancesNode", + }, + "labelDefinition": Object { + "displayValue": "auxiliary", + "rawValue": "Steel", + "typeName": "string", + }, + }, + "type": "Delete", + }, + ], + "continuationToken": Object { + "currHierarchyNode": "currHierarchyNode", + "prevHierarchyNode": "prevHierarchyNode", + }, +} +`; + exports[`HierarchyUpdateInfo fromJSON deserializes partial \`HierarchyUpdateInfo\` from JSON 1`] = ` Array [ Object { diff --git a/presentation/common/src/test/Update.test.ts b/presentation/common/src/test/Update.test.ts index c54670f3ff47..2ce1730e687b 100644 --- a/presentation/common/src/test/Update.test.ts +++ b/presentation/common/src/test/Update.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; import { - HierarchyUpdateInfo, HierarchyUpdateInfoJSON, NodeDeletionInfo, NodeDeletionInfoJSON, NodeInsertionInfo, NodeInsertionInfoJSON, NodeUpdateInfo, - NodeUpdateInfoJSON, PartialHierarchyModification, UpdateInfo, UpdateInfoJSON, + HierarchyCompareInfo, HierarchyCompareInfoJSON, HierarchyUpdateInfo, HierarchyUpdateInfoJSON, NodeDeletionInfo, NodeDeletionInfoJSON, + NodeInsertionInfo, NodeInsertionInfoJSON, NodeUpdateInfo, NodeUpdateInfoJSON, PartialHierarchyModification, UpdateInfo, UpdateInfoJSON, } from "../presentation-common"; import { createRandomECInstancesNode, createRandomECInstancesNodeJSON } from "./_helpers/random"; @@ -174,3 +174,75 @@ describe("PartialHierarchyModification", () => { }); }); + +describe("HierarchyCompareInfo", () => { + + describe("toJSON", () => { + + it("serializes `HierarchyCompareInfo` to JSON", () => { + const info: HierarchyCompareInfo = { + changes: [ + { + type: "Insert", + position: 123, + node: createRandomECInstancesNode(), + }, + { + type: "Update", + node: createRandomECInstancesNode(), + changes: [{ + name: "test", + old: "old value", + new: "new value", + }], + }, + { + type: "Delete", + node: createRandomECInstancesNode(), + }, + ], + continuationToken: { + prevHierarchyNode: "prevHierarchyNode", + currHierarchyNode: "currHierarchyNode", + }, + }; + expect(HierarchyCompareInfo.toJSON(info)).to.matchSnapshot(); + }); + + }); + + describe("fromJSON", () => { + + it("deserializes `HierarchyCompareInfo` from JSON", () => { + const info: HierarchyCompareInfoJSON = { + changes: [ + { + type: "Insert", + position: 123, + node: createRandomECInstancesNodeJSON(), + }, + { + type: "Update", + node: createRandomECInstancesNodeJSON(), + changes: [{ + name: "test", + old: "old value", + new: "new value", + }], + }, + { + type: "Delete", + node: createRandomECInstancesNodeJSON(), + }, + ], + continuationToken: { + prevHierarchyNode: "prevHierarchyNode", + currHierarchyNode: "currHierarchyNode", + }, + }; + expect(HierarchyCompareInfo.fromJSON(info)).to.matchSnapshot(); + }); + + }); + +}); diff --git a/presentation/frontend/src/presentation-frontend/PresentationManager.ts b/presentation/frontend/src/presentation-frontend/PresentationManager.ts index 5c5897de11ac..775f270972a2 100644 --- a/presentation/frontend/src/presentation-frontend/PresentationManager.ts +++ b/presentation/frontend/src/presentation-frontend/PresentationManager.ts @@ -192,10 +192,22 @@ export class PresentationManager implements IDisposable { return []; const options = await this.addRulesetAndVariablesToOptions(props); - let modifications: PartialHierarchyModification[]; + let modifications: PartialHierarchyModification[] = []; + try { - modifications = (await this.rpcRequestsHandler.compareHierarchies(this.toRpcTokenOptions(options))) - .map(PartialHierarchyModification.fromJSON); + while (true) { + const result = (await this.rpcRequestsHandler.compareHierarchiesPaged(this.toRpcTokenOptions(options))); + modifications.push(...result.changes.map(PartialHierarchyModification.fromJSON)); + if (!result.continuationToken) + break; + + if (result.changes.length === 0) { + Logger.logError(PresentationFrontendLoggerCategory.Package, "Hierarchy compare returned no changes but has continuation token."); + return []; + } + + options.continuationToken = result.continuationToken; + } } catch (e) { if (e instanceof PresentationError && e.errorNumber === PresentationStatus.Canceled) { modifications = []; diff --git a/presentation/frontend/src/test/PresentationManager.test.ts b/presentation/frontend/src/test/PresentationManager.test.ts index b9de380e8296..7bd0cc257d7a 100644 --- a/presentation/frontend/src/test/PresentationManager.test.ts +++ b/presentation/frontend/src/test/PresentationManager.test.ts @@ -11,20 +11,22 @@ import { IModelRpcProps } from "@bentley/imodeljs-common"; import { EventSource, IModelConnection, NativeApp } from "@bentley/imodeljs-frontend"; import { I18N, I18NNamespace } from "@bentley/imodeljs-i18n"; import { - Content, ContentDescriptorRequestOptions, ContentRequestOptions, Descriptor, DisplayLabelRequestOptions, - DisplayLabelsRequestOptions, DisplayValueGroup, DistinctValuesRequestOptions, ExtendedContentRequestOptions, ExtendedHierarchyRequestOptions, - FieldDescriptor, FieldDescriptorType, HierarchyRequestOptions, InstanceKey, Item, KeySet, LabelDefinition, LabelRequestOptions, - Node, NodeKey, NodePathElement, Paged, PresentationDataCompareOptions, PresentationError, PresentationRpcEvents, PresentationRpcInterface, - PresentationStatus, PresentationUnitSystem, RegisteredRuleset, RequestPriority, RpcRequestsHandler, Ruleset, RulesetVariable, UpdateInfo, - VariableValueTypes, + Content, ContentDescriptorRequestOptions, ContentRequestOptions, Descriptor, DisplayLabelRequestOptions, DisplayLabelsRequestOptions, + DisplayValueGroup, DistinctValuesRequestOptions, ExtendedContentRequestOptions, ExtendedHierarchyRequestOptions, FieldDescriptor, + FieldDescriptorType, HierarchyCompareInfoJSON, HierarchyRequestOptions, InstanceKey, Item, KeySet, LabelDefinition, LabelRequestOptions, Node, + NodeKey, NodePathElement, Paged, PartialHierarchyModification, PresentationDataCompareOptions, PresentationError, PresentationRpcEvents, + PresentationRpcInterface, PresentationStatus, PresentationUnitSystem, RegisteredRuleset, RequestPriority, RpcRequestsHandler, Ruleset, + RulesetVariable, UpdateInfo, VariableValueTypes, } from "@bentley/presentation-common"; import * as moq from "@bentley/presentation-common/lib/test/_helpers/Mocks"; import { - createRandomBaseNodeKey, createRandomDescriptor, createRandomECInstanceKey, createRandomECInstancesNode, createRandomECInstancesNodeKey, - createRandomLabelDefinition, createRandomNodePathElement, createRandomRuleset, createRandomTransientId, + createRandomBaseNodeKey, createRandomDescriptor, createRandomECInstanceKey, createRandomECInstancesNode, createRandomECInstancesNodeJSON, + createRandomECInstancesNodeKey, createRandomLabelDefinition, createRandomNodePathElement, createRandomRuleset, createRandomTransientId, } from "@bentley/presentation-common/lib/test/_helpers/random"; import { Presentation } from "../presentation-frontend/Presentation"; -import { buildPagedResponse, IModelContentChangeEventArgs, IModelHierarchyChangeEventArgs, PresentationManager } from "../presentation-frontend/PresentationManager"; +import { + buildPagedResponse, IModelContentChangeEventArgs, IModelHierarchyChangeEventArgs, PresentationManager, +} from "../presentation-frontend/PresentationManager"; import { RulesetManagerImpl } from "../presentation-frontend/RulesetManager"; import { RulesetVariablesManagerImpl } from "../presentation-frontend/RulesetVariablesManager"; import { TRANSIENT_ELEMENT_CLASSNAME } from "../presentation-frontend/selection/SelectionManager"; @@ -1202,7 +1204,7 @@ describe("PresentationManager", () => { rulesetOrId: "test", }; const actualResult = await manager.compareHierarchies(options); - rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchies(moq.It.isAny()), moq.Times.never()); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.never()); expect(actualResult).to.deep.eq([]); }); @@ -1215,11 +1217,11 @@ describe("PresentationManager", () => { rulesetOrId: "test2", }; rpcRequestsHandlerMock - .setup(async (x) => x.compareHierarchies(prepareOptions({ ...options }))) + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options }))) .throws(new PresentationError(PresentationStatus.Canceled)) .verifiable(); const actualResult = await manager.compareHierarchies(options); - rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchies(moq.It.isAny()), moq.Times.once()); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.once()); expect(actualResult).to.deep.eq([]); }); @@ -1232,11 +1234,11 @@ describe("PresentationManager", () => { rulesetOrId: "test", }; rpcRequestsHandlerMock - .setup(async (x) => x.compareHierarchies(prepareOptions({ ...options }))) + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options }))) .throws(new PresentationError(PresentationStatus.Error)) .verifiable(); await expect(manager.compareHierarchies(options)).to.eventually.be.rejectedWith(PresentationError); - rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchies(moq.It.isAny()), moq.Times.once()); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.once()); }); it("requests hierarchy comparison and returns result", async () => { @@ -1250,12 +1252,109 @@ describe("PresentationManager", () => { rulesetVariables: [], expandedNodeKeys: [createRandomBaseNodeKey()], }; + const compareInfo: HierarchyCompareInfoJSON = { + changes: [ + { + type: "Insert", + position: 0, + node: createRandomECInstancesNodeJSON(), + }, + ], + }; rpcRequestsHandlerMock - .setup(async (x) => x.compareHierarchies(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON) }))) - .throws(new PresentationError(PresentationStatus.Error)) + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON) }))) + .returns(async () => compareInfo) .verifiable(); - await expect(manager.compareHierarchies(options)).to.eventually.be.rejectedWith(PresentationError); - rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchies(moq.It.isAny()), moq.Times.once()); + const changes = await manager.compareHierarchies(options); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.once()); + expect(changes).to.be.deep.eq(compareInfo.changes.map(PartialHierarchyModification.fromJSON)); + }); + + it("makes multiple requests until collects all results", async () => { + const options: PresentationDataCompareOptions = { + imodel: testData.imodelMock.object, + prev: { + rulesetOrId: "test1", + rulesetVariables: [], + }, + rulesetOrId: "test", + rulesetVariables: [], + expandedNodeKeys: [createRandomBaseNodeKey()], + }; + const compareInfo1: HierarchyCompareInfoJSON = { + changes: [ + { + type: "Insert", + position: 0, + node: createRandomECInstancesNodeJSON(), + }, + ], + continuationToken: { + prevHierarchyNode: "prevNode", + currHierarchyNode: "currNode", + }, + }; + const compareInfo2: HierarchyCompareInfoJSON = { + changes: [ + { + type: "Insert", + position: 1, + node: createRandomECInstancesNodeJSON(), + }, + ], + }; + rpcRequestsHandlerMock + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON) }))) + .returns(async () => compareInfo1); + rpcRequestsHandlerMock + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON), continuationToken: compareInfo1.continuationToken }))) + .returns(async () => compareInfo2); + const changes = await manager.compareHierarchies(options); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.exactly(2)); + expect(changes.length).to.be.eq(2); + expect(changes).to.be.deep.eq([...compareInfo1.changes.map(PartialHierarchyModification.fromJSON), ...compareInfo2.changes.map(PartialHierarchyModification.fromJSON)]); + }); + + it("avoid infinitely requesting if continuation token returned with no changes", async () => { + const options: PresentationDataCompareOptions = { + imodel: testData.imodelMock.object, + prev: { + rulesetOrId: "test1", + rulesetVariables: [], + }, + rulesetOrId: "test", + rulesetVariables: [], + expandedNodeKeys: [createRandomBaseNodeKey()], + }; + const compareInfo1: HierarchyCompareInfoJSON = { + changes: [ + { + type: "Insert", + position: 0, + node: createRandomECInstancesNodeJSON(), + }, + ], + continuationToken: { + prevHierarchyNode: "prevNode", + currHierarchyNode: "currNode", + }, + }; + const compareInfo2: HierarchyCompareInfoJSON = { + changes: [], + continuationToken: { + prevHierarchyNode: "prevNode1", + currHierarchyNode: "currNode1", + }, + }; + rpcRequestsHandlerMock + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON) }))) + .returns(async () => compareInfo1); + rpcRequestsHandlerMock + .setup(async (x) => x.compareHierarchiesPaged(prepareOptions({ ...options, expandedNodeKeys: options.expandedNodeKeys!.map(NodeKey.toJSON), continuationToken: compareInfo1.continuationToken }))) + .returns(async () => compareInfo2); + const changes = await manager.compareHierarchies(options); + rpcRequestsHandlerMock.verify(async (x) => x.compareHierarchiesPaged(moq.It.isAny()), moq.Times.exactly(2)); + expect(changes.length).to.be.eq(0); }); });