From 822e56bd7fad8b4b12964375961a36e22a430955 Mon Sep 17 00:00:00 2001 From: Josh Wulf Date: Wed, 10 Apr 2024 14:17:48 +1200 Subject: [PATCH] docs(repo): add type of RESTError to JSDoc --- .../operate/operate-integration.spec.ts | 45 ++++++++++ src/admin/lib/AdminApiClient.ts | 45 +++++----- src/index.ts | 2 + src/lib/GotErrors.ts | 13 +++ src/lib/GotHooks.ts | 16 ++++ src/lib/index.ts | 2 + src/modeler/lib/ModelerAPIClient.ts | 83 ++++++++++++++----- src/operate/lib/OperateApiClient.ts | 66 ++++++++++----- src/optimize/lib/OptimizeApiClient.ts | 40 ++++----- src/tasklist/lib/TasklistApiClient.ts | 31 +++---- 10 files changed, 236 insertions(+), 107 deletions(-) create mode 100644 src/lib/GotErrors.ts create mode 100644 src/lib/GotHooks.ts diff --git a/src/__tests__/operate/operate-integration.spec.ts b/src/__tests__/operate/operate-integration.spec.ts index 6360db57..a0578033 100644 --- a/src/__tests__/operate/operate-integration.spec.ts +++ b/src/__tests__/operate/operate-integration.spec.ts @@ -1,3 +1,4 @@ +import { RESTError } from 'lib' import { LosslessNumber } from 'lossless-json' import { OperateApiClient } from '../../operate' @@ -51,3 +52,47 @@ test('getJSONVariablesforProcess works', async () => { const res = await c.getJSONVariablesforProcess(p.processInstanceKey) expect(res.foo).toBe('bar') }) + +test('test error type', async () => { + const c = new OperateApiClient() + const zeebe = new ZeebeGrpcClient() + await zeebe.deployResource({ + processFilename: 'src/__tests__/testdata/Operate-StraightThrough.bpmn', + }) + const p = await zeebe.createProcessInstanceWithResult({ + bpmnProcessId: 'operate-straightthrough', + variables: { + foo: 'bar', + }, + }) + await new Promise((res) => setTimeout(() => res(null), 5000)) + /** + * Here we request a process instance that doesn't exist. + * Understanding that this is the issue requires a bit of gymnastics by the consumer. + * This call may fail due to lack of permissions, a network error (including misconfiguration), or the process instance not existing. + * To rule out the other issues and focus on the process instance not existing, we need to catch the error and check the response body. + * (e.response?.body as string).includes('404') will return true if the response body contains the string '404'. + * This is a bit of a hack, but it's the best we can do without a more specific error type. + * Do we really want to expose consumers to this? + */ + const res = await c + .getProcessInstance(`${p.processInstanceKey}1`) + .catch((e: RESTError) => { + // console.log(e.code) + // `ERR_NON_2XX_3XX_RESPONSE` + + // console.log(e.message) + // `Response code 404 (Not Found) (request to http://localhost:8081/v1/process-instances/22517998149629301)` + // Note: The request url has been enhanced into the message by a hook. + + // console.log(e.response?.body) + // `{"status":404,"message":"No process instances found for key 22517998149629301 ","instance":"76807bf1-d877-4f8e-bd0d-6d953b1799e5","type":"Requested resource not found"}` + + // console.log(typeof e.response?.body) + // `string` + + expect((e.response?.body as string).includes('404')).toBe(true) + return false + }) + expect(res).toBe(false) +}) diff --git a/src/admin/lib/AdminApiClient.ts b/src/admin/lib/AdminApiClient.ts index effda3e1..565406f6 100644 --- a/src/admin/lib/AdminApiClient.ts +++ b/src/admin/lib/AdminApiClient.ts @@ -9,6 +9,8 @@ import { RequireConfiguration, constructOAuthProvider, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, } from '../../lib' import { IOAuthProvider } from '../../oauth' @@ -16,6 +18,10 @@ import * as Dto from './AdminDto' const debug = d('camunda:adminconsole') +/** + * This class provides methods to interact with the Camunda Admin API. + * @throws {RESTError} An error that may occur during API operations. + */ export class AdminApiClient { private userAgentString: string private oAuthProvider: IOAuthProvider @@ -44,25 +50,9 @@ export class AdminApiClient { https: { certificateAuthority, }, - handlers: [ - (options, next) => { - if (Object.isFrozen(options.context)) { - options.context = { ...options.context } - } - Error.captureStackTrace(options.context) - return next(options) - }, - ], + handlers: [gotErrorHandler], hooks: { - beforeError: [ - (error) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(error as any).source = (error as any).options.context.stack.split( - '\n' - ) - return error - }, - ], + beforeError: [gotBeforeErrorHook], }, }) debug('prefixUrl', `${baseUrl}/clusters`) @@ -82,6 +72,7 @@ export class AdminApiClient { /** * * @description Get an array of the current API clients for this cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetClients) for more details. + * @throws {RESTError} * @param clusterUuid - The cluster UUID * */ @@ -94,7 +85,7 @@ export class AdminApiClient { /** * @description Create a new API client for a cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/CreateClient) for more details. - * @returns + * @throws {RESTError} */ async createClient(req: { clusterUuid: string @@ -117,6 +108,7 @@ export class AdminApiClient { * @description Get the details of an API client. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetClient) for more details. * @param clusterUuid * @param clientId + * @throws {RESTError} * @returns */ async getClient( @@ -133,6 +125,7 @@ export class AdminApiClient { * @description See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/DeleteClient) for more details. * @param clusterUuid * @param clientId + * @throws {RESTError} */ async deleteClient(clusterUuid: string, clientId: string): Promise { const headers = await this.getHeaders() @@ -146,6 +139,7 @@ export class AdminApiClient { /** * * @description Return an array of clusters. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetClusters) for more details. + * @throws {RESTError} */ async getClusters(): Promise { const headers = await this.getHeaders() @@ -157,6 +151,7 @@ export class AdminApiClient { /** * * @description Create a new cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/CreateCluster) for more details. + * @throws {RESTError} */ async createCluster( createClusterRequest: Dto.CreateClusterBody @@ -173,6 +168,7 @@ export class AdminApiClient { /** * * @description Retrieve the metadata for a cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetCluster) for more details. + * @throws {RESTError} * */ async getCluster(clusterUuid: string): Promise { @@ -185,6 +181,7 @@ export class AdminApiClient { /** * * @description Delete a cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/DeleteCluster) for more details. + * @throws {RESTError} * */ async deleteCluster(clusterUuid: string): Promise { @@ -199,6 +196,7 @@ export class AdminApiClient { /** * * @description Retrieve the available parameters for cluster creation. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetParameters) for more details. + * @throws {RESTError} */ async getParameters(): Promise { const headers = await this.getHeaders() @@ -210,6 +208,7 @@ export class AdminApiClient { /** * * @description Retrieve the connector secrets. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/GetSecrets) for more details. + * @throws {RESTError} */ async getSecrets(clusterUuid: string): Promise<{ [key: string]: string }> { const headers = await this.getHeaders() @@ -221,6 +220,7 @@ export class AdminApiClient { /** * * @description Create a new connector secret. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/CreateSecret) for more details. + * @throws {RESTError} */ async createSecret({ clusterUuid, @@ -242,6 +242,7 @@ export class AdminApiClient { /** * * @description Delete a connector secret. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/DeleteSecret) for more details. + * @throws {RESTError} */ async deleteSecret(clusterUuid: string, secretName: string): Promise { const headers = await this.getHeaders() @@ -255,6 +256,7 @@ export class AdminApiClient { /** * * @description Add one or more IPs to the whitelist for the cluster. See [the API Documentation](https://console.cloud.camunda.io/customer-api/openapi/docs/#/default/UpdateIpWhitelist) for more details. + * @throws {RESTError} * @param ipwhitelist * @returns */ @@ -266,7 +268,7 @@ export class AdminApiClient { ip: string }, ] - ) { + ): Promise { const headers = await this.getHeaders() return this.rest .put(`${clusterUuid}/ipwhitelist`, { @@ -281,6 +283,7 @@ export class AdminApiClient { /** * * @description Retrieve a list of members and pending invites for your organisation. See the [API Documentation]() for more details. + * @throws {RESTError} */ async getUsers(): Promise { const headers = await this.getHeaders() @@ -294,6 +297,7 @@ export class AdminApiClient { /** * * @description Add a member. See the [API Documentation]() for more details. + * @throws {RESTError} * */ async createMember( @@ -312,6 +316,7 @@ export class AdminApiClient { /** * * @description Delete a member from your organization. See the [API Documentation]() for more details. + * @throws {RESTError} * */ async deleteMember(email: string): Promise { diff --git a/src/index.ts b/src/index.ts index 44ca7649..005030c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ import * as Optimize from './optimize' import * as Tasklist from './tasklist' import * as Zeebe from './zeebe' +export { RESTError } from './lib' + export const Dto = { ChildDto, BigIntValue, Int64String, LosslessDto } export { Admin, Auth, Camunda8, Modeler, Operate, Optimize, Tasklist, Zeebe } diff --git a/src/lib/GotErrors.ts b/src/lib/GotErrors.ts new file mode 100644 index 00000000..330d206b --- /dev/null +++ b/src/lib/GotErrors.ts @@ -0,0 +1,13 @@ +import * as Got from 'got' + +export type RESTError = + | Got.HTTPError + | Got.RequestError + | Got.ReadError + | Got.ParseError + | Got.HTTPError + | Got.TimeoutError + | Got.CancelError + | Got.CacheError + | Got.MaxRedirectsError + | Got.UnsupportedProtocolError diff --git a/src/lib/GotHooks.ts b/src/lib/GotHooks.ts new file mode 100644 index 00000000..306a891e --- /dev/null +++ b/src/lib/GotHooks.ts @@ -0,0 +1,16 @@ +export const gotErrorHandler = (options, next) => { + if (Object.isFrozen(options.context)) { + options.context = { ...options.context } + } + Error.captureStackTrace(options.context) + + return next(options) +} + +export const gotBeforeErrorHook = (error) => { + const { request } = error + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(error as any).source = (error as any).options.context.stack.split('\n') + error.message += ` (request to ${request?.options.url.href})` + return error +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 8b076b5d..bcaa3ebd 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -6,6 +6,8 @@ export * from './CreateUserAgentString' export * from './Delay' export * from './EnvironmentSetup' export { packageVersion } from './GetPackageVersion' +export * from './GotErrors' +export * from './GotHooks' export * from './LosslessJsonParser' export { RequireConfiguration } from './RequireConfiguration' export * from './SuppressZeebeLogging' diff --git a/src/modeler/lib/ModelerAPIClient.ts b/src/modeler/lib/ModelerAPIClient.ts index 48cfb50b..15c1ae43 100644 --- a/src/modeler/lib/ModelerAPIClient.ts +++ b/src/modeler/lib/ModelerAPIClient.ts @@ -8,6 +8,8 @@ import { GetCertificateAuthority, constructOAuthProvider, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, } from '../../lib' import { IOAuthProvider } from '../../oauth' @@ -44,25 +46,9 @@ export class ModelerApiClient { certificateAuthority, }, responseType: 'json', - handlers: [ - (options, next) => { - if (Object.isFrozen(options.context)) { - options.context = { ...options.context } - } - Error.captureStackTrace(options.context) - return next(options) - }, - ], + handlers: [gotErrorHandler], hooks: { - beforeError: [ - (error) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(error as any).source = (error as any).options.context.stack.split( - '\n' - ) - return error - }, - ], + beforeError: [gotBeforeErrorHook], }, }) debug(`baseUrl: ${prefixUrl}`) @@ -97,6 +83,7 @@ export class ModelerApiClient { /** * Adds a new collaborator to a project or modifies the permission level of an existing collaborator. * Note: Only users that are part of the authorized organization (see GET /api/v1/info) and logged in to Web Modeler at least once can be added to a project. + * @throws {RESTError} */ async addCollaborator(req: Dto.CreateCollaboratorDto): Promise { const headers = await this.getHeaders() @@ -114,6 +101,7 @@ export class ModelerApiClient { * sort specifies by which fields and direction (ASC/DESC) the result should be sorted. * page specifies the page number to return. * size specifies the number of items per page. The default value is 10. + * @throws {RESTError} */ async searchCollaborators( req: Dto.PubSearchDtoProjectCollaboratorDto @@ -129,6 +117,11 @@ export class ModelerApiClient { ) as Promise } + /** + * + * @throws {RESTError} + * @returns + */ async deleteCollaborator({ email, projectId, @@ -165,7 +158,9 @@ export class ModelerApiClient { * * The value of content.version is managed by Web Modeler and will be updated automatically. * - * Note: The simplePath transforms any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). This form of escaping facilitates the processing of path-like structures within file and folder names. + * Note: The simplePath transforms any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). + * This form of escaping facilitates the processing of path-like structures within file and folder names. + * @throws {RESTError} */ async createFile(req: Dto.CreateFileDto): Promise { const headers = await this.getHeaders() @@ -185,6 +180,7 @@ export class ModelerApiClient { * facilitates the processing of path-like structures within file and folder names. * * Does this throw if it is not found? + * @throws {RESTError} */ async getFile(fileId: string): Promise { const headers = await this.getHeaders() @@ -195,7 +191,9 @@ export class ModelerApiClient { /** * Deletes a file. - * Note: Deleting a file will also delete other resources attached to the file (comments, call activity/business rule task links, milestones and shares) which might have side-effects. Deletion of resources is recursive and cannot be undone. + * Note: Deleting a file will also delete other resources attached to the file (comments, call activity/business rule task links, milestones and shares) which might have side-effects. + * Deletion of resources is recursive and cannot be undone. + * @throws {RESTError} */ async deleteFile(fileId: string): Promise { const headers = await this.getHeaders() @@ -217,7 +215,9 @@ export class ModelerApiClient { * The value of content.name can only be changed via name. * The value of content.id is not updatable. * The value of content.version is managed by Web Modeler and will be updated automatically. - * Note: The simplePath transforms any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). This form of escaping facilitates the processing of path-like structures within file and folder names. + * Note: The simplePath transforms any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). + * This form of escaping facilitates the processing of path-like structures within file and folder names. + * @throws {RESTError} */ async updateFile( fileId: string, @@ -252,7 +252,9 @@ export class ModelerApiClient { * page specifies the page number to return. * size specifies the number of items per page. The default value is 10. * - * Note: The simplePath transform any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). This form of escaping facilitates the processing of path-like structures within file and folder names. + * Note: The simplePath transform any occurrences of slashes ("/") in file and folder names into an escape sequence consisting of a backslash followed by a slash ("\/"). + * This form of escaping facilitates the processing of path-like structures within file and folder names. + * @throws {RESTError} */ async searchFiles( req: Dto.PubSearchDtoFileMetadataDto @@ -275,6 +277,7 @@ export class ModelerApiClient { * When projectId is given and parentId is either null or omitted altogether, the folder will be created in the root of the project. * * When projectId and parentId are both given, they must be consistent - i.e. the parent folder is in the project. + * @throws {RESTError} */ async createFolder(req: Dto.CreateFolderDto): Promise { const headers = await this.getHeaders() @@ -286,6 +289,11 @@ export class ModelerApiClient { .then(this.decodeResponseOrThrow) as Promise } + /** + * + * @throws {RESTError} + * @returns + */ async getFolder(folderId: string): Promise { const headers = await this.getHeaders() return this.rest(`folders/${folderId}`, { @@ -296,6 +304,7 @@ export class ModelerApiClient { /** * * Deletes an empty folder. A folder is considered empty if there are no files in it. Deletion of resources is recursive and cannot be undone. + * @throws {RESTError} */ async deleteFolder(folderId: string): Promise { const headers = await this.getHeaders() @@ -316,6 +325,7 @@ export class ModelerApiClient { * When projectId is given and parentId is either null or omitted altogether, the file will be moved to the root of the project. * * When projectId and parentId are both given, they must be consistent - i.e. the new parent folder is in the new project. + * @throws {RESTError} */ async updateFolder( folderId: string, @@ -330,6 +340,10 @@ export class ModelerApiClient { .then(this.decodeResponseOrThrow) as Promise } + /** + * + * @throws {RESTError} + */ async getInfo(): Promise { const headers = await this.getHeaders() return this.rest(`info`, { @@ -337,6 +351,10 @@ export class ModelerApiClient { }).then(this.decodeResponseOrThrow) as Promise } + /** + * + * @throws {RESTError} + */ async createMilestone( req: Dto.CreateMilestoneDto ): Promise { @@ -349,6 +367,10 @@ export class ModelerApiClient { .then(this.decodeResponseOrThrow) as Promise } + /** + * + * @throws {RESTError} + */ async getMilestone(milestoneId: string): Promise { const headers = await this.getHeaders() return this.rest(`milestones/${milestoneId}`, { @@ -358,6 +380,7 @@ export class ModelerApiClient { /** * Deletion of resources is recursive and cannot be undone. + * @throws {RESTError} */ async deleteMilestone(milestoneId: string) { const headers = await this.getHeaders() @@ -368,6 +391,7 @@ export class ModelerApiClient { /** * Returns a link to a visual comparison between two milestones where the milestone referenced by milestone1Id acts as a baseline to compare the milestone referenced by milestone2Id against. + * @throws {RESTError} */ async getMilestoneComparison( milestone1Id: string, @@ -401,6 +425,7 @@ export class ModelerApiClient { * page specifies the page number to return. * * size specifies the number of items per page. The default value is 10. + * @throws {RESTError} */ async searchMilestones( req: Dto.PubSearchDtoMilestoneMetadataDto @@ -418,6 +443,7 @@ export class ModelerApiClient { /** * Creates a new project. This project will be created without any collaborators, so it will not be visible in the UI by default. To assign collaborators, use `addCollaborator()`. + * @throws {RESTError} */ async createProject(name: string): Promise { const headers = await this.getHeaders() @@ -429,6 +455,11 @@ export class ModelerApiClient { .then(this.decodeResponseOrThrow) as Promise } + /** + * + * @throws {RESTError} + * @returns + */ async getProject(projectId: string): Promise { const headers = await this.getHeaders() return this.rest(`projects/${projectId}`, { @@ -437,7 +468,8 @@ export class ModelerApiClient { } /** - * This endpoint deletes an empty project. A project is considered empty if there are no files in it. Deletion of resources is recursive and cannot be undone. + * @description This endpoint deletes an empty project. A project is considered empty if there are no files in it. Deletion of resources is recursive and cannot be undone. + * @throws {RESTError} */ async deleteProject(projectId: string) { const headers = await this.getHeaders() @@ -448,6 +480,10 @@ export class ModelerApiClient { .then(this.decodeResponseOrThrow) } + /** + * + * @throws {RESTError} + */ async renameProject( projectId: string, name: string @@ -484,6 +520,7 @@ export class ModelerApiClient { * page specifies the page number to return. * * size specifies the number of items per page. The default value is 10. + * @throws {RESTError} */ async searchProjects( req: Dto.PubSearchDtoProjectMetadataDto diff --git a/src/operate/lib/OperateApiClient.ts b/src/operate/lib/OperateApiClient.ts index ae61dbc7..27462765 100644 --- a/src/operate/lib/OperateApiClient.ts +++ b/src/operate/lib/OperateApiClient.ts @@ -8,6 +8,8 @@ import { RequireConfiguration, constructOAuthProvider, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, losslessParse, losslessStringify, } from '../../lib' @@ -67,6 +69,7 @@ export class OperateApiClient { * ``` * const operate = new OperateApiClient() * ``` + * @throws {RESTError} An error that may occur during API operations. */ constructor(options?: { config?: DeepPartial @@ -92,26 +95,9 @@ export class OperateApiClient { https: { certificateAuthority, }, - handlers: [ - (options, next) => { - if (Object.isFrozen(options.context)) { - options.context = { ...options.context } - } - Error.captureStackTrace(options.context) - - return next(options) - }, - ], + handlers: [gotErrorHandler], hooks: { - beforeError: [ - (error) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(error as any).source = (error as any).options.context.stack.split( - '\n' - ) - return error - }, - ], + beforeError: [gotBeforeErrorHook], }, }) this.tenantId = config.CAMUNDA_TENANT_ID @@ -154,6 +140,7 @@ export class OperateApiClient { * @description Search and retrieve process definitions. * * [Camunda 8 Documentation](https://docs.camunda.io/docs/apis-clients/operate-api/#process-definition) + * @throws {RESTError} * @example * ``` * const query: Query = { @@ -187,7 +174,7 @@ export class OperateApiClient { /** * * @description Retrieve the metadata for a specific process definition, by key. - * + * @throws {RESTError} * [Camunda 8 Documentation](https://docs.camunda.io/docs/apis-clients/operate-api/#process-definition) * @example * ``` @@ -213,6 +200,10 @@ export class OperateApiClient { }).text() } + /** + * + * @throws {RESTError} + */ public async searchDecisionDefinitions( query: Query ): Promise> { @@ -225,6 +216,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async getDecisionDefinition( decisionDefinitionKey: number | string ): Promise { @@ -235,6 +229,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async searchDecisionInstances( query: Query ): Promise> { @@ -247,6 +244,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async getDecisionInstance( decisionInstanceKey: number | string ): Promise { @@ -258,6 +258,7 @@ export class OperateApiClient { } /** * @description Search and retrieve process instances. + * @throws {RESTError} * @example * ``` * const operate = new OperateApiClient() @@ -298,6 +299,7 @@ export class OperateApiClient { /** * * @description Retrieve a specific process instance by id. + * @throws {RESTError} * @example * ``` * const operate = new OperateApiClient() @@ -315,6 +317,7 @@ export class OperateApiClient { /** * @description Delete a specific process instance by key. + * @throws {RESTError} * @example * ``` * const operate = new OperateApiClient() @@ -339,6 +342,7 @@ export class OperateApiClient { /** * @description Get the statistics for a process instance, grouped by flow nodes + * @throws {RESTError} */ public async getProcessInstanceStatistics( processInstanceKey: number | string @@ -352,6 +356,7 @@ export class OperateApiClient { /** * @description Get sequence flows of process instance by key + * @throws {RESTError} */ public async getProcessInstanceSequenceFlows( processInstanceKey: number | string @@ -365,6 +370,7 @@ export class OperateApiClient { /** * * @description Search and retrieve incidents. + * @throws {RESTError} * @example * ``` * const operate = new OperateApiClient() @@ -400,6 +406,7 @@ export class OperateApiClient { /** * * @description Retrieve an incident by incident key. + * @throws {RESTError} * @example * ``` * const operate = new OperateApiClient() @@ -415,6 +422,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async searchFlownodeInstances( query: Query ): Promise> { @@ -429,6 +439,9 @@ export class OperateApiClient { .json() } + /** + * @throws {RESTError} + */ public async getFlownodeInstance( key: number | string ): Promise { @@ -439,6 +452,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async searchVariables( query: Query ): Promise> { @@ -455,6 +471,7 @@ export class OperateApiClient { /** * @description Retrieve the variables for a Process Instance, given its key + * @throws {RESTError} * @param processInstanceKey * @returns */ @@ -478,7 +495,7 @@ export class OperateApiClient { /** * @description Retrieve the variables for a Process Instance as an object, given its key * @param processInstanceKey - * @returns + * @throws {RESTError} */ public async getJSONVariablesforProcess( processInstanceKey: number | string @@ -518,6 +535,7 @@ export class OperateApiClient { /** * * @description Return a variable identified by its variable key + * @throws {RESTError} * @returns */ public async getVariables(variableKey: number | string): Promise { @@ -527,6 +545,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async searchDecisionRequirements(query: Query) { const headers = await this.getHeaders() const json = this.addTenantIdToFilter(query) @@ -549,6 +570,9 @@ export class OperateApiClient { }).json() } + /** + * @throws {RESTError} + */ public async getDecisionRequirementsXML(key: string | number) { const headers = await this.getHeaders() return this.rest(`drd/${key}/xml`, { diff --git a/src/optimize/lib/OptimizeApiClient.ts b/src/optimize/lib/OptimizeApiClient.ts index 7e9383a0..7f3eed5b 100644 --- a/src/optimize/lib/OptimizeApiClient.ts +++ b/src/optimize/lib/OptimizeApiClient.ts @@ -8,6 +8,8 @@ import { RequireConfiguration, constructOAuthProvider, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, } from '../../lib' import { IOAuthProvider } from '../../oauth' @@ -25,6 +27,7 @@ import { ReportResults } from './ReportResults' /** * @description The high-level API client for Optimize. + * @throws {RESTError} If the request fails * @example * ``` * const optimize = new OptimizeApiClient() @@ -73,25 +76,9 @@ export class OptimizeApiClient { https: { certificateAuthority, }, - handlers: [ - (options, next) => { - if (Object.isFrozen(options.context)) { - options.context = { ...options.context } - } - Error.captureStackTrace(options.context) - return next(options) - }, - ], + handlers: [gotErrorHandler], hooks: { - beforeError: [ - (error) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(error as any).source = (error as any).options.context.stack.split( - '\n' - ) - return error - }, - ], + beforeError: [gotBeforeErrorHook], }, }) } @@ -121,6 +108,7 @@ export class OptimizeApiClient { * If sharing had been previously enabled and then disabled, re-enabling sharing will allow users to access previously shared URLs under the same address as before. Calling this endpoint when sharing is already enabled will have no effect. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/configuration/enable-sharing/) + * @throws {RESTError} If the request fails * @example * ```typescript * const client = new OptimizeApiClient() @@ -143,6 +131,7 @@ export class OptimizeApiClient { * When sharing is disabled, previously shared URLs will no longer be accessible. Upon re-enabling sharing, the previously shared URLs will work once again under the same address as before. Calling this endpoint when sharing is already disabled will have no effect. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/configuration/disable-sharing/) + * @throws {RESTError} If the request fails * @example * ```typescript * const client = new OptimizeApiClient() @@ -164,7 +153,7 @@ export class OptimizeApiClient { * The response contains a list of IDs of the dashboards existing in the collection with the given collection ID. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/dashboard/get-dashboard-ids/) - * + * @throws {RESTError} If the request fails * @param collectionId The ID of the collection for which to retrieve the dashboard IDs. * @example * ``` @@ -197,6 +186,7 @@ export class OptimizeApiClient { * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/dashboard/export-dashboard-definitions/) * + * @throws {RESTError} If the request fails * @param dashboardIds Array of dashboard ids * @example * ``` @@ -220,7 +210,7 @@ export class OptimizeApiClient { * @description This API allows users to retrieve all report IDs from a given collection. The response contains a list of IDs of the reports existing in the collection with the given collection ID. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/report/get-report-ids/) - * + * @throws {RESTError} If the request fails * @param collectionId the id of the collection * @example * ``` @@ -239,7 +229,7 @@ export class OptimizeApiClient { * @description The report deletion API allows you to delete reports by ID from Optimize. * * [Camunda 8 documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/report/delete-report/) - * + * @throws {RESTError} If the request fails * @param reportId The ID of the report you wish to delete * * @example @@ -263,7 +253,7 @@ export class OptimizeApiClient { * The obtained list of entity exports can be imported into other Optimize systems either using the dedicated import API or via UI. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/report/export-report-definitions/) - * + * @throws {RESTError} If the request fails * @param reportIds array of report IDs * @example * ``` @@ -288,6 +278,7 @@ export class OptimizeApiClient { * @param reportId * @param limit * @param paginationTimeoutSec + * @throws {RESTError} If the request fails * @example * ``` * const client = new OptimizeApiClient() @@ -325,6 +316,7 @@ export class OptimizeApiClient { * * Especially if this data changes over time, it is advisable to use this REST API to persist external variable updates to Optimize, as otherwise Optimize may not be aware of data changes in the external system. * @param variables + * @throws {RESTError} If the request fails * @example * ``` * const variables = [ @@ -363,7 +355,7 @@ export class OptimizeApiClient { * @description The purpose of Health-Readiness REST API is to return information indicating whether Optimize is ready to be used. * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/health-readiness/) - * + * @throws {RESTError} If the request fails * @example * ``` * const client = new OptimizeApiClient() @@ -387,6 +379,7 @@ export class OptimizeApiClient { * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/import-entities/) * + * @throws {RESTError} If the request fails * @example * ``` * const entities = [ @@ -433,6 +426,7 @@ export class OptimizeApiClient { * * [Camunda 8 Documentation](https://docs.camunda.io/optimize/apis-clients/optimize-api/variable-labeling/) * + * @throws {RESTError} If the request fails * @example * ``` * const variableLabels = { diff --git a/src/tasklist/lib/TasklistApiClient.ts b/src/tasklist/lib/TasklistApiClient.ts index 4b31ee9a..74cf7124 100644 --- a/src/tasklist/lib/TasklistApiClient.ts +++ b/src/tasklist/lib/TasklistApiClient.ts @@ -9,6 +9,8 @@ import { RequireConfiguration, constructOAuthProvider, createUserAgentString, + gotBeforeErrorHook, + gotErrorHandler, losslessParse, losslessStringify, } from '../../lib' @@ -77,25 +79,9 @@ export class TasklistApiClient { https: { certificateAuthority, }, - handlers: [ - (options, next) => { - if (Object.isFrozen(options.context)) { - options.context = { ...options.context } - } - Error.captureStackTrace(options.context) - return next(options) - }, - ], + handlers: [gotErrorHandler], hooks: { - beforeError: [ - (error) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(error as any).source = (error as any).options.context.stack.split( - '\n' - ) - return error - }, - ], + beforeError: [gotBeforeErrorHook], }, }) trace(`prefixUrl: ${prefixUrl}`) @@ -136,6 +122,7 @@ export class TasklistApiClient { /** * @description Query Tasklist for a list of tasks. See the [API documentation](https://docs.camunda.io/docs/apis-clients/tasklist-api/queries/tasks/). * @throws Status 400 - An error is returned when more than one search parameters among [`searchAfter`, `searchAfterOrEqual`, `searchBefore`, `searchBeforeOrEqual`] are present in request + * @throws {RESTError} * @example * ``` * const tasklist = new TasklistApiClient() @@ -173,7 +160,7 @@ export class TasklistApiClient { /** * @description Return a task by id, or throw if not found. - * @throws Will throw if no task of the given taskId exists + * @throws {RESTError} Will throw if no task of the given taskId exists * @returns */ public async getTask(taskId: string): Promise { @@ -187,6 +174,7 @@ export class TasklistApiClient { /** * @description Get the form details by form id and processDefinitionKey. + * @throws {RESTError} */ public async getForm( formId: string, @@ -208,7 +196,7 @@ export class TasklistApiClient { /** * @description This method returns a list of task variables for the specified taskId and variableNames. If the variableNames parameter is empty, all variables associated with the task will be returned. - * @throws Status 404 - An error is returned when the task with the taskId is not found. + * @throws {RESTError} */ public async getVariables({ taskId, @@ -246,6 +234,7 @@ export class TasklistApiClient { /** * @description Assign a task with taskId to assignee or the active user. + * @throws {RESTError} * @throws Status 400 - An error is returned when the task is not active (not in the CREATED state). * Status 400 - An error is returned when task was already assigned, except the case when JWT authentication token used and allowOverrideAssignment = true. * Status 403 - An error is returned when user doesn't have the permission to assign another user to this task. @@ -275,6 +264,7 @@ export class TasklistApiClient { /** * @description Complete a task with taskId and optional variables + * @throws {RESTError} * @throws Status 400 An error is returned when the task is not active (not in the CREATED state). * @throws Status 400 An error is returned if the task was not claimed (assigned) before. * @throws Status 400 An error is returned if the task is not assigned to the current user. @@ -302,6 +292,7 @@ export class TasklistApiClient { * @throws Status 400 An error is returned when the task is not active (not in the CREATED state). * @throws Status 400 An error is returned if the task was not claimed (assigned) before. * @throws Status 404 An error is returned when the task with the taskId is not found. + * @throws {RESTError} */ public async unassignTask(taskId: string): Promise { const headers = await this.getHeaders()