Skip to content

Commit d7df6d2

Browse files
Add putScene method for full scene replacement (#86)
1 parent 2209ff1 commit d7df6d2

File tree

8 files changed

+141
-1
lines changed

8 files changed

+141
-1
lines changed

.changeset/curly-chefs-bake.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@itwin/scenes-client": minor
3+
---
4+
5+
Add new client method `putScene(params: PutSceneParams)`
6+
7+
- Fully replaces an existing scene and all its objects in a single request. If the specified scene does not exist, it will be created.
8+
- Uses new types `SceneUpsert` and `PutSceneParams`

packages/scenes-client/src/api/sceneApi.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
GetSceneMetadataParams,
2222
isSceneMetadataResponse,
2323
SceneMetadataResponse,
24+
PutSceneParams,
2425
} from "../models/index.js";
2526
import { iteratePagedEndpoint } from "../utilities.js";
2627
import { callApi, AuthArgs } from "./apiFetch.js";
@@ -238,6 +239,50 @@ export async function postScene({
238239
});
239240
}
240241

242+
/**
243+
* Create or replace an existing scene.
244+
* @param params - {@link PutSceneParams}
245+
* @returns Created/updated scene details.
246+
* @throws {ScenesApiError} If the API call fails or the response format is invalid.
247+
*/
248+
export async function putScene({
249+
iTwinId,
250+
sceneId,
251+
scene,
252+
getAccessToken,
253+
baseUrl,
254+
}: PutSceneParams & AuthArgs): Promise<SceneResponse> {
255+
return callApi<SceneResponse>({
256+
endpoint: `/${sceneId}?iTwinId=${iTwinId}`,
257+
getAccessToken,
258+
baseUrl,
259+
postProcess: async (response) => {
260+
if (!response.ok) {
261+
await handleErrorResponse(response);
262+
}
263+
const responseJson = await response.json();
264+
if (!isSceneResponse(responseJson)) {
265+
throw new ScenesApiError(
266+
{
267+
code: "InvalidResponse",
268+
message: "Error creating or replacing scene: unexpected response format",
269+
},
270+
response.status,
271+
);
272+
}
273+
return responseJson;
274+
},
275+
fetchOptions: {
276+
method: "PUT",
277+
body: JSON.stringify(scene),
278+
},
279+
additionalHeaders: {
280+
Accept: "application/vnd.bentley.itwin-platform.v1+json",
281+
"Content-Type": "application/json",
282+
},
283+
});
284+
}
285+
241286
/**
242287
* Updates an existing scene's metadata.
243288
* @param params - {@link PatchSceneParams}

packages/scenes-client/src/client.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
deleteScene,
1111
getScenes,
1212
getSceneMetadata,
13+
putScene,
1314
} from "./api/sceneApi.js";
1415
import {
1516
postObjects,
@@ -46,6 +47,7 @@ import {
4647
PatchObjectParam,
4748
GetSceneMetadataParams,
4849
SceneMetadataResponse,
50+
PutSceneParams,
4951
} from "./models/index.js";
5052

5153
type AccessTokenFn = () => Promise<string>;
@@ -163,7 +165,25 @@ export class SceneClient {
163165
}
164166

165167
/**
166-
* Update an existing scene.
168+
* Create or replace an existing scene and all its objects.
169+
* @param params.iTwinId – The iTwin's unique identifier.
170+
* @param params.sceneId – The scene's unique identifier.
171+
* @param params.scene – The scene creation payload.
172+
* @returns SceneResponse containing the created/updated Scene's details.
173+
* @throws {ScenesApiError} If the API call fails or the response format is invalid.
174+
*/
175+
async putScene(params: PutSceneParams): Promise<SceneResponse> {
176+
return putScene({
177+
iTwinId: params.iTwinId,
178+
sceneId: params.sceneId,
179+
scene: params.scene,
180+
getAccessToken: this.getAccessToken,
181+
baseUrl: this.baseUrl,
182+
});
183+
}
184+
185+
/**
186+
* Update an existing scene's metadata. To replace scene objects, use {@link putScene}.
167187
* @param params.iTwinId – The iTwin's unique identifier.
168188
* @param params.sceneId – The scene's unique identifier.
169189
* @param params.scene – The scene update payload.

packages/scenes-client/src/models/apiParams.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { BulkSceneObjectUpdate, SceneObjectUpdate } from "./object/sceneObjectUp
88
import { GetScenesOptions } from "./scene/getScenesOptions.js";
99
import { SceneCreate } from "./scene/sceneCreate.js";
1010
import { SceneUpdate } from "./scene/sceneUpdate.js";
11+
import { SceneUpsert } from "./scene/sceneUpsert.js";
1112

1213
export type ITwinParams = { iTwinId: string };
1314
export type SceneParams = ITwinParams & { sceneId: string };
@@ -18,6 +19,7 @@ export type GetSceneParams = SceneParams & Pick<GetObjectsOptions, "orderBy">;
1819
export type GetScenesParams = ITwinParams & Omit<GetScenesOptions, "delayMs">;
1920
export type GetAllScenesParams = ITwinParams & GetScenesOptions;
2021
export type PostSceneParams = ITwinParams & { scene: SceneCreate };
22+
export type PutSceneParams = SceneParams & { scene: SceneUpsert };
2123
export type PatchSceneParams = SceneParams & { scene: SceneUpdate };
2224
export type DeleteSceneParams = SceneParams;
2325

packages/scenes-client/src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from "./scene/sceneData.js";
2121
export * from "./scene/sceneDataLinks.js";
2222
export * from "./scene/sceneMinimal.js";
2323
export * from "./scene/sceneUpdate.js";
24+
export * from "./scene/sceneUpsert.js";
2425
export * from "./scene/sceneWithLinks.js";
2526

2627
export * from "./apiParams.js";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3+
* See LICENSE.md in the project root for license terms and full copyright notice.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { SceneCreate } from "./sceneCreate.js";
6+
7+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
8+
export interface SceneUpsert extends Omit<SceneCreate, "id"> {}

packages/scenes-client/tests/integration.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,32 @@ describe("Scenes operation", () => {
168168
expect(upd2.scene.description).toBe(undefined); // removed optional description
169169
});
170170

171+
it("replace scene", async () => {
172+
const upd = await client.putScene({
173+
iTwinId: ITWIN_ID,
174+
sceneId: sceneAId,
175+
scene: {
176+
displayName: "ReplaceA",
177+
description: "Description",
178+
sceneData: { objects: [LAYER_OBJ] },
179+
},
180+
});
181+
182+
expect(upd.scene.displayName).toBe("ReplaceA");
183+
expect(upd.scene.description).toBe("Description");
184+
expect(upd.scene.sceneData.objects).toHaveLength(1);
185+
186+
const upd2 = await client.putScene({
187+
iTwinId: ITWIN_ID,
188+
sceneId: sceneAId,
189+
scene: { displayName: "ReplaceB" },
190+
});
191+
192+
expect(upd2.scene.displayName).toBe("ReplaceB");
193+
expect(upd2.scene.description).toBe(undefined); // removed description
194+
expect(upd2.scene.sceneData.objects).toStrictEqual([]); // removed objects
195+
});
196+
171197
it("delete scene", async () => {
172198
await client.deleteScene({ iTwinId: ITWIN_ID, sceneId: sceneAId });
173199
await expect(client.getScene({ iTwinId: ITWIN_ID, sceneId: sceneAId })).rejects.toMatchObject({

packages/scenes-client/tests/unit.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,27 @@ describe("Scenes Operations", () => {
145145
});
146146
});
147147

148+
it("putScene()", async () => {
149+
fetchMock.mockImplementation(() => createSuccessfulResponse(exampleSceneResponse));
150+
const client = new SceneClient(getAccessToken);
151+
const scenePayload = { displayName: "My Scene", sceneData: { objects: [] } };
152+
await client.putScene({
153+
iTwinId: "itw-1",
154+
sceneId: "scene-1",
155+
scene: scenePayload,
156+
});
157+
158+
verifyFetch(fetchMock, {
159+
url: `${BASE_DOMAIN}/scene-1?iTwinId=itw-1`,
160+
headers: {
161+
"Content-Type": "application/json",
162+
Accept: "application/vnd.bentley.itwin-platform.v1+json",
163+
},
164+
method: "PUT",
165+
body: JSON.stringify(scenePayload),
166+
});
167+
});
168+
148169
it("patchScene()", async () => {
149170
fetchMock.mockImplementation(() => createSuccessfulResponse(exampleSceneMetadataResponse));
150171
const client = new SceneClient(getAccessToken);
@@ -377,6 +398,15 @@ describe("Error Handling", () => {
377398
scene: { displayName: "Test", sceneData: { objects: [] } },
378399
}),
379400
},
401+
{
402+
name: "scene upsert",
403+
method: () =>
404+
client.putScene({
405+
iTwinId: "itw-1",
406+
sceneId: "scene-1",
407+
scene: { displayName: "Replace", sceneData: { objects: [] } },
408+
}),
409+
},
380410
{
381411
name: "scene updates",
382412
method: () =>

0 commit comments

Comments
 (0)