UNPKG

@itwin/presentation-backend

Version:

Backend of iTwin.js Presentation library

403 lines • 20.4 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module RPC */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PresentationRpcImpl = exports.MAX_ALLOWED_KEYS_PAGE_SIZE = exports.MAX_ALLOWED_PAGE_SIZE = void 0; const core_backend_1 = require("@itwin/core-backend"); const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const presentation_common_1 = require("@itwin/presentation-common"); const internal_1 = require("@itwin/presentation-common/internal"); // @ts-expect-error TS complains about `with` in CJS builds; The path is fine at runtime, but not at compile time // eslint-disable-next-line @itwin/import-within-package const package_json_1 = __importDefault(require("../../../package.json")); const BackendLoggerCategory_js_1 = require("./BackendLoggerCategory.js"); const InternalSymbols_js_1 = require("./InternalSymbols.js"); const Presentation_js_1 = require("./Presentation.js"); const PresentationManagerDetail_js_1 = require("./PresentationManagerDetail.js"); const TemporaryStorage_js_1 = require("./TemporaryStorage.js"); const packageJsonVersion = package_json_1.default.version; /** @internal */ exports.MAX_ALLOWED_PAGE_SIZE = 1000; /** @internal */ exports.MAX_ALLOWED_KEYS_PAGE_SIZE = 10000; const DEFAULT_REQUEST_TIMEOUT = 5000; /** * The backend implementation of PresentationRpcInterface. All it's basically * responsible for is forwarding calls to [[Presentation.manager]]. * * @internal */ class PresentationRpcImpl extends presentation_common_1.PresentationRpcInterface { _requestTimeout; _pendingRequests; _cancelEvents; _statusHandler; constructor(props) { super(); this._requestTimeout = props?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT; this._pendingRequests = new TemporaryStorage_js_1.TemporaryStorage({ // remove the pending request after request timeout + 10 seconds - this gives // frontend 10 seconds to re-send the request until it's removed from requests' cache unusedValueLifetime: this._requestTimeout > 0 ? this._requestTimeout + 10 * 1000 : undefined, // attempt to clean up every second cleanupInterval: 1000, cleanupHandler: (id, _, reason) => { if (reason !== "request") { core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Cleaning up request without frontend retrieving it: ${id}.`); /* c8 ignore next */ this._cancelEvents.get(id)?.raiseEvent(); } this._cancelEvents.delete(id); }, }); this._cancelEvents = new Map(); this._statusHandler = createStatusCategoryHandler(); core_bentley_1.StatusCategory.handlers.add(this._statusHandler); } [Symbol.dispose]() { this._pendingRequests[Symbol.dispose](); core_bentley_1.StatusCategory.handlers.delete(this._statusHandler); } get requestTimeout() { return this._requestTimeout; } get pendingRequests() { return this._pendingRequests; } /** Returns an ok response with result inside */ successResponse(result, diagnostics) { return { statusCode: presentation_common_1.PresentationStatus.Success, result, diagnostics, }; } /** Returns a bad request response with empty result and an error code */ errorResponse(errorCode, errorMessage, diagnostics) { return { statusCode: errorCode, result: undefined, errorMessage, diagnostics, }; } /** * Get the [[PresentationManager]] used by this RPC impl. */ getManager(clientId) { return Presentation_js_1.Presentation.getManager(clientId); } async getIModel(token) { const imodel = core_backend_1.IModelDb.findByKey(token.key); // call refreshContainer, just in case this is a V2 checkpoint whose sasToken is about to expire, or its default transaction is about to be restarted. await imodel.refreshContainerForRpc(core_backend_1.RpcTrace.expectCurrentActivity.accessToken); return imodel; } async makeRequest(token, requestId, requestOptions, request) { const serializedRequestOptionsForLogging = JSON.stringify({ ...(0, core_bentley_1.omit)(requestOptions, ["rulesetOrId"]), ...(requestOptions.rulesetOrId ? { rulesetId: (0, PresentationManagerDetail_js_1.getRulesetIdObject)(requestOptions.rulesetOrId).uniqueId } : undefined), }); core_bentley_1.Logger.logInfo(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Received '${requestId}' request. Params: ${serializedRequestOptionsForLogging}`); const imodel = await this.getIModel(token); const requestKey = JSON.stringify({ iModelKey: token.key, requestId, requestOptions }); let resultPromise = this._pendingRequests.getValue(requestKey); if (resultPromise) { core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request already pending`); } else { core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request not found, creating a new one`); const { clientId: _, diagnostics: diagnosticsOptions, rulesetVariables, ...options } = requestOptions; const managerRequestOptions = { ...options, imodel, cancelEvent: new core_bentley_1.BeEvent(), }; // set up ruleset variables if (rulesetVariables) { managerRequestOptions.rulesetVariables = rulesetVariables.map(presentation_common_1.RulesetVariable.fromJSON); } // set up diagnostics listener let diagnostics; const getDiagnostics = () => { if (!diagnostics) { diagnostics = {}; } return diagnostics; }; if (diagnosticsOptions) { if (diagnosticsOptions.backendVersion) { getDiagnostics().backendVersion = packageJsonVersion; } managerRequestOptions.diagnostics = { ...diagnosticsOptions, handler: (d) => { if (d.logs) { const target = getDiagnostics(); if (target.logs) { target.logs.push(...d.logs); } else { target.logs = [...d.logs]; } } }, }; } // initiate request resultPromise = request(managerRequestOptions).then((result) => this.successResponse(result, diagnostics)); // store the request promise this._pendingRequests.addValue(requestKey, resultPromise); this._cancelEvents.set(requestKey, managerRequestOptions.cancelEvent); } if (this._requestTimeout === 0) { core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request timeout not configured, returning promise without a timeout.`); void resultPromise.finally(() => { this._pendingRequests.deleteValue(requestKey); }); return resultPromise; } core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Returning a promise with a timeout of ${this._requestTimeout}.`); const timeout = (0, internal_1.createCancellableTimeoutPromise)(this._requestTimeout); return Promise.race([ resultPromise, timeout.promise.then(() => { // eslint-disable-next-line @typescript-eslint/only-throw-error throw new core_common_1.RpcPendingResponse("Timeout"); }), ]) .then((response) => { core_bentley_1.Logger.logTrace(BackendLoggerCategory_js_1.PresentationBackendLoggerCategory.Rpc, `Request completed, returning result.`); this._pendingRequests.deleteValue(requestKey); return response; }) .finally(() => { timeout.cancel(); }); } async getNodesCount(token, requestOptions) { return this.makeRequest(token, "getNodesCount", requestOptions, async (options) => { return this.getManager(requestOptions.clientId).getNodesCount(options); }); } async getPagedNodes(token, requestOptions) { return this.makeRequest(token, "getPagedNodes", requestOptions, async (options) => { options = enforceValidPageSize(options); const [serializedHierarchyLevel, count] = await Promise.all([ this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodes(options), this.getManager(requestOptions.clientId).getNodesCount(options), ]); const hierarchyLevel = (0, internal_1.deepReplaceNullsToUndefined)(JSON.parse(serializedHierarchyLevel)); return { total: count, items: hierarchyLevel.nodes, }; }); } async getNodesDescriptor(token, requestOptions) { return this.makeRequest(token, "getNodesDescriptor", requestOptions, async (options) => { return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodesDescriptor(options); }); } async getNodePaths(token, requestOptions) { return this.makeRequest(token, "getNodePaths", requestOptions, async (options) => { return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getNodePaths(options); }); } async getFilteredNodePaths(token, requestOptions) { return this.makeRequest(token, "getFilteredNodePaths", requestOptions, async (options) => { return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getFilteredNodePaths(options); }); } async getContentSources(token, requestOptions) { return this.makeRequest(token, "getContentSources", requestOptions, async (options) => { const result = await this.getManager(requestOptions.clientId).getContentSources(options); const classesMap = {}; const selectClasses = result.map((sci) => presentation_common_1.SelectClassInfo.toCompressedJSON(sci, classesMap)); return { sources: selectClasses, classesMap }; }); } async getContentDescriptor(token, requestOptions) { return this.makeRequest(token, "getContentDescriptor", requestOptions, async (options) => { options = { ...options, contentFlags: (options.contentFlags ?? 0) | PresentationManagerDetail_js_1.DESCRIPTOR_ONLY_CONTENT_FLAG, // always append the "descriptor only" flag when handling request from the frontend keys: presentation_common_1.KeySet.fromJSON(options.keys), }; // Here we send a plain JSON string but we will parse it to DescriptorJSON on the frontend. This way we are // bypassing unnecessary deserialization and serialization. return Presentation_js_1.Presentation.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContentDescriptor(options); }); } async getContentSetSize(token, requestOptions) { return this.makeRequest(token, "getContentSetSize", requestOptions, async (options) => { options = { ...options, keys: presentation_common_1.KeySet.fromJSON(options.keys), }; return this.getManager(requestOptions.clientId).getContentSetSize(options); }); } async getPagedContent(token, requestOptions) { return this.makeRequest(token, "getPagedContent", requestOptions, async (options) => { options = enforceValidPageSize({ ...options, keys: presentation_common_1.KeySet.fromJSON(options.keys), }); const [size, content] = await Promise.all([ this.getManager(requestOptions.clientId).getContentSetSize(options), this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContent(options), ]); if (!content) { return undefined; } return { descriptor: content.descriptor.toJSON(), contentSet: { total: size, items: content.contentSet.map((i) => i.toJSON()), }, }; }); } async getPagedContentSet(token, requestOptions) { // eslint-disable-next-line @typescript-eslint/no-deprecated const response = await this.getPagedContent(token, requestOptions); if (response.statusCode !== presentation_common_1.PresentationStatus.Success) { return this.errorResponse(response.statusCode, response.errorMessage, response.diagnostics); } if (!response.result) { return this.errorResponse(presentation_common_1.PresentationStatus.Error, `Failed to get content set (received a success response with empty result)`, response.diagnostics); } return { ...response, result: response.result.contentSet, }; } async getElementProperties(token, requestOptions) { return this.makeRequest(token, "getElementProperties", { ...requestOptions }, async (options) => { const { clientId, ...restOptions } = options; return this.getManager(clientId).getElementProperties(restOptions); }); } async getPagedDistinctValues(token, requestOptions) { return this.makeRequest(token, "getPagedDistinctValues", requestOptions, async (options) => { options = enforceValidPageSize({ ...options, keys: presentation_common_1.KeySet.fromJSON(options.keys), }); return this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getPagedDistinctValues(options); }); } async getContentInstanceKeys(token, requestOptions) { return this.makeRequest(token, "getContentInstanceKeys", requestOptions, async (options) => { const { displayType, ...optionsNoDisplayType } = options; options = enforceValidPageSize({ ...optionsNoDisplayType, keys: presentation_common_1.KeySet.fromJSON(optionsNoDisplayType.keys), descriptor: { displayType, contentFlags: presentation_common_1.ContentFlags.KeysOnly, }, }, exports.MAX_ALLOWED_KEYS_PAGE_SIZE); const [size, content] = await Promise.all([ this.getManager(requestOptions.clientId).getContentSetSize(options), this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getContent(options), ]); if (size === 0 || !content) { return { total: 0, items: new presentation_common_1.KeySet().toJSON() }; } return { total: size, items: content.contentSet.reduce((keys, item) => keys.add(item.primaryKeys), new presentation_common_1.KeySet()).toJSON(), }; }); } async getDisplayLabelDefinition(token, requestOptions) { return this.makeRequest(token, "getDisplayLabelDefinition", requestOptions, async (options) => { const label = await this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getDisplayLabelDefinition(options); return label; }); } async getPagedDisplayLabelDefinitions(token, requestOptions) { const pageOpts = enforceValidPageSize({ paging: { start: 0, size: requestOptions.keys.length } }); if (pageOpts.paging.size < requestOptions.keys.length) { requestOptions.keys.splice(pageOpts.paging.size); } return this.makeRequest(token, "getPagedDisplayLabelDefinitions", requestOptions, async (options) => { const labels = await this.getManager(requestOptions.clientId)[InternalSymbols_js_1._presentation_manager_detail].getDisplayLabelDefinitions({ ...options, keys: options.keys, }); return { total: options.keys.length, items: labels, }; }); } /* eslint-disable @typescript-eslint/no-deprecated */ async getSelectionScopes(token, requestOptions) { return this.makeRequest(token, "getSelectionScopes", requestOptions, async (options) => this.getManager(requestOptions.clientId).getSelectionScopes(options)); } async computeSelection(token, requestOptions) { return this.makeRequest(token, "computeSelection", requestOptions, async (options) => { const keys = await this.getManager(requestOptions.clientId).computeSelection(options); return keys.toJSON(); }); } } exports.PresentationRpcImpl = PresentationRpcImpl; const enforceValidPageSize = (requestOptions, maxPageSize = exports.MAX_ALLOWED_PAGE_SIZE) => { const validPageSize = getValidPageSize(requestOptions.paging?.size, maxPageSize); if (!requestOptions.paging || requestOptions.paging.size !== validPageSize) { return { ...requestOptions, paging: { ...requestOptions.paging, size: validPageSize } }; } return requestOptions; }; const getValidPageSize = (size, maxPageSize) => { const requestedSize = size ?? 0; return requestedSize === 0 || requestedSize > maxPageSize ? maxPageSize : requestedSize; }; // not testing temporary solution /* c8 ignore start */ function createStatusCategoryHandler() { return (e) => { if (e instanceof presentation_common_1.PresentationError) { switch (e.errorNumber) { case presentation_common_1.PresentationStatus.NotInitialized: return new (class extends core_bentley_1.ErrorCategory { name = "Internal server error"; code = 500; })(); case presentation_common_1.PresentationStatus.Canceled: return new (class extends core_bentley_1.SuccessCategory { name = "Cancelled"; code = 204; })(); case presentation_common_1.PresentationStatus.ResultSetTooLarge: return new (class extends core_bentley_1.ErrorCategory { name = "Result set is too large"; code = 413; })(); case presentation_common_1.PresentationStatus.Error: case presentation_common_1.PresentationStatus.InvalidArgument: return new (class extends core_bentley_1.ErrorCategory { name = "Invalid request props"; code = 422; })(); } } return undefined; }; } /* c8 ignore end */ //# sourceMappingURL=PresentationRpcImpl.js.map