diff --git a/.vscode/launch.json b/.vscode/launch.json index 521b79d7..3095ba5c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}\\bin\\run", + "program": "${workspaceFolder}/bin/run.js", "args": [ "changed-elements", "enable", @@ -25,7 +25,7 @@ "skipFiles": [ "/**" ], - "program": "${workspaceFolder}\\bin\\run", + "program": "${workspaceFolder}/bin/run.js", "args": [ "docs-generator" ] diff --git a/docs/itwin/share/create.md b/docs/itwin/share/create.md new file mode 100644 index 00000000..75caf729 --- /dev/null +++ b/docs/itwin/share/create.md @@ -0,0 +1,29 @@ +# itp itwin share create + +Create a new iTwin Share. + +## Options + +- **`-i, --itwin-id`** + The ID of the iTwin to be shared. + **Type:** `string` **Required:** Yes + +- **`--contract`** + The name of share contract. Defaults to 'Default' name if omitted. + **Type:** `string` **Required:** No + +- **`--expiration`** + The expiration date for the share. Defaults to the maximum allowed period for the given share contract if omitted + **Type:** `string` **Required:** No + +## Examples + +```bash +itp itwin share create --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 + +itp itwin share create --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --contract Default --expiration 2025-12-31T23:59:59Z +``` + +## API Reference + +[Create iTwin Share](https://developer.bentley.com/apis/access-control-v2/operations/create-itwin-share/) \ No newline at end of file diff --git a/docs/itwin/share/info.md b/docs/itwin/share/info.md new file mode 100644 index 00000000..6be320f1 --- /dev/null +++ b/docs/itwin/share/info.md @@ -0,0 +1,23 @@ +# itp itwin share info + +Retrieves the specified iTwin Share for the specified iTwin. + +## Options + +- **`-i, --itwin-id`** + The ID of the iTwin. + **Type:** `string` **Required:** Yes + +- **`--share-id`** + iTwin Share ID. + **Type:** `string` **Required:** Yes + +## Examples + +```bash +itp itwin share info --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --share-id f012944d-417f-436c-8e9c-ddc70c7a338b +``` + +## API Reference + +[Get iTwin Share](https://developer.bentley.com/apis/access-control-v2/operations/get-itwin-share/) \ No newline at end of file diff --git a/docs/itwin/share/list.md b/docs/itwin/share/list.md new file mode 100644 index 00000000..2e8d61ef --- /dev/null +++ b/docs/itwin/share/list.md @@ -0,0 +1,19 @@ +# itp itwin share list + +Retrieves a list of available iTwin shares that are currently active for a specified iTwin. + +## Options + +- **`-i, --itwin-id`** + The ID of the iTwin. + **Type:** `string` **Required:** Yes + +## Examples + +```bash +itp itwin share list --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 +``` + +## API Reference + +[Get a list of created iTwin Shares](https://developer.bentley.com/apis/access-control-v2/operations/get-itwin-shares/) \ No newline at end of file diff --git a/docs/itwin/share/revoke.md b/docs/itwin/share/revoke.md new file mode 100644 index 00000000..f92d9411 --- /dev/null +++ b/docs/itwin/share/revoke.md @@ -0,0 +1,23 @@ +# itp itwin share revoke + +Revokes a specified share for a specified iTwin. Any future requests made with the associated shareKey will no longer work. + +## Options + +- **`-i, --itwin-id`** + The ID of the iTwin to be shared. + **Type:** `string` **Required:** Yes + +- **`--share-id`** + iTwin Share ID. + **Type:** `string` **Required:** Yes + +## Examples + +```bash +itp itwin share revoke --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --share-id bf4d8b36-25d7-4b72-b38b-12c1f0325f42 +``` + +## API Reference + +[Revokes a specified share for a specified iTwin.](https://developer.bentley.com/apis/access-control-v2/operations/revoke-itwin-share/) \ No newline at end of file diff --git a/integration-tests/itwin/itwin.test.ts b/integration-tests/itwin/itwin.test.ts index a779bbcb..9b63d9a1 100644 --- a/integration-tests/itwin/itwin.test.ts +++ b/integration-tests/itwin/itwin.test.ts @@ -9,6 +9,7 @@ import deleteTests from "./delete.test"; import infoTests from "./info.test"; import listTests from "./list.test"; import repositoryTests from "./repository.test"; +import shareTests from "./share.test"; import updateTests from "./update.test"; const tests = () => @@ -19,6 +20,7 @@ const tests = () => updateTests(); deleteTests(); repositoryTests(); + shareTests(); }); export default tests; diff --git a/integration-tests/itwin/share.test.ts b/integration-tests/itwin/share.test.ts new file mode 100644 index 00000000..d9de2dee --- /dev/null +++ b/integration-tests/itwin/share.test.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from "chai"; + +import { runCommand } from "@oclif/test"; + +import { ItwinShare } from "../../src/services/access-control/models/itwin-share.js"; +import { ResultResponse } from "../../src/services/general-models/result-response.js"; +import { createITwin } from "../utils/helpers"; +import runSuiteIfMainModule from "../utils/run-suite-if-main-module"; + +function assertItwinShare(share?: ItwinShare): ItwinShare { + expect(share).to.be.instanceOf(Object); + expect(share).to.have.property("id"); + expect(share).to.have.property("iTwinId"); + expect(share).to.have.property("shareKey"); + expect(share).to.have.property("shareContract"); + expect(share).to.have.property("expiration"); + expect(share!.shareContract).equals("Default"); + expect(share!.expiration).match(/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/); + return share!; +} + +const tests = () => + describe("share", () => { + let testITwinId: string; + let shareId: string; + + before(async () => { + const testITwin = await createITwin(`cli-itwin-integration-test-${new Date().toISOString()}`, "Thing", "Asset"); + testITwinId = testITwin.id as string; + + // Create iTwin Share + const { result: iTwinShare, error } = await runCommand(`itwin share create --itwin-id ${testITwinId}`); + expect(error).to.be.undefined; + const share = assertItwinShare(iTwinShare); + shareId = share.id; + }); + + after(async () => { + const { result: deleteResult } = await runCommand(`itwin delete --itwin-id ${testITwinId}`); + expect(deleteResult).to.have.property("result", "deleted"); + }); + + it("should get specific iTwin share info", async () => { + const { result: iTwinShare, error } = await runCommand(`itwin share info --itwin-id ${testITwinId} --share-id ${shareId}`); + expect(error).to.be.undefined; + assertItwinShare(iTwinShare); + }); + + it("should get a list of iTwin shares", async () => { + const { result: iTwinShares, error } = await runCommand(`itwin share list --itwin-id ${testITwinId}`); + expect(error).to.be.undefined; + expect(iTwinShares).to.be.instanceOf(Array); + expect(iTwinShares).to.have.lengthOf(1); + assertItwinShare(iTwinShares![0]); + }); + + it("should return error if share does not exist", async () => { + const invalidShareId = "bf4d8b36-25d7-4b72-b38b-12c1f0325f42"; + const { result: iTwinShare, error } = await runCommand(`itwin share revoke --itwin-id ${testITwinId} --share-id ${invalidShareId}`); + expect(iTwinShare).to.be.undefined; + expect(error?.message).to.contain("ShareNotFound"); + }); + + it("should create and revoke iTwin share", async () => { + // Create iTwin Share + const { result: iTwinShare, error: createError } = await runCommand(`itwin share create --itwin-id ${testITwinId}`); + expect(createError).to.be.undefined; + const share = assertItwinShare(iTwinShare); + + // Revoke iTwin share + const { result: revokeResult, error: revokeError } = await runCommand( + `itwin share revoke --itwin-id ${testITwinId} --share-id ${share.id}`, + ); + expect(revokeError).to.be.undefined; + expect(revokeResult).to.have.property("result", "revoked"); + + // Verify the share has been revoked + const { result: getResult, error: getError } = await runCommand(`itwin share info --itwin-id ${testITwinId} --share-id ${share.id}`); + expect(getResult).to.be.undefined; + expect(getError?.message).to.contain("ShareNotFound"); + }); + + it("should not create iTwin share with too long expiration period", async () => { + const { result: iTwinShare, error } = await runCommand(`itwin share create --itwin-id ${testITwinId} --expiration 2100-01-01`); + expect(iTwinShare).to.be.undefined; + expect(error?.message).to.contain("InvalidShareRequest"); + expect(error?.message).to.contain("Value outside of valid range."); + }); + + it("should not create iTwin share with non-existing contract name", async () => { + const { result: iTwinShare, error } = await runCommand(`itwin share create --itwin-id ${testITwinId} --contract InvalidContractName`); + expect(iTwinShare).to.be.undefined; + expect(error?.message).to.contain("ShareContractNotFound"); + expect(error?.message).to.contain("Requested share contract is not available."); + }); + }); + +export default tests; + +runSuiteIfMainModule(import.meta, tests); diff --git a/mocked-integration-tests/access-control/group/add.test.ts b/mocked-integration-tests/access-control/group/add.test.ts index dacc2d2f..e7fc131e 100644 --- a/mocked-integration-tests/access-control/group/add.test.ts +++ b/mocked-integration-tests/access-control/group/add.test.ts @@ -38,7 +38,7 @@ const tests = () => `access-control group create --itwin-id ${iTwinId} --name "${groupName}" --description "${groupDescription}"`, ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/group/delete.test.ts b/mocked-integration-tests/access-control/group/delete.test.ts index b00f8722..dd739b84 100644 --- a/mocked-integration-tests/access-control/group/delete.test.ts +++ b/mocked-integration-tests/access-control/group/delete.test.ts @@ -28,7 +28,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control group delete --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when group is not found", async () => { @@ -36,7 +36,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control group delete --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/group/info.test.ts b/mocked-integration-tests/access-control/group/info.test.ts index abda6aca..2ab4a926 100644 --- a/mocked-integration-tests/access-control/group/info.test.ts +++ b/mocked-integration-tests/access-control/group/info.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control group info --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when group is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control group info --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/group/list.test.ts b/mocked-integration-tests/access-control/group/list.test.ts index 507898f5..025c034c 100644 --- a/mocked-integration-tests/access-control/group/list.test.ts +++ b/mocked-integration-tests/access-control/group/list.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: createError } = await runCommand(`access-control group list --itwin-id ${iTwinId}`); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/group/update.test.ts b/mocked-integration-tests/access-control/group/update.test.ts index 50c35f77..a6dcc7da 100644 --- a/mocked-integration-tests/access-control/group/update.test.ts +++ b/mocked-integration-tests/access-control/group/update.test.ts @@ -45,7 +45,7 @@ const tests = () => `access-control group update --itwin-id ${iTwinId} --group-id ${groupId} --name "${groupName}" --description "${groupDescription}" --member ${members[0].email} --ims-group "${imsGroups[0]}"`, ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/group/add.test.ts b/mocked-integration-tests/access-control/member/group/add.test.ts index fb838a1a..faafbc0c 100644 --- a/mocked-integration-tests/access-control/member/group/add.test.ts +++ b/mocked-integration-tests/access-control/member/group/add.test.ts @@ -55,7 +55,7 @@ const tests = () => ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when provided iTwin is not found", async () => { @@ -72,7 +72,7 @@ const tests = () => ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when provided group is not found", async () => { @@ -89,7 +89,7 @@ const tests = () => ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/group/delete.test.ts b/mocked-integration-tests/access-control/member/group/delete.test.ts index f63bf9c4..587ffc41 100644 --- a/mocked-integration-tests/access-control/member/group/delete.test.ts +++ b/mocked-integration-tests/access-control/member/group/delete.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member group delete --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when iTwin is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member group delete --itwin-id ${iTwinId} --group-id ${groupId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/group/info.test.ts b/mocked-integration-tests/access-control/member/group/info.test.ts index 0f6c5fc9..d866fdf1 100644 --- a/mocked-integration-tests/access-control/member/group/info.test.ts +++ b/mocked-integration-tests/access-control/member/group/info.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: infoError } = await runCommand(`access-control member group info --itwin-id ${iTwinId} --group-id ${groupId}`); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when provided user member is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: infoError } = await runCommand(`access-control member group info --itwin-id ${iTwinId} --group-id ${groupId}`); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/group/list.test.ts b/mocked-integration-tests/access-control/member/group/list.test.ts index 777d24f0..75af58ba 100644 --- a/mocked-integration-tests/access-control/member/group/list.test.ts +++ b/mocked-integration-tests/access-control/member/group/list.test.ts @@ -28,7 +28,7 @@ const tests = () => const { error: listError } = await runCommand(`access-control member group list --itwin-id ${iTwinId}`); expect(listError).to.not.be.undefined; - expect(listError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(listError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/group/update.test.ts b/mocked-integration-tests/access-control/member/group/update.test.ts index e9459bbf..509bd29d 100644 --- a/mocked-integration-tests/access-control/member/group/update.test.ts +++ b/mocked-integration-tests/access-control/member/group/update.test.ts @@ -34,7 +34,7 @@ const tests = () => `access-control member group update -i ${iTwinId} --group-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error if provided member is not found", async () => { @@ -44,7 +44,7 @@ const tests = () => `access-control member group update -i ${iTwinId} --group-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error if provided role is not found", async () => { @@ -54,7 +54,7 @@ const tests = () => `access-control member group update -i ${iTwinId} --group-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/owner/add.test.ts b/mocked-integration-tests/access-control/member/owner/add.test.ts index 921b7d11..d42b1a07 100644 --- a/mocked-integration-tests/access-control/member/owner/add.test.ts +++ b/mocked-integration-tests/access-control/member/owner/add.test.ts @@ -37,7 +37,7 @@ const tests = () => const { error } = await runCommand(`access-control member owner add --itwin-id ${iTwinId} --email ${emailToAdd}`); expect(error).to.not.be.undefined; - expect(error?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(error?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/owner/delete.test.ts b/mocked-integration-tests/access-control/member/owner/delete.test.ts index 0c45f02d..15dc4331 100644 --- a/mocked-integration-tests/access-control/member/owner/delete.test.ts +++ b/mocked-integration-tests/access-control/member/owner/delete.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member owner delete --itwin-id ${iTwinId} --member-id ${memberId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when iTwin is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member owner delete --itwin-id ${iTwinId} --member-id ${memberId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/owner/list.test.ts b/mocked-integration-tests/access-control/member/owner/list.test.ts index f265c15e..9785600e 100644 --- a/mocked-integration-tests/access-control/member/owner/list.test.ts +++ b/mocked-integration-tests/access-control/member/owner/list.test.ts @@ -28,7 +28,7 @@ const tests = () => const { error: listError } = await runCommand(`access-control member owner list --itwin-id ${iTwinId}`); expect(listError).to.not.be.undefined; - expect(listError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(listError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/user/add.test.ts b/mocked-integration-tests/access-control/member/user/add.test.ts index f9457bca..8aab9286 100644 --- a/mocked-integration-tests/access-control/member/user/add.test.ts +++ b/mocked-integration-tests/access-control/member/user/add.test.ts @@ -80,7 +80,7 @@ const tests = () => ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when provided iTwin is not found", async () => { @@ -97,7 +97,7 @@ const tests = () => ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/user/delete.test.ts b/mocked-integration-tests/access-control/member/user/delete.test.ts index 0c7c7ce9..f6a083c7 100644 --- a/mocked-integration-tests/access-control/member/user/delete.test.ts +++ b/mocked-integration-tests/access-control/member/user/delete.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member user delete --itwin-id ${iTwinId} --member-id ${memberId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when iTwin is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control member user delete --itwin-id ${iTwinId} --member-id ${memberId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/user/info.test.ts b/mocked-integration-tests/access-control/member/user/info.test.ts index 633859f7..e1d056b9 100644 --- a/mocked-integration-tests/access-control/member/user/info.test.ts +++ b/mocked-integration-tests/access-control/member/user/info.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: infoError } = await runCommand(`access-control member user info --itwin-id ${iTwinId} --member-id ${memberId}`); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when provided user member is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: infoError } = await runCommand(`access-control member user info --itwin-id ${iTwinId} --member-id ${memberId}`); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/user/list.test.ts b/mocked-integration-tests/access-control/member/user/list.test.ts index 01fa6698..a6fd4feb 100644 --- a/mocked-integration-tests/access-control/member/user/list.test.ts +++ b/mocked-integration-tests/access-control/member/user/list.test.ts @@ -28,7 +28,7 @@ const tests = () => const { error: listError } = await runCommand(`access-control member user list --itwin-id ${iTwinId}`); expect(listError).to.not.be.undefined; - expect(listError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(listError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/member/user/update.test.ts b/mocked-integration-tests/access-control/member/user/update.test.ts index 7e45d908..542d1bb0 100644 --- a/mocked-integration-tests/access-control/member/user/update.test.ts +++ b/mocked-integration-tests/access-control/member/user/update.test.ts @@ -34,7 +34,7 @@ const tests = () => `access-control member user update -i ${iTwinId} --member-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error if provided member is not found", async () => { @@ -44,7 +44,7 @@ const tests = () => `access-control member user update -i ${iTwinId} --member-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error if provided role is not found", async () => { @@ -54,7 +54,7 @@ const tests = () => `access-control member user update -i ${iTwinId} --member-id ${memberId} --role-id ${roleIds[0]} --role-id ${roleIds[1]} --role-id ${roleIds[2]}`, ); expect(updateError).to.not.be.undefined; - expect(updateError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(updateError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/permissions.test.ts b/mocked-integration-tests/access-control/permissions.test.ts index 451ddb49..5d98876c 100644 --- a/mocked-integration-tests/access-control/permissions.test.ts +++ b/mocked-integration-tests/access-control/permissions.test.ts @@ -28,7 +28,7 @@ const tests = () => const { error } = await runCommand(`access-control permissions me --itwin-id ${iTwinId}`); expect(error).to.not.be.undefined; - expect(error?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(error?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should list all permissions", async () => { diff --git a/mocked-integration-tests/access-control/roles/add.test.ts b/mocked-integration-tests/access-control/roles/add.test.ts index 68083937..3bb723d9 100644 --- a/mocked-integration-tests/access-control/roles/add.test.ts +++ b/mocked-integration-tests/access-control/roles/add.test.ts @@ -38,7 +38,7 @@ const tests = () => `access-control role create --itwin-id ${iTwinId} --name "${roleName}" --description "${roleDescription}"`, ); expect(createError).to.not.be.undefined; - expect(createError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(createError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/roles/delete.test.ts b/mocked-integration-tests/access-control/roles/delete.test.ts index 4c17b713..23e5f8be 100644 --- a/mocked-integration-tests/access-control/roles/delete.test.ts +++ b/mocked-integration-tests/access-control/roles/delete.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control role delete --itwin-id ${iTwinId} --role-id ${roleId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when role is not found", async () => { @@ -37,7 +37,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control role delete --itwin-id ${iTwinId} --role-id ${roleId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/roles/info.test.ts b/mocked-integration-tests/access-control/roles/info.test.ts index c6b0adfc..8c5185b2 100644 --- a/mocked-integration-tests/access-control/roles/info.test.ts +++ b/mocked-integration-tests/access-control/roles/info.test.ts @@ -30,7 +30,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control role info --itwin-id ${iTwinId} --role-id ${roleId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when role is not found", async () => { @@ -38,7 +38,7 @@ const tests = () => const { error: deleteError } = await runCommand(`access-control role info --itwin-id ${iTwinId} --role-id ${roleId}`); expect(deleteError).to.not.be.undefined; - expect(deleteError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(deleteError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/roles/list.test.ts b/mocked-integration-tests/access-control/roles/list.test.ts index 6dfc4bb2..a7045bf4 100644 --- a/mocked-integration-tests/access-control/roles/list.test.ts +++ b/mocked-integration-tests/access-control/roles/list.test.ts @@ -29,7 +29,7 @@ const tests = () => const { error: infoError } = await runCommand(`access-control role list --itwin-id ${iTwinId}`); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/mocked-integration-tests/access-control/roles/update.test.ts b/mocked-integration-tests/access-control/roles/update.test.ts index 235f7838..a5387e10 100644 --- a/mocked-integration-tests/access-control/roles/update.test.ts +++ b/mocked-integration-tests/access-control/roles/update.test.ts @@ -36,7 +36,7 @@ const tests = () => `access-control role update --itwin-id ${iTwinId} --role-id ${roleId} --name "${roleName}" --description "${roleDescription}" --permission ${permissions[0]} --permission ${permissions[1]} --permission ${permissions[2]}`, ); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when role is not found", async () => { @@ -46,7 +46,7 @@ const tests = () => `access-control role update --itwin-id ${iTwinId} --role-id ${roleId} --name "${roleName}" --description "${roleDescription}" --permission ${permissions[0]} --permission ${permissions[1]} --permission ${permissions[2]}`, ); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); it("should return an error when permission is not found", async () => { @@ -56,7 +56,7 @@ const tests = () => `access-control role update --itwin-id ${iTwinId} --role-id ${roleId} --name "${roleName}" --description "${roleDescription}" --permission ${permissions[0]} --permission ${permissions[1]} --permission ${permissions[2]}`, ); expect(infoError).to.not.be.undefined; - expect(infoError?.message).to.be.equal(`HTTP error! ${JSON.stringify(response)}`); + expect(infoError?.message).to.be.equal(JSON.stringify(response.error, null, 2)); }); }); diff --git a/src/commands/itwin/share/create.ts b/src/commands/itwin/share/create.ts new file mode 100644 index 00000000..4daf6604 --- /dev/null +++ b/src/commands/itwin/share/create.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { Flags } from "@oclif/core"; + +import { ApiReference } from "../../../extensions/api-reference.js"; +import BaseCommand from "../../../extensions/base-command.js"; +import { CustomFlags } from "../../../extensions/custom-flags.js"; +import { ItwinShare } from "../../../services/access-control/models/itwin-share.js"; + +export default class CreateItwinShare extends BaseCommand { + public static apiReference: ApiReference = { + link: "https://developer.bentley.com/apis/access-control-v2/operations/create-itwin-share/", + name: "Create iTwin Share", + }; + + public static description = "Create a new iTwin Share."; + + public static examples = [ + { + command: `<%= config.bin %> <%= command.id %> --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51`, + description: "Example 1:", + }, + { + command: `<%= config.bin %> <%= command.id %> --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --contract Default --expiration 2025-12-31T23:59:59Z`, + description: "Example 2:", + }, + ]; + + public static flags = { + "itwin-id": CustomFlags.iTwinIDFlag({ + description: "The ID of the iTwin to be shared.", + }), + contract: Flags.string({ + description: "The name of share contract. Defaults to 'Default' name if omitted.", + helpValue: "", + }), + expiration: Flags.string({ + description: "The expiration date for the share. Defaults to the maximum allowed period for the given share contract if omitted", + helpValue: "", + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(CreateItwinShare); + const service = await this.getAccessControlService(); + const result = await service.createiTwinShare(flags["itwin-id"], { + shareContract: flags.contract, + expiration: flags.expiration, + }); + return this.logAndReturnResult(result); + } +} diff --git a/src/commands/itwin/share/info.ts b/src/commands/itwin/share/info.ts new file mode 100644 index 00000000..f55c9095 --- /dev/null +++ b/src/commands/itwin/share/info.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { Flags } from "@oclif/core"; + +import { ApiReference } from "../../../extensions/api-reference.js"; +import BaseCommand from "../../../extensions/base-command.js"; +import { CustomFlags } from "../../../extensions/custom-flags.js"; +import { ItwinShare } from "../../../services/access-control/models/itwin-share.js"; + +export default class GetItwinShare extends BaseCommand { + public static apiReference: ApiReference = { + link: "https://developer.bentley.com/apis/access-control-v2/operations/get-itwin-share/", + name: "Get iTwin Share", + }; + + public static description = "Retrieves the specified iTwin Share for the specified iTwin."; + + public static examples = [ + { + command: `<%= config.bin %> <%= command.id %> --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --share-id f012944d-417f-436c-8e9c-ddc70c7a338b`, + description: "Example 1:", + }, + ]; + + public static flags = { + "itwin-id": CustomFlags.iTwinIDFlag({ + description: "The ID of the iTwin.", + }), + "share-id": Flags.string({ + description: "iTwin Share ID.", + helpValue: "", + required: true, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(GetItwinShare); + const service = await this.getAccessControlService(); + const result = await service.getiTwinShare(flags["itwin-id"], flags["share-id"]); + + return this.logAndReturnResult(result); + } +} diff --git a/src/commands/itwin/share/list.ts b/src/commands/itwin/share/list.ts new file mode 100644 index 00000000..b9b63e90 --- /dev/null +++ b/src/commands/itwin/share/list.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { ApiReference } from "../../../extensions/api-reference.js"; +import BaseCommand from "../../../extensions/base-command.js"; +import { CustomFlags } from "../../../extensions/custom-flags.js"; +import { ItwinShare } from "../../../services/access-control/models/itwin-share.js"; + +export default class ListItwinShare extends BaseCommand { + public static apiReference: ApiReference = { + link: "https://developer.bentley.com/apis/access-control-v2/operations/get-itwin-shares/", + name: "Get a list of created iTwin Shares", + }; + + public static description = "Retrieves a list of available iTwin shares that are currently active for a specified iTwin."; + + public static examples = [ + { + command: `<%= config.bin %> <%= command.id %> --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51`, + description: "Example 1:", + }, + ]; + + public static flags = { + "itwin-id": CustomFlags.iTwinIDFlag({ + description: "The ID of the iTwin.", + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(ListItwinShare); + const service = await this.getAccessControlService(); + const result = await service.getiTwinShares(flags["itwin-id"]); + + return this.logAndReturnResult(result); + } +} diff --git a/src/commands/itwin/share/revoke.ts b/src/commands/itwin/share/revoke.ts new file mode 100644 index 00000000..acc15674 --- /dev/null +++ b/src/commands/itwin/share/revoke.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +import { Flags } from "@oclif/core"; + +import { ApiReference } from "../../../extensions/api-reference.js"; +import BaseCommand from "../../../extensions/base-command.js"; +import { CustomFlags } from "../../../extensions/custom-flags.js"; +import { ResultResponse } from "../../../services/general-models/result-response.js"; + +export default class RevokeItwinShare extends BaseCommand { + public static apiReference: ApiReference = { + link: "https://developer.bentley.com/apis/access-control-v2/operations/revoke-itwin-share/", + name: "Revokes a specified share for a specified iTwin.", + }; + + public static description = "Revokes a specified share for a specified iTwin. Any future requests made with the associated shareKey will no longer work."; + + public static examples = [ + { + command: `<%= config.bin %> <%= command.id %> --itwin-id ad0ba809-9241-48ad-9eb0-c8038c1a1d51 --share-id bf4d8b36-25d7-4b72-b38b-12c1f0325f42`, + description: "Example 1:", + }, + ]; + + public static flags = { + "itwin-id": CustomFlags.iTwinIDFlag({ + description: "The ID of the iTwin to be shared.", + }), + "share-id": Flags.string({ + description: "iTwin Share ID.", + helpValue: "", + required: true, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(RevokeItwinShare); + const service = await this.getAccessControlService(); + const result = await service.deleteiTwinShare(flags["itwin-id"], flags["share-id"]); + + return this.logAndReturnResult(result); + } +} diff --git a/src/extensions/configuration.ts b/src/extensions/configuration.ts index 01cc6b4e..c0191460 100644 --- a/src/extensions/configuration.ts +++ b/src/extensions/configuration.ts @@ -25,12 +25,13 @@ export class Configuration { this.issuerUrl = configJson.issuerUrl; } - if (process.env.ITP_SERVICE_CLIENT_ID) { + if (process.env.ITP_SERVICE_CLIENT_ID && process.env.ITP_SERVICE_CLIENT_SECRET) { this.clientId = process.env.ITP_SERVICE_CLIENT_ID; - } - - if (process.env.ITP_SERVICE_CLIENT_SECRET) { this.clientSecret = process.env.ITP_SERVICE_CLIENT_SECRET; + } else { + if (process.env.ITP_NATIVE_CLIENT_ID) { + this.clientId = process.env.ITP_NATIVE_CLIENT_ID; + } } if (process.env.ITP_ISSUER_URL) { diff --git a/src/services/access-control/access-control-client.ts b/src/services/access-control/access-control-client.ts index e38e56b7..7165dac9 100644 --- a/src/services/access-control/access-control-client.ts +++ b/src/services/access-control/access-control-client.ts @@ -5,6 +5,7 @@ import { ITwinPlatformApiClient } from "../iTwin-platform-api-client.js"; import { Group, GroupResponse, GroupsResponse, GroupUpdate } from "./models/group.js"; +import { ItwinShareCreate, ItwinShareResponse, ItwinSharesResponse } from "./models/itwin-share.js"; import { Permissions } from "./models/permissions.js"; import { Role, RoleResponse, RolesResponse } from "./models/role.js"; @@ -32,6 +33,14 @@ export class AccessControlClient { }); } + public async createiTwinShare(iTwinId: string, share?: ItwinShareCreate): Promise { + return this._iTwinPlatformApiClient.sendRequest({ + apiPath: `accesscontrol/itwins/${iTwinId}/shares`, + body: share, + method: "POST", + }); + } + public async deleteGroup(iTwinId: string, groupId: string): Promise { await this._iTwinPlatformApiClient.sendRequestNoResponse({ apiPath: `accesscontrol/itwins/${iTwinId}/groups/${groupId}`, @@ -46,6 +55,13 @@ export class AccessControlClient { }); } + public async deleteiTwinShare(iTwinId: string, shareId: string): Promise { + await this._iTwinPlatformApiClient.sendRequestNoResponse({ + apiPath: `accesscontrol/itwins/${iTwinId}/shares/${shareId}`, + method: "DELETE", + }); + } + public async getAllAvailableiTwinPermissions(): Promise { return this._iTwinPlatformApiClient.sendRequest({ apiPath: `accesscontrol/itwins/permissions`, @@ -88,6 +104,20 @@ export class AccessControlClient { }); } + public async getiTwinShare(iTwinId: string, shareId: string): Promise { + return this._iTwinPlatformApiClient.sendRequest({ + apiPath: `accesscontrol/itwins/${iTwinId}/shares/${shareId}`, + method: "GET", + }); + } + + public async getiTwinShares(iTwinId: string): Promise { + return this._iTwinPlatformApiClient.sendRequest({ + apiPath: `accesscontrol/itwins/${iTwinId}/shares`, + method: "GET", + }); + } + public async updateGroup(iTwinId: string, groupId: string, group: GroupUpdate): Promise { return this._iTwinPlatformApiClient.sendRequest({ apiPath: `accesscontrol/itwins/${iTwinId}/groups/${groupId}`, diff --git a/src/services/access-control/access-control-service.ts b/src/services/access-control/access-control-service.ts index 9cb4a388..4bc1394f 100644 --- a/src/services/access-control/access-control-service.ts +++ b/src/services/access-control/access-control-service.ts @@ -7,6 +7,7 @@ import { LoggingCallbacks } from "../general-models/logging-callbacks.js"; import { ResultResponse } from "../general-models/result-response.js"; import { AccessControlClient } from "./access-control-client.js"; import { Group, GroupUpdate } from "./models/group.js"; +import { ItwinShare, ItwinShareCreate } from "./models/itwin-share.js"; import { Role } from "./models/role.js"; export class AccessControlService { @@ -103,4 +104,28 @@ export class AccessControlService { return response.role; } + + public async createiTwinShare(iTwinId: string, shareCreate?: ItwinShareCreate): Promise { + const response = await this._accessControlClient.createiTwinShare(iTwinId, shareCreate); + + return response.share; + } + + public async deleteiTwinShare(iTwinId: string, shareId: string): Promise { + await this._accessControlClient.deleteiTwinShare(iTwinId, shareId); + + return { result: "revoked" }; + } + + public async getiTwinShare(iTwinId: string, shareId: string): Promise { + const response = await this._accessControlClient.getiTwinShare(iTwinId, shareId); + + return response.share; + } + + public async getiTwinShares(iTwinId: string): Promise { + const response = await this._accessControlClient.getiTwinShares(iTwinId); + + return response.shares; + } } diff --git a/src/services/access-control/models/itwin-share.ts b/src/services/access-control/models/itwin-share.ts new file mode 100644 index 00000000..5d228d67 --- /dev/null +++ b/src/services/access-control/models/itwin-share.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Bentley Systems, Incorporated. All rights reserved. + * See LICENSE.md in the project root for license terms and full copyright notice. + *--------------------------------------------------------------------------------------------*/ + +export interface ItwinShareCreate { + shareContract?: string; + expiration?: string; +} + +export interface ItwinShareResponse { + share: ItwinShare; +} + +export interface ItwinSharesResponse { + shares: ItwinShare[]; +} + +export interface ItwinShare { + id: string; + iTwinId: string; + shareKey: string; + shareContract: string; + expiration: string; +} diff --git a/src/services/iTwin-platform-api-client.ts b/src/services/iTwin-platform-api-client.ts index 964746d0..51284205 100644 --- a/src/services/iTwin-platform-api-client.ts +++ b/src/services/iTwin-platform-api-client.ts @@ -90,13 +90,19 @@ export class ITwinPlatformApiClient { const response = await fetch(`${this._iTwinPlatformApiBasePath}/${apiPath}${queryString}`, fetchOptions); if (!response.ok) { + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + const errorText = await response.text(); + throw new Error(`HTTP error occurred (${response.status}).\n${errorText}`); + } + const errorResponseData = await response.json(); - try { - const typedError = errorResponseData as ErrorResponse; - const stringifiedError = JSON.stringify(typedError.error, Object.getOwnPropertyNames(typedError.error)); - throw new Error(`HTTP error! status: ${response.status}. Response data: ${stringifiedError}`); - } catch { - throw new Error(`HTTP error! ${JSON.stringify(errorResponseData)}`); + // Check if errorResponseData is directly of type Error + if (errorResponseData && typeof errorResponseData === "object" && "error" in errorResponseData) { + const directErrorString = JSON.stringify(errorResponseData.error, Object.getOwnPropertyNames(errorResponseData.error), 2); + throw new Error(directErrorString); + } else { + throw new Error(`HTTP error occurred (${response.status}).\n${JSON.stringify(errorResponseData, null, 2)}`); } } @@ -108,19 +114,3 @@ export class ITwinPlatformApiClient { return responseData as T; } } - -interface ErrorResponse { - error: Error; -} - -interface Error { - code: string; - details: ErrorDetails[]; - message: string; -} - -interface ErrorDetails { - code: string; - message: string; - target: string; -}